From d3edca1a194f9fdcb802b76f46e15cfcd2134574 Mon Sep 17 00:00:00 2001 From: Daniel Kopka Date: Fri, 29 May 2015 15:04:23 +0200 Subject: [PATCH 001/558] channels fix --- syncano/models/channels.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/syncano/models/channels.py b/syncano/models/channels.py index ae03155..ac8e658 100644 --- a/syncano/models/channels.py +++ b/syncano/models/channels.py @@ -91,6 +91,10 @@ class Channel(Model): >>> channel.poll(callback=callback) """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + TYPE_CHOICES = ( {'display_name': 'Default', 'value': 'default'}, {'display_name': 'Separate rooms', 'value': 'separate_rooms'}, @@ -103,11 +107,12 @@ class Channel(Model): ) name = fields.StringField(max_length=64, primary_key=True) - type = fields.ChoiceField(choices=TYPE_CHOICES, required=False) + type = fields.ChoiceField(choices=TYPE_CHOICES, required=False, default='default') group = fields.IntegerField(label='group id', required=False) group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - custom_publish = fields.BooleanField(default=False) + custom_publish = fields.BooleanField(default=False, required=False) + links = fields.HyperlinkedField(links=LINKS) class Meta: parent = Instance From dd128817353f923a07effd4688e8b9a763229566 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 2 Jun 2015 15:13:21 +0200 Subject: [PATCH 002/558] add pyOpenSSL support packages --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9e6815a..eb37cb0 100644 --- a/setup.py +++ b/setup.py @@ -27,8 +27,11 @@ def readme(): install_requires=[ 'requests==2.7.0', 'certifi', - 'six==1.9.0', + 'ndg-httpsclient==0.4.0', + 'pyasn1==0.1.7', + 'pyOpenSSL==0.15.1', 'python-slugify==0.1.0', + 'six==1.9.0', 'validictory==1.0.0', ], tests_require=[ From e52bfcf589e3bfdbadaca90b86c139afd620b0b8 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 2 Jun 2015 18:24:23 +0200 Subject: [PATCH 003/558] corrected --- tests/integration_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index b4e3a24..881b469 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -332,7 +332,7 @@ def test_source_run(self): trace.reload() self.assertEquals(trace.status, 'success') - self.assertEquals(trace.result, 'IntegrationTest') + self.assertEquals(trace.result, '{"stderror": "", "stdout": "IntegrationTest"}') codebox.delete() @@ -382,5 +382,5 @@ def test_codebox_run(self): trace = webhook.run() self.assertEquals(trace.status, 'success') - self.assertEquals(trace.result, 'IntegrationTest') + self.assertEquals(trace.result, '{"stderror": "", "stdout": "IntegrationTest"}') webhook.delete() From ff95fddc06b6e427b7954fe6e2a91c5d259ae5c0 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 3 Jun 2015 15:38:49 +0200 Subject: [PATCH 004/558] fix --- syncano/models/manager.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 044301a..4395ae6 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -416,6 +416,12 @@ def contribute_to_class(self, model, name): # pragma: no cover def _filter(self, *args, **kwargs): if args and self.endpoint: properties = self.model._meta.get_endpoint_properties(self.endpoint) + + # if 'id' occurs, it should be before 'instance_name' + if 'id' in properties: + properties.remove('id') + properties.insert(0, 'id') + mapped_args = {k: v for k, v in zip(properties, args)} self.properties.update(mapped_args) self.properties.update(kwargs) From ca6f57ccd0e3b73fbef7fea1df34d6591cbd2e62 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 3 Jun 2015 17:03:10 +0200 Subject: [PATCH 005/558] sane conditions --- syncano/models/manager.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 4395ae6..afa2f6d 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -417,10 +417,13 @@ def _filter(self, *args, **kwargs): if args and self.endpoint: properties = self.model._meta.get_endpoint_properties(self.endpoint) - # if 'id' occurs, it should be before 'instance_name' - if 'id' in properties: - properties.remove('id') - properties.insert(0, 'id') + # let user get object by 'id' + too_much_properties = len(args) < len(properties) + id_specified = 'id' in properties + instance_specified = self.model.instance_name + + if too_much_properties and id_specified and instance_specified: + properties = ['id'] mapped_args = {k: v for k, v in zip(properties, args)} self.properties.update(mapped_args) From c7717fbf4b746efac7ed30b55a821883e561696a Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 3 Jun 2015 17:46:22 +0200 Subject: [PATCH 006/558] check if instance is specified --- syncano/models/fields.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index e36e94d..d72b83e 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -74,7 +74,8 @@ def __unicode__(self): return six.u(repr(self)) def __get__(self, instance, owner): - return instance._raw_data.get(self.name, self.default) + if instance is not None: + return instance._raw_data.get(self.name, self.default) def __set__(self, instance, value): if self.read_only and value and instance._raw_data.get(self.name): From 868a1c7822ab239e9e51358dec9f29ad875b1707 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 5 Jun 2015 10:49:46 +0200 Subject: [PATCH 007/558] allow specifying update fields without weird 'data' syntax --- syncano/models/manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 044301a..0a494ce 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -249,7 +249,11 @@ def update(self, *args, **kwargs): """ self.endpoint = 'detail' self.method = self.get_allowed_method('PUT', 'PATCH', 'POST') - self.data = kwargs.pop('data') + self.data = kwargs.pop('data', None) + + if self.data is None: + self.data = kwargs + self._filter(*args, **kwargs) return self.request() From bbae32ae21cb8942377c2833d6786e07611749a8 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 5 Jun 2015 11:00:00 +0200 Subject: [PATCH 008/558] updated docs + simplified --- syncano/models/manager.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 0a494ce..7921dd1 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -239,21 +239,24 @@ def delete(self, *args, **kwargs): @clone def update(self, *args, **kwargs): """ - Updates single instance based on provided arguments. + Updates single instance based on provided arguments. There to ways to do so: + + 1. Django-style update. + 2. By specifying **data** argument. + The **data** is a dictionary of (field, value) pairs used to update the object. Usage:: + instance = Instance.please.update('test-one', description='new one') + instance = Instance.please.update(name='test-one', description='new one') + instance = Instance.please.update('test-one', data={'description': 'new one'}) instance = Instance.please.update(name='test-one', data={'description': 'new one'}) """ self.endpoint = 'detail' self.method = self.get_allowed_method('PUT', 'PATCH', 'POST') - self.data = kwargs.pop('data', None) - - if self.data is None: - self.data = kwargs - + self.data = kwargs.pop('data', kwargs) self._filter(*args, **kwargs) return self.request() From ee67e1964996ab7575c5c8df3ae844285805130c Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 5 Jun 2015 11:15:34 +0200 Subject: [PATCH 009/558] add name field for triggers and schedules --- syncano/models/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/syncano/models/base.py b/syncano/models/base.py index eb23fcc..deade09 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -583,6 +583,7 @@ class Schedule(Model): {'type': 'list', 'name': 'codebox'}, ] + name = fields.StringField(max_length=80) interval_sec = fields.IntegerField(read_only=False, required=False) crontab = fields.StringField(max_length=40, required=False) payload = fields.StringField(required=False) @@ -875,6 +876,7 @@ class Trigger(Model): {'display_name': 'post_delete', 'value': 'post_delete'}, ) + name = fields.StringField(max_length=80) codebox = fields.IntegerField(label='codebox id') klass = fields.StringField(label='class name') signal = fields.ChoiceField(choices=SIGNAL_CHOICES) From 0d1c1dc75a7683d2ee16d188971b6b27c3de2783 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 5 Jun 2015 16:22:14 +0200 Subject: [PATCH 010/558] corrected --- tests/integration_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index b4e3a24..881b469 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -332,7 +332,7 @@ def test_source_run(self): trace.reload() self.assertEquals(trace.status, 'success') - self.assertEquals(trace.result, 'IntegrationTest') + self.assertEquals(trace.result, '{"stderror": "", "stdout": "IntegrationTest"}') codebox.delete() @@ -382,5 +382,5 @@ def test_codebox_run(self): trace = webhook.run() self.assertEquals(trace.status, 'success') - self.assertEquals(trace.result, 'IntegrationTest') + self.assertEquals(trace.result, '{"stderror": "", "stdout": "IntegrationTest"}') webhook.delete() From 17d9d3f7c8ee3a027a35d0f1fb09a490946149ec Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 5 Jun 2015 16:46:56 +0200 Subject: [PATCH 011/558] corrected docstrings --- syncano/models/base.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index deade09..4fcf28a 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -441,12 +441,12 @@ class CodeBox(Model): """ OO wrapper around codeboxes `endpoint `_. + :ivar name: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` :ivar source: :class:`~syncano.models.fields.StringField` :ivar runtime_name: :class:`~syncano.models.fields.ChoiceField` :ivar config: :class:`~syncano.models.fields.Field` - :ivar name: :class:`~syncano.models.fields.StringField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` :ivar created_at: :class:`~syncano.models.fields.DateTimeField` :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` @@ -567,14 +567,13 @@ class Schedule(Model): """ OO wrapper around codebox schedules `endpoint `_. - :ivar description: :class:`~syncano.models.fields.StringField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar source: :class:`~syncano.models.fields.StringField` - :ivar runtime_name: :class:`~syncano.models.fields.ChoiceField` - :ivar config: :class:`~syncano.models.fields.Field` :ivar name: :class:`~syncano.models.fields.StringField` + :ivar interval_sec: :class:`~syncano.models.fields.IntegerField` + :ivar crontab: :class:`~syncano.models.fields.StringField` + :ivar payload: :class:`~syncano.models.fields.HyperliStringFieldnkedField` :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + :ivar scheduled_next: :class:`~syncano.models.fields.DateTimeField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` """ LINKS = [ @@ -856,6 +855,7 @@ class Trigger(Model): """ OO wrapper around triggers `endpoint `_. + :ivar name: :class:`~syncano.models.fields.StringField` :ivar codebox: :class:`~syncano.models.fields.IntegerField` :ivar klass: :class:`~syncano.models.fields.StringField` :ivar signal: :class:`~syncano.models.fields.ChoiceField` From aa943893d0f3ab72b73fb2d4acd6e6347c194e82 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 5 Jun 2015 17:15:17 +0200 Subject: [PATCH 012/558] added support for webhooks' and triggers' traces --- syncano/models/base.py | 72 ++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index eb23fcc..3b02495 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -532,7 +532,17 @@ def run(self, **payload): return CodeBoxTrace(**response) -class CodeBoxTrace(Model): +class Trace(Model): + """ + Base class for traces. + + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + STATUS_CHOICES = ( {'display_name': 'Success', 'value': 'success'}, {'display_name': 'Failure', 'value': 'failure'}, @@ -549,6 +559,8 @@ class CodeBoxTrace(Model): result = fields.StringField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) + +class CodeBoxTrace(Trace): class Meta: parent = CodeBox endpoints = { @@ -604,33 +616,7 @@ class Meta: } -class Trace(Model): - """ - OO wrapper around codebox schedules traces `endpoint `_. - - :ivar status: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` - :ivar result: :class:`~syncano.models.fields.StringField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - - STATUS_CHOICES = ( - {'display_name': 'Success', 'value': 'success'}, - {'display_name': 'Failure', 'value': 'failure'}, - {'display_name': 'Timeout', 'value': 'timeout'}, - {'display_name': 'Pending', 'value': 'pending'}, - ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) - duration = fields.IntegerField(read_only=True, required=False) - +class ScheduleTrace(Trace): class Meta: parent = Schedule endpoints = { @@ -896,6 +882,21 @@ class Meta: } +class TriggerTrace(Trace): + class Meta: + parent = Trigger + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } + + class WebhookResult(object): """ OO wrapper around result of :meth:`~syncano.models.base.Webhook.run` method. @@ -991,3 +992,18 @@ def run(self, **payload): } response = connection.request('POST', endpoint, **request) return self.RESULT_CLASS(**response) + + +class WebhookTrace(Trace): + class Meta: + parent = Webhook + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } From 048a73d22e2971219a13e7d76c15332000aa4f12 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 5 Jun 2015 17:31:37 +0200 Subject: [PATCH 013/558] corrected --- syncano/models/base.py | 84 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 3b02495..fb99d63 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -532,10 +532,8 @@ def run(self, **payload): return CodeBoxTrace(**response) -class Trace(Model): +class CodeBoxTrace(Model): """ - Base class for traces. - :ivar status: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` @@ -559,8 +557,6 @@ class Trace(Model): result = fields.StringField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) - -class CodeBoxTrace(Trace): class Meta: parent = CodeBox endpoints = { @@ -616,7 +612,31 @@ class Meta: } -class ScheduleTrace(Trace): +class ScheduleTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + class Meta: parent = Schedule endpoints = { @@ -882,7 +902,31 @@ class Meta: } -class TriggerTrace(Trace): +class TriggerTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + class Meta: parent = Trigger endpoints = { @@ -994,7 +1038,31 @@ def run(self, **payload): return self.RESULT_CLASS(**response) -class WebhookTrace(Trace): +class WebhookTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + class Meta: parent = Webhook endpoints = { From fb92a0dedbb7a400dd4f09e4a3b80a12ccace54f Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 8 Jun 2015 11:15:38 +0200 Subject: [PATCH 014/558] test added --- tests/test_manager.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_manager.py b/tests/test_manager.py index f3bd52b..c5f9946 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -164,6 +164,13 @@ def test_update(self, clone_mock, filter_mock, request_mock): self.assertEqual(self.manager.endpoint, 'detail') self.assertEqual(self.manager.data, {'x': 1, 'y': 2}) + result = self.manager.update(1, 2, a=1, b=2, x=3, y=2) + self.assertEqual(request_mock, result) + + self.assertEqual(self.manager.method, 'PUT') + self.assertEqual(self.manager.endpoint, 'detail') + self.assertEqual(self.manager.data, {'x': 3, 'y': 2, 'a': 1, 'b': 2}) + @mock.patch('syncano.models.manager.Manager.update') @mock.patch('syncano.models.manager.Manager.create') def test_update_or_create(self, create_mock, update_mock): From 5a9537de7e06a96fc719e532be39321db8f833c7 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 8 Jun 2015 11:58:39 +0200 Subject: [PATCH 015/558] replace webhook result with trace --- syncano/models/base.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index fb99d63..98a168c 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -941,21 +941,6 @@ class Meta: } -class WebhookResult(object): - """ - OO wrapper around result of :meth:`~syncano.models.base.Webhook.run` method. - """ - def __init__(self, status, duration, result, executed_at): - self.status = status - self.duration = duration - self.result = result - self.executed_at = executed_at - - if isinstance(executed_at, six.string_types): - executed_at = executed_at.split('Z')[0] - self.executed_at = datetime.strptime(executed_at, '%Y-%m-%dT%H:%M:%S.%f') - - class Webhook(Model): """ OO wrapper around webhooks `endpoint `_. @@ -979,8 +964,6 @@ class Webhook(Model): >>> wh.run(variable_one=1, variable_two=2) """ - RESULT_CLASS = WebhookResult - LINKS = ( {'type': 'detail', 'name': 'self'}, {'type': 'detail', 'name': 'codebox'}, @@ -1035,7 +1018,7 @@ def run(self, **payload): } } response = connection.request('POST', endpoint, **request) - return self.RESULT_CLASS(**response) + return WebhookTrace(**response) class WebhookTrace(Model): From b14830563d0d51745b26517505dd1b774e71a123 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 8 Jun 2015 12:16:44 +0200 Subject: [PATCH 016/558] necessary fields added --- syncano/models/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syncano/models/base.py b/syncano/models/base.py index 98a168c..14e5fd8 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -1018,6 +1018,7 @@ def run(self, **payload): } } response = connection.request('POST', endpoint, **request) + response.update({'instance_name': self.instance_name, 'webhook_slug': self.slug}) return WebhookTrace(**response) From 497c42945544e56e36c266773673315b45ceed16 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 8 Jun 2015 12:18:38 +0200 Subject: [PATCH 017/558] corrected --- tests/test_manager.py | 4 ++-- tests/test_models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index f3bd52b..9dac93f 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -4,7 +4,7 @@ from syncano.exceptions import (SyncanoDoesNotExist, SyncanoRequestError, SyncanoValueError) from syncano.models.base import (CodeBox, CodeBoxTrace, Instance, Object, - Webhook, WebhookResult) + Webhook, WebhookTrace) try: from unittest import mock @@ -433,7 +433,7 @@ def test_run(self, clone_mock, filter_mock, request_mock): self.assertFalse(request_mock.called) result = self.manager.run(1, 2, a=1, b=2, payload={'x': 1, 'y': 2}) - self.assertIsInstance(result, WebhookResult) + self.assertIsInstance(result, WebhookTrace) self.assertEqual(result.status, 'success') self.assertEqual(result.duration, 937) self.assertEqual(result.result, '1') diff --git a/tests/test_models.py b/tests/test_models.py index 23ecd79..75e4142 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -3,7 +3,7 @@ from syncano.exceptions import SyncanoValidationError, SyncanoValueError from syncano.models import (CodeBox, CodeBoxTrace, Instance, Object, Webhook, - WebhookResult) + WebhookTrace) try: from unittest import mock @@ -368,7 +368,7 @@ def test_run(self, connection_mock): result = model.run(x=1, y=2) self.assertTrue(connection_mock.called) self.assertTrue(connection_mock.request.called) - self.assertIsInstance(result, WebhookResult) + self.assertIsInstance(result, WebhookTrace) self.assertEqual(result.status, 'success') self.assertEqual(result.duration, 937) self.assertEqual(result.result, '1') From 5cb607479508abd89afab2eb556e3d10f2458f10 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 8 Jun 2015 12:22:02 +0200 Subject: [PATCH 018/558] replace RESULT_CLASS with Trace --- syncano/models/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 044301a..deb06d0 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -556,7 +556,7 @@ def run(self, *args, **kwargs): response = self.request() # Workaround for circular import - return registry.Webhook.RESULT_CLASS(**response) + return registry.WebhookTrace(**response) class ObjectManager(Manager): From 7294e3d8ea25896f78099faf9bf6425f3b454d37 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 8 Jun 2015 16:11:53 +0200 Subject: [PATCH 019/558] do not check instance --- syncano/models/manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index b17ecb2..45b031e 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -427,9 +427,8 @@ def _filter(self, *args, **kwargs): # let user get object by 'id' too_much_properties = len(args) < len(properties) id_specified = 'id' in properties - instance_specified = self.model.instance_name - if too_much_properties and id_specified and instance_specified: + if too_much_properties and id_specified: properties = ['id'] mapped_args = {k: v for k, v in zip(properties, args)} From dda464028b7332ca7d712d1e8c50218b90c7e67a Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 8 Jun 2015 17:10:04 +0200 Subject: [PATCH 020/558] slug to name --- syncano/models/base.py | 20 ++++++++++---------- syncano/models/fields.py | 2 +- tests/integration_test.py | 4 ++-- tests/test_models.py | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 6a9605f..f8edde9 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -947,21 +947,21 @@ class Webhook(Model): """ OO wrapper around webhooks `endpoint `_. - :ivar slug: :class:`~syncano.models.fields.SlugField` + :ivar name: :class:`~syncano.models.fields.SlugField` :ivar codebox: :class:`~syncano.models.fields.IntegerField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` .. note:: **WebHook** has special method called ``run`` which will execute related codebox:: - >>> Webhook.please.run('instance-name', 'webhook-slug') - >>> Webhook.please.run('instance-name', 'webhook-slug', payload={'variable_one': 1, 'variable_two': 2}) - >>> Webhook.please.run('instance-name', 'webhook-slug', + >>> Webhook.please.run('instance-name', 'webhook-name') + >>> Webhook.please.run('instance-name', 'webhook-name', payload={'variable_one': 1, 'variable_two': 2}) + >>> Webhook.please.run('instance-name', 'webhook-name', payload="{\"variable_one\": 1, \"variable_two\": 2}") or via instance:: - >>> wh = Webhook.please.get('instance-name', 'webhook-slug') + >>> wh = Webhook.please.get('instance-name', 'webhook-name') >>> wh.run() >>> wh.run(variable_one=1, variable_two=2) @@ -971,7 +971,7 @@ class Webhook(Model): {'type': 'detail', 'name': 'codebox'}, ) - slug = fields.SlugField(max_length=50, primary_key=True) + name = fields.SlugField(max_length=50, primary_key=True) codebox = fields.IntegerField(label='codebox id') public = fields.BooleanField(required=False, default=False) public_link = fields.ChoiceField(required=False, read_only=True) @@ -984,7 +984,7 @@ class Meta: endpoints = { 'detail': { 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/webhooks/{slug}/', + 'path': '/webhooks/{name}/', }, 'list': { 'methods': ['post', 'get'], @@ -992,7 +992,7 @@ class Meta: }, 'run': { 'methods': ['post'], - 'path': '/webhooks/{slug}/run/', + 'path': '/webhooks/{name}/run/', }, 'public': { 'methods': ['get'], @@ -1004,7 +1004,7 @@ def run(self, **payload): """ Usage:: - >>> wh = Webhook.please.get('instance-name', 'webhook-slug') + >>> wh = Webhook.please.get('instance-name', 'webhook-name') >>> wh.run() >>> wh.run(variable_one=1, variable_two=2) """ @@ -1020,7 +1020,7 @@ def run(self, **payload): } } response = connection.request('POST', endpoint, **request) - response.update({'instance_name': self.instance_name, 'webhook_slug': self.slug}) + response.update({'instance_name': self.instance_name, 'webhook_name': self.name}) return WebhookTrace(**response) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index d72b83e..84a8b17 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -581,7 +581,7 @@ def to_native(self, value): 'integer': IntegerField, 'float': FloatField, 'boolean': BooleanField, - 'slug': SlugField, + 'name': SlugField, 'email': EmailField, 'choice': ChoiceField, 'date': DateField, diff --git a/tests/integration_test.py b/tests/integration_test.py index 881b469..05b66b8 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -368,7 +368,7 @@ def test_create(self): webhook = self.model.please.create( instance_name=self.instance.name, codebox=self.codebox.id, - slug='wh%s' % self.generate_hash()[:10], + name='wh%s' % self.generate_hash()[:10], ) webhook.delete() @@ -377,7 +377,7 @@ def test_codebox_run(self): webhook = self.model.please.create( instance_name=self.instance.name, codebox=self.codebox.id, - slug='wh%s' % self.generate_hash()[:10], + name='wh%s' % self.generate_hash()[:10], ) trace = webhook.run() diff --git a/tests/test_models.py b/tests/test_models.py index 75e4142..6c2ca31 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -354,7 +354,7 @@ def setUp(self): @mock.patch('syncano.models.Webhook._get_connection') def test_run(self, connection_mock): - model = Webhook(instance_name='test', slug='slug', links={'run': '/v1/instances/test/webhooks/slug/run/'}) + model = Webhook(instance_name='test', name='name', links={'run': '/v1/instances/test/webhooks/name/run/'}) connection_mock.return_value = connection_mock connection_mock.request.return_value = { 'status': 'success', @@ -377,7 +377,7 @@ def test_run(self, connection_mock): connection_mock.assert_called_once_with(x=1, y=2) connection_mock.request.assert_called_once_with( 'POST', - '/v1/instances/test/webhooks/slug/run/', + '/v1/instances/test/webhooks/name/run/', data={'payload': '{"y": 2, "x": 1}'} ) From 595870a3a9571829017d9eab55d5f514c61edea2 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 8 Jun 2015 17:30:59 +0200 Subject: [PATCH 021/558] remove unused import --- syncano/models/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index f8edde9..9a4c8a5 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -3,7 +3,6 @@ import inspect import json from copy import deepcopy -from datetime import datetime import six From 10ac03251307133c79df57505a50252721216958 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 9 Jun 2015 11:10:21 +0200 Subject: [PATCH 022/558] fix typo --- docs/source/interacting.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/interacting.rst b/docs/source/interacting.rst index e4dc680..b77f2f4 100644 --- a/docs/source/interacting.rst +++ b/docs/source/interacting.rst @@ -268,4 +268,4 @@ Some settings can be overwritten via environmental variables e.g: $ export SYNCANO_INSTANCE=test .. warning:: - **DEBUG** loglevel will **disbale** SSL cert check. + **DEBUG** loglevel will **disable** SSL cert check. From 53cfbebf83ca021332ef1162bef74a5f659f8f84 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 9 Jun 2015 19:11:45 +0200 Subject: [PATCH 023/558] add codebox field in Schedule --- syncano/models/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/syncano/models/base.py b/syncano/models/base.py index 9a4c8a5..51093fe 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -575,6 +575,7 @@ class Schedule(Model): OO wrapper around codebox schedules `endpoint `_. :ivar name: :class:`~syncano.models.fields.StringField` + :ivar codebox: :class:`~syncano.models.fields.IntegerField` :ivar interval_sec: :class:`~syncano.models.fields.IntegerField` :ivar crontab: :class:`~syncano.models.fields.StringField` :ivar payload: :class:`~syncano.models.fields.HyperliStringFieldnkedField` @@ -590,6 +591,7 @@ class Schedule(Model): ] name = fields.StringField(max_length=80) + codebox = fields.IntegerField(label='codebox id') interval_sec = fields.IntegerField(read_only=False, required=False) crontab = fields.StringField(max_length=40, required=False) payload = fields.StringField(required=False) From 58c9e6a2378950f7f27645bb67b274d5658a1c1e Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Thu, 11 Jun 2015 12:06:17 +0200 Subject: [PATCH 024/558] add update requests for schedule --- syncano/models/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 51093fe..75e6990 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -603,7 +603,7 @@ class Meta: parent = Instance endpoints = { 'detail': { - 'methods': ['get', 'delete'], + 'methods': ['put', 'get', 'patch', 'delete'], 'path': '/schedules/{id}/', }, 'list': { From 4f6edfbbc6db6896f67007695a1adb25b868ba28 Mon Sep 17 00:00:00 2001 From: Mariusz Wisniewski Date: Mon, 15 Jun 2015 14:56:29 -0400 Subject: [PATCH 025/558] Readme update Added info about docs and quick start guide. --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b769961..70861fb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,18 @@ # Syncano v4.0 -[Docs](http://syncano.github.io/syncano-python/) +## Python QuickStart Guide +--- +You can find quick start on installing and using Syncano's Python library (for both Obj-C and Swift) in our [documentation](http://docs.syncano.com/v4.0/docs/python). + +For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/v4.0/docs/getting-stared-with-syncano) should be very helpful. + +In case you need help working with the library - email us at libraries@syncano.com - we will be happy to help! + +You can also find library reference hosted on GitHub pages [here](http://syncano.github.io/syncano-python/). ## Backwards incompatible changes +--- Version 4.0 is designed for new release of Syncano platform and it's **not compatible** with any previous releases. @@ -13,4 +22,4 @@ and it can be installed directly from pip via: ``` pip install syncano==0.6.2 --pre -``` \ No newline at end of file +``` From 3800d248161e31ec02882bb25f20dfe753d2c308 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 17 Jun 2015 12:17:41 +0200 Subject: [PATCH 026/558] changed name to label if not unique --- syncano/models/base.py | 12 ++++++------ tests/integration_test.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 51093fe..cbd666d 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -440,7 +440,7 @@ class CodeBox(Model): """ OO wrapper around codeboxes `endpoint `_. - :ivar name: :class:`~syncano.models.fields.StringField` + :ivar label: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` :ivar source: :class:`~syncano.models.fields.StringField` :ivar runtime_name: :class:`~syncano.models.fields.ChoiceField` @@ -477,7 +477,7 @@ class CodeBox(Model): {'display_name': 'ruby', 'value': 'ruby'}, ) - name = fields.StringField(max_length=80) + label = fields.StringField(max_length=80) description = fields.StringField(required=False) source = fields.StringField() runtime_name = fields.ChoiceField(choices=RUNTIME_CHOICES) @@ -574,7 +574,7 @@ class Schedule(Model): """ OO wrapper around codebox schedules `endpoint `_. - :ivar name: :class:`~syncano.models.fields.StringField` + :ivar label: :class:`~syncano.models.fields.StringField` :ivar codebox: :class:`~syncano.models.fields.IntegerField` :ivar interval_sec: :class:`~syncano.models.fields.IntegerField` :ivar crontab: :class:`~syncano.models.fields.StringField` @@ -590,7 +590,7 @@ class Schedule(Model): {'type': 'list', 'name': 'codebox'}, ] - name = fields.StringField(max_length=80) + label = fields.StringField(max_length=80) codebox = fields.IntegerField(label='codebox id') interval_sec = fields.IntegerField(read_only=False, required=False) crontab = fields.StringField(max_length=40, required=False) @@ -862,7 +862,7 @@ class Trigger(Model): """ OO wrapper around triggers `endpoint `_. - :ivar name: :class:`~syncano.models.fields.StringField` + :ivar label: :class:`~syncano.models.fields.StringField` :ivar codebox: :class:`~syncano.models.fields.IntegerField` :ivar klass: :class:`~syncano.models.fields.StringField` :ivar signal: :class:`~syncano.models.fields.ChoiceField` @@ -883,7 +883,7 @@ class Trigger(Model): {'display_name': 'post_delete', 'value': 'post_delete'}, ) - name = fields.StringField(max_length=80) + label = fields.StringField(max_length=80) codebox = fields.IntegerField(label='codebox id') klass = fields.StringField(label='class name') signal = fields.ChoiceField(choices=SIGNAL_CHOICES) diff --git a/tests/integration_test.py b/tests/integration_test.py index 05b66b8..6ca1cc6 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -295,7 +295,7 @@ def test_list(self): def test_create(self): codebox = self.model.please.create( instance_name=self.instance.name, - name='cb%s' % self.generate_hash()[:10], + label='cb%s' % self.generate_hash()[:10], runtime_name='python', source='print "IntegrationTest"' ) @@ -305,7 +305,7 @@ def test_create(self): def test_update(self): codebox = self.model.please.create( instance_name=self.instance.name, - name='cb%s' % self.generate_hash()[:10], + label='cb%s' % self.generate_hash()[:10], runtime_name='python', source='print "IntegrationTest"' ) @@ -321,7 +321,7 @@ def test_update(self): def test_source_run(self): codebox = self.model.please.create( instance_name=self.instance.name, - name='cb%s' % self.generate_hash()[:10], + label='cb%s' % self.generate_hash()[:10], runtime_name='python', source='print "IntegrationTest"' ) @@ -332,7 +332,7 @@ def test_source_run(self): trace.reload() self.assertEquals(trace.status, 'success') - self.assertEquals(trace.result, '{"stderror": "", "stdout": "IntegrationTest"}') + self.assertEquals(trace.result, u'{u\'stderr\': u\'\', u\'stdout\': u\'IntegrationTest\'}') codebox.delete() @@ -346,7 +346,7 @@ def setUpClass(cls): cls.codebox = CodeBox.please.create( instance_name=cls.instance.name, - name='cb%s' % cls.generate_hash()[:10], + label='cb%s' % cls.generate_hash()[:10], runtime_name='python', source='print "IntegrationTest"' ) @@ -382,5 +382,5 @@ def test_codebox_run(self): trace = webhook.run() self.assertEquals(trace.status, 'success') - self.assertEquals(trace.result, '{"stderror": "", "stdout": "IntegrationTest"}') + self.assertEquals(trace.result, u'{u\'stderr\': u\'\', u\'stdout\': u\'IntegrationTest\'}') webhook.delete() From e078325a526fe6f07fd07b53f7a7e071b1fed42a Mon Sep 17 00:00:00 2001 From: Mariusz Wisniewski Date: Wed, 17 Jun 2015 08:54:54 -0400 Subject: [PATCH 027/558] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70861fb..9c09456 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Python QuickStart Guide --- -You can find quick start on installing and using Syncano's Python library (for both Obj-C and Swift) in our [documentation](http://docs.syncano.com/v4.0/docs/python). +You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/v4.0/docs/python). For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/v4.0/docs/getting-stared-with-syncano) should be very helpful. From 0c16474d6aa164bbdcd6e3e52199bfa6dbbf3c93 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 17 Jun 2015 15:14:57 +0200 Subject: [PATCH 028/558] make sure to convert list to string --- syncano/exceptions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/syncano/exceptions.py b/syncano/exceptions.py index 7ce09f4..7fdfbc8 100644 --- a/syncano/exceptions.py +++ b/syncano/exceptions.py @@ -32,7 +32,9 @@ def __init__(self, status_code, reason, *args): self.status_code = status_code if isinstance(reason, dict): - message = ''.join(reason.get(k, '') for k in ['detail', 'error', '__all__']) + joined_details = (''.join(reason.get(k, '')) for k in ['detail', 'error', '__all__']) + message = ''.join(joined_details) + if not message: for name, value in six.iteritems(reason): if isinstance(value, (list, dict)): From a728efcd17f652fc964b2da5cf5ad0ae8d071038 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Thu, 18 Jun 2015 10:55:30 +0200 Subject: [PATCH 029/558] add field mapping --- syncano/models/base.py | 7 +++++-- syncano/models/fields.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 74a8a8a..4c6c73b 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -204,7 +204,10 @@ def to_native(self): value = getattr(self, field.name) if not value and field.blank: continue - data[field.name] = field.to_native(value) + if field.mapping: + data[field.mapping] = field.to_native(value) + else: + data[field.name] = field.to_native(value) return data def get_endpoint_data(self): @@ -885,7 +888,7 @@ class Trigger(Model): label = fields.StringField(max_length=80) codebox = fields.IntegerField(label='codebox id') - klass = fields.StringField(label='class name') + klass = fields.StringField(label='class name', mapping='class') signal = fields.ChoiceField(choices=SIGNAL_CHOICES) links = fields.HyperlinkedField(links=LINKS) created_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 84a8b17..56cf94d 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -37,6 +37,7 @@ def __init__(self, name=None, **kwargs): self.read_only = kwargs.pop('read_only', self.read_only) self.blank = kwargs.pop('blank', self.blank) self.label = kwargs.pop('label', None) + self.mapping = kwargs.pop('mapping', None) self.max_length = kwargs.pop('max_length', None) self.min_length = kwargs.pop('min_length', None) self.query_allowed = kwargs.pop('query_allowed', self.query_allowed) From 73fc7386243ae71829a7a46beaed8102afce4556 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 26 Jun 2015 13:19:05 +0200 Subject: [PATCH 030/558] raw solution --- syncano/connection.py | 10 +++++++++- syncano/models/base.py | 6 +++++- syncano/models/fields.py | 9 ++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 5093218..9cdb484 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -161,6 +161,7 @@ def make_request(self, method_name, path, **kwargs): :raises SyncanoValueError: if invalid request method was chosen :raises SyncanoRequestError: if something went wrong during the request """ + files = kwargs.get('data', {}).pop('files', None) params = self.build_params(kwargs) method = getattr(self.session, method_name.lower(), None) @@ -181,7 +182,6 @@ def make_request(self, method_name, path, **kwargs): # Encode request payload if 'data' in params and not isinstance(params['data'], six.string_types): params['data'] = json.dumps(params['data']) - url = self.build_url(path) response = method(url, **params) @@ -197,6 +197,14 @@ def make_request(self, method_name, path, **kwargs): if is_client_error(response.status_code): raise SyncanoRequestError(response.status_code, content) + if files: + method = getattr(self.session, 'patch') + params.pop('data') + params['headers'].pop('content-type') + params['files'] = files + response = method(url + '{}/'.format(content['id']), **params) + content = response.json() + # Other errors if not is_success(response.status_code): self.logger.debug('Request Error: %s', url) diff --git a/syncano/models/base.py b/syncano/models/base.py index 74a8a8a..4bb6d86 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -128,6 +128,7 @@ def save(self, **kwargs): endpoint = self._meta.resolve_endpoint(endpoint_name, properties) request = {'data': data} + response = connection.request(method, endpoint, **request) self.to_python(response) @@ -204,7 +205,10 @@ def to_native(self): value = getattr(self, field.name) if not value and field.blank: continue - data[field.name] = field.to_native(value) + + param_name = getattr(field, 'param_name', field.name) + data[param_name] = field.to_native(value) + return data def get_endpoint_data(self): diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 84a8b17..9a4b436 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -467,6 +467,13 @@ def to_native(self, value): return value +class FileField(WritableField): + param_name = 'files' + + def to_native(self, value): + return {self.name: value} + + class JSONField(WritableField): query_allowed = False schema = None @@ -575,7 +582,7 @@ def to_native(self, value): MAPPING = { 'string': StringField, 'text': StringField, - 'file': StringField, + 'file': FileField, 'ref': StringField, 'reference': ReferenceField, 'integer': IntegerField, From 0461acc71cc3ea8b709d4c469209fea5f70f2915 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 26 Jun 2015 13:50:13 +0200 Subject: [PATCH 031/558] extract response error checking, correct updating filefield --- syncano/connection.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 9cdb484..b60f712 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -184,7 +184,24 @@ def make_request(self, method_name, path, **kwargs): params['data'] = json.dumps(params['data']) url = self.build_url(path) response = method(url, **params) + content = self.get_response_content(url, response) + if files: + params.pop('data') + params['headers'].pop('content-type') + params['files'] = files + + if response.status_code == 201: + url = '{}{}/'.format(url, content['id']) + + patch = getattr(self.session, 'patch') + response = patch(url, **params) + + content = self.get_response_content(url, response) + + return content + + def get_response_content(self, url, response): try: content = response.json() except ValueError: @@ -197,14 +214,6 @@ def make_request(self, method_name, path, **kwargs): if is_client_error(response.status_code): raise SyncanoRequestError(response.status_code, content) - if files: - method = getattr(self.session, 'patch') - params.pop('data') - params['headers'].pop('content-type') - params['files'] = files - response = method(url + '{}/'.format(content['id']), **params) - content = response.json() - # Other errors if not is_success(response.status_code): self.logger.debug('Request Error: %s', url) From b1dfa2f378c892297f9656551943a2780a953b21 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 26 Jun 2015 15:54:57 +0200 Subject: [PATCH 032/558] add comments --- syncano/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index b60f712..be8b6dd 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -187,6 +187,7 @@ def make_request(self, method_name, path, **kwargs): content = self.get_response_content(url, response) if files: + # remove 'data' and 'content-type' to avoid "ValueError: Data must not be a string." params.pop('data') params['headers'].pop('content-type') params['files'] = files @@ -195,8 +196,8 @@ def make_request(self, method_name, path, **kwargs): url = '{}{}/'.format(url, content['id']) patch = getattr(self.session, 'patch') + # second request is needed to upload a file response = patch(url, **params) - content = self.get_response_content(url, response) return content From ed351cf3e0b379028784ef4cecab0703578c229b Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 29 Jun 2015 16:45:58 +0200 Subject: [PATCH 033/558] changed klass to class_name --- syncano/models/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 4c6c73b..507f70e 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -867,7 +867,7 @@ class Trigger(Model): :ivar label: :class:`~syncano.models.fields.StringField` :ivar codebox: :class:`~syncano.models.fields.IntegerField` - :ivar klass: :class:`~syncano.models.fields.StringField` + :ivar class_name: :class:`~syncano.models.fields.StringField` :ivar signal: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` :ivar created_at: :class:`~syncano.models.fields.DateTimeField` @@ -888,7 +888,7 @@ class Trigger(Model): label = fields.StringField(max_length=80) codebox = fields.IntegerField(label='codebox id') - klass = fields.StringField(label='class name', mapping='class') + class_name = fields.StringField(label='class name', mapping='class') signal = fields.ChoiceField(choices=SIGNAL_CHOICES) links = fields.HyperlinkedField(links=LINKS) created_at = fields.DateTimeField(read_only=True, required=False) From 67d7c1e86c1e7e1b7db476a07dc49d97f3e018e3 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 30 Jun 2015 11:39:23 +0200 Subject: [PATCH 034/558] add permission fields to apikey model --- syncano/models/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/syncano/models/base.py b/syncano/models/base.py index 4c6c73b..3620190 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -348,6 +348,8 @@ class ApiKey(Model): OO wrapper around instance api keys `endpoint `_. :ivar api_key: :class:`~syncano.models.fields.StringField` + :ivar allow_user_create: :class:`~syncano.models.fields.StringField` + :ivar ignore_acl: :class:`~syncano.models.fields.StringField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` """ LINKS = [ @@ -355,6 +357,8 @@ class ApiKey(Model): ] api_key = fields.StringField(read_only=True, required=False) + allow_user_create = fields.BooleanField(required=False, default=False) + ignore_acl = fields.BooleanField(required=False, default=False) links = fields.HyperlinkedField(links=LINKS) class Meta: From 1c4890c20599c80509af11499d8e5e4bb916f6b9 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 30 Jun 2015 11:40:47 +0200 Subject: [PATCH 035/558] correct docstring --- syncano/models/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 3620190..00fda87 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -348,8 +348,8 @@ class ApiKey(Model): OO wrapper around instance api keys `endpoint `_. :ivar api_key: :class:`~syncano.models.fields.StringField` - :ivar allow_user_create: :class:`~syncano.models.fields.StringField` - :ivar ignore_acl: :class:`~syncano.models.fields.StringField` + :ivar allow_user_create: :class:`~syncano.models.fields.BooleanField` + :ivar ignore_acl: :class:`~syncano.models.fields.BooleanField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` """ LINKS = [ From eb1e8cce83afbdb3b9193cc39381fed400bf520b Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 30 Jun 2015 13:19:40 +0200 Subject: [PATCH 036/558] add reset_link support for webhooks --- syncano/models/base.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/syncano/models/base.py b/syncano/models/base.py index 507f70e..8b3f907 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -998,6 +998,10 @@ class Meta: 'methods': ['post'], 'path': '/webhooks/{name}/run/', }, + 'reset': { + 'methods': ['post'], + 'path': '/webhooks/{name}/reset_link/', + }, 'public': { 'methods': ['get'], 'path': 'webhooks/p/{public_link}/', @@ -1027,6 +1031,18 @@ def run(self, **payload): response.update({'instance_name': self.instance_name, 'webhook_name': self.name}) return WebhookTrace(**response) + def reset(self, **payload): + """ + Usage:: + + >>> wh = Webhook.please.get('instance-name', 'webhook-name') + >>> wh.reset() + """ + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('reset', properties) + connection = self._get_connection(**payload) + return connection.request('POST', endpoint) + class WebhookTrace(Model): """ From cd1439e4c96fb600d16f76f456888fe31e8f5b61 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 13 Jul 2015 18:07:23 +0200 Subject: [PATCH 037/558] add users model --- syncano/models/base.py | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 1cd5939..2cccd00 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -320,6 +320,7 @@ class Instance(Model): {'type': 'list', 'name': 'runtimes'}, {'type': 'list', 'name': 'api_keys'}, {'type': 'list', 'name': 'triggers'}, + {'type': 'list', 'name': 'users'}, {'type': 'list', 'name': 'webhooks'}, {'type': 'list', 'name': 'schedules'}, ) @@ -1011,7 +1012,7 @@ class Meta: }, 'public': { 'methods': ['get'], - 'path': 'webhooks/p/{public_link}/', + 'path': '/webhooks/p/{public_link}/', } } @@ -1088,3 +1089,51 @@ class Meta: 'path': '/traces/', } } + + +class User(Model): + """ + OO wrapper around instances `endpoint `_. + + :ivar username: :class:`~syncano.models.fields.StringField` + :ivar password: :class:`~syncano.models.fields.StringField` + :ivar user_key: :class:`~syncano.models.fields.StringField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + username = fields.StringField(max_length=64, required=True) + password = fields.StringField(read_only=False, required=True) + user_key = fields.StringField(read_only=True, required=False) + + links = fields.HyperlinkedField(links=LINKS) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'patch', 'put', 'get'], + 'path': '/users/{id}/', + }, + 'reset_key': { + 'methods': ['post'], + 'path': '/users/{id}/reset_key/', + }, + 'list': { + 'methods': ['get'], + 'path': '/users/', + } + } + + def reset_key(self, **payload): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('reset_key', properties) + connection = self._get_connection(**payload) + return connection.request('POST', endpoint) From 4433227fd81a1ee62e9e1a6e02068233af70c7a5 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 13 Jul 2015 19:09:44 +0200 Subject: [PATCH 038/558] auth for user + requests --- syncano/connection.py | 70 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index be8b6dd..a4aacd2 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -60,13 +60,23 @@ class Connection(object): """ AUTH_SUFFIX = 'v1/account/auth' + USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' CONTENT_TYPE = 'application/json' def __init__(self, host=None, email=None, password=None, api_key=None, **kwargs): self.host = host or syncano.API_ROOT self.email = email or syncano.EMAIL self.password = password or syncano.PASSWORD + self.api_key = api_key or syncano.APIKEY + + # instance indicates if we want to connect User or Admin + self.instance_name = kwargs.get('instance_name') + self.user_key = kwargs.get('user_key') + + if self.api_key and self.instance_name: + self.AUTH_SUFFIX = self.USER_AUTH_SUFFIX.format(name=self.instance_name) + self.logger = kwargs.get('logger') or syncano.logger self.timeout = kwargs.get('timeout') or 30 self.session = requests.Session() @@ -88,7 +98,12 @@ def build_params(self, params): if 'content-type' not in params['headers']: params['headers']['content-type'] = self.CONTENT_TYPE - if self.api_key and 'Authorization' not in params['headers']: + if self.user_key: + params['headers'].update({ + 'X-USER-KEY': self.user_key, + 'X-API-KEY': self.api_key + }) + elif self.api_key and 'Authorization' not in params['headers']: params['headers']['Authorization'] = 'ApiKey %s' % self.api_key # We don't need to check SSL cert in DEBUG mode @@ -143,7 +158,10 @@ def request(self, method_name, path, **kwargs): """ if not self.is_authenticated(): - self.authenticate() + if self.instance_name: + self.authenticate() + else: + self.authenticate_user() return self.make_request(method_name, path, **kwargs) @@ -262,12 +280,60 @@ def authenticate(self, email=None, password=None): data = {'email': email, 'password': password} response = self.make_request('POST', self.AUTH_SUFFIX, data=data) + account_key = response.get('account_key') self.api_key = account_key self.logger.debug('Authentication successful: %s', account_key) return account_key + def authenticate_user(self, email=None, password=None, api_key=None): + """ + :type email: string + :param email: Your Syncano account email address + + :type password: string + :param password: Your Syncano password + + :type api_key: string + :param api_key: Your Syncano api_key for instance + + :rtype: string + :return: Your ``User Key`` + """ + + if self.is_authenticated(): + self.logger.debug('Connection already authenticated: %s', self.user_key) + return self.user_key + + email = email or self.email + password = password or self.password + api_key = api_key or self.api_key + + if not email: + raise SyncanoValueError('"email" is required.') + + if not password: + raise SyncanoValueError('"password" is required.') + + if not api_key: + raise SyncanoValueError('"api_key" is required.') + + self.logger.debug('Authenticating: %s', email) + + headers = { + 'content-type': self.CONTENT_TYPE, + 'X-API-KEY': api_key + } + + data = {'email': email, 'password': password} + response = self.make_request('POST', self.AUTH_SUFFIX, data=data, headers=headers) + + user_key = response.get('user_key') + self.user_key = user_key + self.logger.debug('Authentication successful: %s', user_key) + return user_key + class ConnectionMixin(object): """Injects connection attribute with support of basic validation.""" From 2c05263f9c2b82aceb8ac7e2ddde58a9917c9f93 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 13 Jul 2015 19:14:06 +0200 Subject: [PATCH 039/558] correct is_auth --- syncano/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index a4aacd2..cddbaed 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -248,7 +248,8 @@ def is_authenticated(self): :rtype: boolean :return: Session authentication state """ - + if self.instance_name: + return self.user_key is not None return self.api_key is not None def authenticate(self, email=None, password=None): From 672f2c518d30fc4b8f21c3d11b84225d4b2b6f86 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 14 Jul 2015 15:02:42 +0200 Subject: [PATCH 040/558] correct field mapping --- syncano/models/base.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 1cd5939..bc87fc9 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -192,8 +192,13 @@ def to_python(self, data): :param data: Raw data """ for field in self._meta.fields: - if field.name in data: - value = data[field.name] + field_name = field.name + + if field.mapping is not None: + field_name = field.mapping + + if field_name in data: + value = data[field_name] setattr(self, field.name, value) def to_native(self): @@ -884,7 +889,7 @@ class Trigger(Model): LINKS = ( {'type': 'detail', 'name': 'self'}, {'type': 'detail', 'name': 'codebox'}, - {'type': 'detail', 'name': 'klass'}, + {'type': 'detail', 'name': 'class_name'}, {'type': 'detail', 'name': 'traces'}, ) SIGNAL_CHOICES = ( From a7805a1e57ce0bf733b0ea276cd4c1e2c23bf422 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 14 Jul 2015 15:03:09 +0200 Subject: [PATCH 041/558] check patch first, then put --- syncano/models/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 45b031e..13d87da 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -255,7 +255,7 @@ def update(self, *args, **kwargs): instance = Instance.please.update(name='test-one', data={'description': 'new one'}) """ self.endpoint = 'detail' - self.method = self.get_allowed_method('PUT', 'PATCH', 'POST') + self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') self.data = kwargs.pop('data', kwargs) self._filter(*args, **kwargs) return self.request() From 4e8308e37ce9ec541cbe4c2d2eefc9059a08ee0f Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 14 Jul 2015 15:08:46 +0200 Subject: [PATCH 042/558] fix tests --- tests/test_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index 4f52a47..1969e4e 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -160,14 +160,14 @@ def test_update(self, clone_mock, filter_mock, request_mock): filter_mock.assert_called_once_with(1, 2, a=1, b=2) request_mock.assert_called_once_with() - self.assertEqual(self.manager.method, 'PUT') + self.assertEqual(self.manager.method, 'PATCH') self.assertEqual(self.manager.endpoint, 'detail') self.assertEqual(self.manager.data, {'x': 1, 'y': 2}) result = self.manager.update(1, 2, a=1, b=2, x=3, y=2) self.assertEqual(request_mock, result) - self.assertEqual(self.manager.method, 'PUT') + self.assertEqual(self.manager.method, 'PATCH') self.assertEqual(self.manager.endpoint, 'detail') self.assertEqual(self.manager.data, {'x': 3, 'y': 2, 'a': 1, 'b': 2}) From aa3a7299c95aea7a4ea2bbfc96cbf70fc35ccf1f Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 14 Jul 2015 16:40:38 +0200 Subject: [PATCH 043/558] refactored --- syncano/connection.py | 109 +++++++++++++++------------------------ tests/test_connection.py | 8 ++- 2 files changed, 47 insertions(+), 70 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index cddbaed..85fa9a8 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -156,13 +156,9 @@ def request(self, method_name, path, **kwargs): :rtype: dict :return: JSON response """ - - if not self.is_authenticated(): - if self.instance_name: - self.authenticate() - else: - self.authenticate_user() - + is_auth, _ = self.is_authenticated() + if not is_auth: + self.authenticate() return self.make_request(method_name, path, **kwargs) def make_request(self, method_name, path, **kwargs): @@ -248,11 +244,11 @@ def is_authenticated(self): :rtype: boolean :return: Session authentication state """ - if self.instance_name: - return self.user_key is not None - return self.api_key is not None + if self.user_key and self.api_key: + return True, 'user' + return self.api_key is not None, 'admin' - def authenticate(self, email=None, password=None): + def authenticate(self, email=None, password=None, api_key=None): """ :type email: string :param email: Your Syncano account email address @@ -260,80 +256,57 @@ def authenticate(self, email=None, password=None): :type password: string :param password: Your Syncano password + :type api_key: string + :param api_key: Your Syncano api_key for instance + :rtype: string :return: Your ``Account Key`` """ + is_auth, who = self.is_authenticated() - if self.is_authenticated(): - self.logger.debug('Connection already authenticated: %s', self.api_key) - return self.api_key - - email = email or self.email - password = password or self.password + if is_auth: + msg = 'Connection already authenticated for {0}: {1}' + key = self.api_key - if not email: - raise SyncanoValueError('"email" is required.') + if who == 'user': + key = self.user_key - if not password: - raise SyncanoValueError('"password" is required.') + self.logger.debug(msg.format(who, key)) + return key self.logger.debug('Authenticating: %s', email) - data = {'email': email, 'password': password} - response = self.make_request('POST', self.AUTH_SUFFIX, data=data) - - account_key = response.get('account_key') - self.api_key = account_key - - self.logger.debug('Authentication successful: %s', account_key) - return account_key - - def authenticate_user(self, email=None, password=None, api_key=None): - """ - :type email: string - :param email: Your Syncano account email address - - :type password: string - :param password: Your Syncano password + if who == 'user': + key = self.authenticate_user(email=email, password=password, api_key=api_key) + else: + key = self.authenticate_admin(email=email, password=password) - :type api_key: string - :param api_key: Your Syncano api_key for instance - - :rtype: string - :return: Your ``User Key`` - """ + self.logger.debug('Authentication successful for {0}: {1}'.format(who, key)) + return key - if self.is_authenticated(): - self.logger.debug('Connection already authenticated: %s', self.user_key) - return self.user_key + def validate_params(self, kwargs): + for k, v in kwargs.iteritems(): + kwargs[k] = v or getattr(self, k) - email = email or self.email - password = password or self.password - api_key = api_key or self.api_key + if kwargs[k] is None: + raise SyncanoValueError('"{}" is required.'.format(k)) + return kwargs - if not email: - raise SyncanoValueError('"email" is required.') - - if not password: - raise SyncanoValueError('"password" is required.') - - if not api_key: - raise SyncanoValueError('"api_key" is required.') - - self.logger.debug('Authenticating: %s', email) + def authenticate_admin(self, **kwargs): + request_args = self.validate_params(kwargs) + response = self.make_request('POST', self.AUTH_SUFFIX, data=request_args) + self.api_key = response.get('account_key') + return self.api_key + def authenticate_user(self, **kwargs): + request_args = self.validate_params(kwargs) headers = { 'content-type': self.CONTENT_TYPE, - 'X-API-KEY': api_key + 'X-API-KEY': request_args.pop('api_key') } - - data = {'email': email, 'password': password} - response = self.make_request('POST', self.AUTH_SUFFIX, data=data, headers=headers) - - user_key = response.get('user_key') - self.user_key = user_key - self.logger.debug('Authentication successful: %s', user_key) - return user_key + response = self.make_request('POST', self.AUTH_SUFFIX, data=request_args, headers=headers) + self.user_key = response.get('user_key') + return self.user_key class ConnectionMixin(object): diff --git a/tests/test_connection.py b/tests/test_connection.py index fe539c6..8973d21 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -227,9 +227,13 @@ def test_request_error(self, post_mock): self.assertTrue(post_mock.called) def test_is_authenticated(self): - self.assertFalse(self.connection.is_authenticated()) + is_auth, who = self.connection.is_authenticated() + self.assertFalse(is_auth) + self.assertEqual(who, 'admin') self.connection.api_key = 'xxxx' - self.assertTrue(self.connection.is_authenticated()) + is_auth, who = self.connection.is_authenticated() + self.assertTrue(is_auth) + self.assertEqual(who, 'admin') @mock.patch('syncano.connection.Connection.make_request') def test_already_authenticated(self, make_request_mock): From 1139b3dad4c117cda2a58b00622b90bb65e9524d Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Thu, 16 Jul 2015 15:52:58 +0200 Subject: [PATCH 044/558] correct user auth --- syncano/connection.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 85fa9a8..e6d6efe 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -72,6 +72,7 @@ def __init__(self, host=None, email=None, password=None, api_key=None, **kwargs) # instance indicates if we want to connect User or Admin self.instance_name = kwargs.get('instance_name') + self.username = kwargs.get('username') self.user_key = kwargs.get('user_key') if self.api_key and self.instance_name: @@ -244,11 +245,11 @@ def is_authenticated(self): :rtype: boolean :return: Session authentication state """ - if self.user_key and self.api_key: - return True, 'user' + if self.username and self.api_key: + return self.user_key is not None, 'user' return self.api_key is not None, 'admin' - def authenticate(self, email=None, password=None, api_key=None): + def authenticate(self, email=None, username=None, password=None, api_key=None): """ :type email: string :param email: Your Syncano account email address @@ -277,7 +278,7 @@ def authenticate(self, email=None, password=None, api_key=None): self.logger.debug('Authenticating: %s', email) if who == 'user': - key = self.authenticate_user(email=email, password=password, api_key=api_key) + key = self.authenticate_user(username=username, password=password, api_key=api_key) else: key = self.authenticate_admin(email=email, password=password) From 45801b37c55ecca435f73f05bf10c584b636f404 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 20 Jul 2015 07:38:53 +0200 Subject: [PATCH 045/558] refactored login params --- syncano/connection.py | 100 +++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index e6d6efe..8aa5fb5 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -59,29 +59,49 @@ class Connection(object): :ivar verify_ssl: Verify SSL certificate """ + CONTENT_TYPE = 'application/json' + AUTH_SUFFIX = 'v1/account/auth' USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' - CONTENT_TYPE = 'application/json' - def __init__(self, host=None, email=None, password=None, api_key=None, **kwargs): - self.host = host or syncano.API_ROOT - self.email = email or syncano.EMAIL - self.password = password or syncano.PASSWORD + ADMIN_LOGIN_PARAMS = ('email', 'password', 'api_key') + USER_LOGIN_PARAMS = ('username', 'password', 'api_key', 'instance_name') - self.api_key = api_key or syncano.APIKEY + def __init__(self, host=None, **kwargs): + self.host = host or syncano.API_ROOT + self.logger = kwargs.get('logger', syncano.logger) + self.timeout = kwargs.get('timeout', 30) + # We don't need to check SSL cert in DEBUG mode + self.verify_ssl = kwargs.pop('verify_ssl', True) - # instance indicates if we want to connect User or Admin - self.instance_name = kwargs.get('instance_name') - self.username = kwargs.get('username') - self.user_key = kwargs.get('user_key') + self._init_login_params(kwargs) - if self.api_key and self.instance_name: + if self.is_user: self.AUTH_SUFFIX = self.USER_AUTH_SUFFIX.format(name=self.instance_name) + self.auth_method = self.authenticate_user + else: + self.auth_method = self.authenticate_admin - self.logger = kwargs.get('logger') or syncano.logger - self.timeout = kwargs.get('timeout') or 30 self.session = requests.Session() - self.verify_ssl = kwargs.pop('verify_ssl', True) + + def _init_login_params(self, login_kwargs): + for param in self.ADMIN_LOGIN_PARAMS + self.USER_LOGIN_PARAMS: + if param in self.ADMIN_LOGIN_PARAMS: + param_lib_default_name = ''.join(param.split('_')).upper() + value = login_kwargs.get(param, getattr(syncano, param_lib_default_name)) + else: + value = login_kwargs.get(param) + setattr(self, param, value) + + @property + def is_user(self): + return all(getattr(self, param, None) for param in self.USER_LOGIN_PARAMS) + + @property + def auth_key(self): + if self.is_user: + return self.user_key + return self.api_key def build_params(self, params): """ @@ -92,14 +112,14 @@ def build_params(self, params): :return: Request params """ params = deepcopy(params) - params['timeout'] = params.get('timeout') or self.timeout - params['headers'] = params.get('headers') or {} - params['verify'] = True + params['timeout'] = params.get('timeout', self.timeout) + params['headers'] = params.get('headers', {}) + params['verify'] = self.verify_ssl if 'content-type' not in params['headers']: params['headers']['content-type'] = self.CONTENT_TYPE - if self.user_key: + if self.is_user: params['headers'].update({ 'X-USER-KEY': self.user_key, 'X-API-KEY': self.api_key @@ -157,7 +177,7 @@ def request(self, method_name, path, **kwargs): :rtype: dict :return: JSON response """ - is_auth, _ = self.is_authenticated() + is_auth = self.is_authenticated() if not is_auth: self.authenticate() return self.make_request(method_name, path, **kwargs) @@ -245,11 +265,11 @@ def is_authenticated(self): :rtype: boolean :return: Session authentication state """ - if self.username and self.api_key: - return self.user_key is not None, 'user' - return self.api_key is not None, 'admin' + if self.is_user: + return self.user_key is not None + return self.api_key is not None - def authenticate(self, email=None, username=None, password=None, api_key=None): + def authenticate(self, **kwargs): """ :type email: string :param email: Your Syncano account email address @@ -263,44 +283,34 @@ def authenticate(self, email=None, username=None, password=None, api_key=None): :rtype: string :return: Your ``Account Key`` """ - is_auth, who = self.is_authenticated() + is_auth = self.is_authenticated() if is_auth: - msg = 'Connection already authenticated for {0}: {1}' - key = self.api_key - - if who == 'user': - key = self.user_key - - self.logger.debug(msg.format(who, key)) - return key - - self.logger.debug('Authenticating: %s', email) - - if who == 'user': - key = self.authenticate_user(username=username, password=password, api_key=api_key) + msg = 'Connection already authenticated: {}' else: - key = self.authenticate_admin(email=email, password=password) - - self.logger.debug('Authentication successful for {0}: {1}'.format(who, key)) + msg = 'Authentication successful: {}' + self.logger.debug('Authenticating') + self.auth_method(**kwargs) + key = self.auth_key + self.logger.debug(msg.format(key)) return key - def validate_params(self, kwargs): - for k, v in kwargs.iteritems(): - kwargs[k] = v or getattr(self, k) + def validate_params(self, kwargs, login_params): + for k in login_params: + kwargs[k] = kwargs.get(k, getattr(self, k)) if kwargs[k] is None: raise SyncanoValueError('"{}" is required.'.format(k)) return kwargs def authenticate_admin(self, **kwargs): - request_args = self.validate_params(kwargs) + request_args = self.validate_params(kwargs, self.ADMIN_LOGIN_PARAMS) response = self.make_request('POST', self.AUTH_SUFFIX, data=request_args) self.api_key = response.get('account_key') return self.api_key def authenticate_user(self, **kwargs): - request_args = self.validate_params(kwargs) + request_args = self.validate_params(kwargs, self.USER_LOGIN_PARAMS) headers = { 'content-type': self.CONTENT_TYPE, 'X-API-KEY': request_args.pop('api_key') From 9bd011c8e5dc099debe0ab1ec16110dae5871127 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 20 Jul 2015 09:49:16 +0200 Subject: [PATCH 046/558] remove api_key from admin login params --- syncano/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 8aa5fb5..64031a8 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -64,7 +64,7 @@ class Connection(object): AUTH_SUFFIX = 'v1/account/auth' USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' - ADMIN_LOGIN_PARAMS = ('email', 'password', 'api_key') + ADMIN_LOGIN_PARAMS = ('email', 'password') USER_LOGIN_PARAMS = ('username', 'password', 'api_key', 'instance_name') def __init__(self, host=None, **kwargs): From 45235d1dd3ef7870202c3f3134f3c1ba786f8aa7 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 20 Jul 2015 09:49:34 +0200 Subject: [PATCH 047/558] corrected is_authenticated test --- tests/test_connection.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 8973d21..fe539c6 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -227,13 +227,9 @@ def test_request_error(self, post_mock): self.assertTrue(post_mock.called) def test_is_authenticated(self): - is_auth, who = self.connection.is_authenticated() - self.assertFalse(is_auth) - self.assertEqual(who, 'admin') + self.assertFalse(self.connection.is_authenticated()) self.connection.api_key = 'xxxx' - is_auth, who = self.connection.is_authenticated() - self.assertTrue(is_auth) - self.assertEqual(who, 'admin') + self.assertTrue(self.connection.is_authenticated()) @mock.patch('syncano.connection.Connection.make_request') def test_already_authenticated(self, make_request_mock): From df172cd5ed26413f56b5d34b6ad39d0958efd013 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 20 Jul 2015 12:21:30 +0200 Subject: [PATCH 048/558] fix creating model from manager --- syncano/models/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index bc87fc9..f9dcb0e 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -194,7 +194,7 @@ def to_python(self, data): for field in self._meta.fields: field_name = field.name - if field.mapping is not None: + if field.mapping is not None and self.pk: field_name = field.mapping if field_name in data: From 35ea420090c6e2bf51eb0e122d60a953521d89ab Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 20 Jul 2015 12:31:13 +0200 Subject: [PATCH 049/558] fix deletion, correct docstring --- syncano/models/manager.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 13d87da..b4a210e 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -144,7 +144,6 @@ def create(self, **kwargs): """ attrs = kwargs.copy() attrs.update(self.properties) - instance = self.model(**attrs) instance.save() @@ -225,11 +224,12 @@ def get_or_create(self, **kwargs): def delete(self, *args, **kwargs): """ Removes single instance based on provided arguments. + Returns None if deletion went fine. Usage:: - instance = Instance.please.delete('test-one') - instance = Instance.please.delete(name='test-one') + Instance.please.delete('test-one') + Instance.please.delete(name='test-one') """ self.method = 'DELETE' self.endpoint = 'detail' @@ -455,6 +455,9 @@ def serialize(self, data, model=None): """Serializes passed data to related :class:`~syncano.models.base.Model` class.""" model = model or self.model + if data == '': + return + if isinstance(data, model): return data From 0d017530e8ffb623bb276140dc211cddc60a79f7 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 20 Jul 2015 16:20:35 +0200 Subject: [PATCH 050/558] update docstring --- syncano/connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 64031a8..5c16d97 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -53,7 +53,9 @@ class Connection(object): :ivar host: Syncano API host :ivar email: Your Syncano email address :ivar password: Your Syncano password - :ivar api_key: Your Syncano ``Account Key`` + :ivar api_key: Your Syncano ``Account Key`` or instance ``Api Key`` + :ivar user_key: Your Syncano ``User Key`` + :ivar instance_name: Your Syncano ``Instance Name`` :ivar logger: Python logger instance :ivar timeout: Default request timeout :ivar verify_ssl: Verify SSL certificate From 58844831a724bd0ebcdecb4cbf4de3206ae4ba8d Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 20 Jul 2015 16:21:07 +0200 Subject: [PATCH 051/558] connect_instance for Users; improve docstrings --- syncano/__init__.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 90d5f77..246186b 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -42,7 +42,13 @@ def connect(*args, **kwargs): :param password: Your Syncano password :type api_key: string - :param api_key: Your Syncano account key + :param api_key: Your Syncano account key or instance api_key + + :type username: string + :param username: Your Syncano username + + :type instance_name: string + :param instance_name: Your Syncano instance_name :type verify_ssl: boolean :param verify_ssl: Verify SSL certificate @@ -51,9 +57,13 @@ def connect(*args, **kwargs): :return: A models registry Usage:: - + # Admin login connection = syncano.connect(email='', password='') + # OR connection = syncano.connect(api_key='') + + # User login + connection = syncano.connect(username='', api_key='', instance_name='') """ from syncano.connection import default_connection from syncano.models import registry @@ -78,7 +88,13 @@ def connect_instance(name=None, *args, **kwargs): :param password: Your Syncano password :type api_key: string - :param api_key: Your Syncano account key + :param api_key: Your Syncano account key or instance api_key + + :type username: string + :param username: Your Syncano username + + :type instance_name: string + :param instance_name: Your Syncano instance_name :type verify_ssl: boolean :param verify_ssl: Verify SSL certificate @@ -88,9 +104,14 @@ def connect_instance(name=None, *args, **kwargs): Usage:: + # For Admin my_instance = syncano.connect_instance('my_instance_name', email='', password='') + # OR my_instance = syncano.connect_instance('my_instance_name', api_key='') + + # For User + my_instance = syncano.connect_instance(username='', api_key='', instance_name='') """ - name = name or INSTANCE + name = name or kwargs.get('instance_name') or INSTANCE connection = connect(*args, **kwargs) return connection.Instance.please.get(name) From 8d8921d91be51cddda8c2ce59a41146222f2998a Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 20 Jul 2015 17:05:35 +0200 Subject: [PATCH 052/558] fix docstring --- syncano/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 246186b..71c25a6 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -63,7 +63,7 @@ def connect(*args, **kwargs): connection = syncano.connect(api_key='') # User login - connection = syncano.connect(username='', api_key='', instance_name='') + connection = syncano.connect(username='', password='', api_key='', instance_name='') """ from syncano.connection import default_connection from syncano.models import registry @@ -110,7 +110,7 @@ def connect_instance(name=None, *args, **kwargs): my_instance = syncano.connect_instance('my_instance_name', api_key='') # For User - my_instance = syncano.connect_instance(username='', api_key='', instance_name='') + my_instance = syncano.connect_instance(username='', password='', api_key='', instance_name='') """ name = name or kwargs.get('instance_name') or INSTANCE connection = connect(*args, **kwargs) From 170751d883cff78ec03cd1d0a6a6320835f699b7 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 21 Jul 2015 05:54:31 +0200 Subject: [PATCH 053/558] fix user login --- syncano/__init__.py | 4 ++++ syncano/connection.py | 1 + 2 files changed, 5 insertions(+) diff --git a/syncano/__init__.py b/syncano/__init__.py index 71c25a6..7873f22 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -69,8 +69,12 @@ def connect(*args, **kwargs): from syncano.models import registry default_connection.open(*args, **kwargs) + instance = kwargs.get('instance_name') + if INSTANCE: registry.set_default_instance(INSTANCE) + elif instance: + registry.set_default_instance(instance) return registry diff --git a/syncano/connection.py b/syncano/connection.py index 5c16d97..fd91800 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -81,6 +81,7 @@ def __init__(self, host=None, **kwargs): if self.is_user: self.AUTH_SUFFIX = self.USER_AUTH_SUFFIX.format(name=self.instance_name) self.auth_method = self.authenticate_user + self.user_key = None else: self.auth_method = self.authenticate_admin From 46ea1eea88aadabfc828e142c8a95bb7cb454dd9 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 22 Jul 2015 11:45:12 +0200 Subject: [PATCH 054/558] add alt login --- syncano/connection.py | 50 +++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index fd91800..48b87eb 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -66,8 +66,10 @@ class Connection(object): AUTH_SUFFIX = 'v1/account/auth' USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' - ADMIN_LOGIN_PARAMS = ('email', 'password') - USER_LOGIN_PARAMS = ('username', 'password', 'api_key', 'instance_name') + ADMIN_LOGIN_PARAMS = {'email', 'password'} + ADMIN_ALT_LOGIN_PARAMS = {'api_key'} + USER_LOGIN_PARAMS = {'username', 'password', 'api_key', 'instance_name'} + USER_ALT_LOGIN_PARAMS = {'user_key', 'api_key', 'instance_name'} def __init__(self, host=None, **kwargs): self.host = host or syncano.API_ROOT @@ -81,24 +83,36 @@ def __init__(self, host=None, **kwargs): if self.is_user: self.AUTH_SUFFIX = self.USER_AUTH_SUFFIX.format(name=self.instance_name) self.auth_method = self.authenticate_user - self.user_key = None else: self.auth_method = self.authenticate_admin self.session = requests.Session() def _init_login_params(self, login_kwargs): - for param in self.ADMIN_LOGIN_PARAMS + self.USER_LOGIN_PARAMS: - if param in self.ADMIN_LOGIN_PARAMS: - param_lib_default_name = ''.join(param.split('_')).upper() - value = login_kwargs.get(param, getattr(syncano, param_lib_default_name)) - else: - value = login_kwargs.get(param) + + def _set_value_or_default(param): + param_lib_default_name = ''.join(param.split('_')).upper() + value = login_kwargs.get(param, getattr(syncano, param_lib_default_name, None)) setattr(self, param, value) + map(_set_value_or_default, self.ADMIN_LOGIN_PARAMS.union(self.ADMIN_ALT_LOGIN_PARAMS, + self.USER_LOGIN_PARAMS, + self.USER_ALT_LOGIN_PARAMS)) + + def _are_params_ok(self, params): + return all(getattr(self, p) for p in params) + @property def is_user(self): - return all(getattr(self, param, None) for param in self.USER_LOGIN_PARAMS) + login_params_ok = self._are_params_ok(self.USER_LOGIN_PARAMS) + alt_login_params_ok = self._are_params_ok(self.USER_ALT_LOGIN_PARAMS) + return login_params_ok or alt_login_params_ok + + @property + def is_alt_login(self): + if self.is_user: + return self._are_params_ok(self.USER_ALT_LOGIN_PARAMS) + return self._are_params_ok(self.ADMIN_ALT_LOGIN_PARAMS) @property def auth_key(self): @@ -298,8 +312,16 @@ def authenticate(self, **kwargs): self.logger.debug(msg.format(key)) return key - def validate_params(self, kwargs, login_params): - for k in login_params: + def validate_params(self, kwargs): + map_login_params = { + (False, False): self.ADMIN_LOGIN_PARAMS, + (True, False): self.ADMIN_ALT_LOGIN_PARAMS, + (False, True): self.USER_LOGIN_PARAMS, + (True, True): self.USER_ALT_LOGIN_PARAMS + } + import ipdb; ipdb.set_trace() # breakpoint 69e19052 // + + for k in map_login_params[(self.is_alt_login, self.is_user)]: kwargs[k] = kwargs.get(k, getattr(self, k)) if kwargs[k] is None: @@ -307,13 +329,13 @@ def validate_params(self, kwargs, login_params): return kwargs def authenticate_admin(self, **kwargs): - request_args = self.validate_params(kwargs, self.ADMIN_LOGIN_PARAMS) + request_args = self.validate_params(kwargs) response = self.make_request('POST', self.AUTH_SUFFIX, data=request_args) self.api_key = response.get('account_key') return self.api_key def authenticate_user(self, **kwargs): - request_args = self.validate_params(kwargs, self.USER_LOGIN_PARAMS) + request_args = self.validate_params(kwargs) headers = { 'content-type': self.CONTENT_TYPE, 'X-API-KEY': request_args.pop('api_key') From d7c6cb906062af212ae481fb07069b93f418af27 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 22 Jul 2015 11:49:26 +0200 Subject: [PATCH 055/558] remove ipdb --- syncano/connection.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 48b87eb..098002a 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -319,7 +319,6 @@ def validate_params(self, kwargs): (False, True): self.USER_LOGIN_PARAMS, (True, True): self.USER_ALT_LOGIN_PARAMS } - import ipdb; ipdb.set_trace() # breakpoint 69e19052 // for k in map_login_params[(self.is_alt_login, self.is_user)]: kwargs[k] = kwargs.get(k, getattr(self, k)) From c369909faae3f4733633f885921ad5ca63a3ad3f Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 24 Jul 2015 14:32:25 +0200 Subject: [PATCH 056/558] divide models --- setup.py | 1 - syncano/connection.py | 1 - syncano/models/__init__.py | 5 + syncano/models/accounts.py | 91 ++++ syncano/models/base.py | 948 +---------------------------------- syncano/models/billing.py | 80 +++ syncano/models/channels.py | 5 +- syncano/models/classes.py | 204 ++++++++ syncano/models/fields.py | 1 - syncano/models/incentives.py | 441 ++++++++++++++++ syncano/models/instances.py | 127 +++++ syncano/models/manager.py | 1 - syncano/models/options.py | 1 - syncano/models/registry.py | 5 +- tests/integration_test.py | 2 +- tests/test_manager.py | 24 +- tests/test_models.py | 12 +- tests/test_options.py | 3 +- 18 files changed, 996 insertions(+), 956 deletions(-) create mode 100644 syncano/models/accounts.py create mode 100644 syncano/models/billing.py create mode 100644 syncano/models/classes.py create mode 100644 syncano/models/incentives.py create mode 100644 syncano/models/instances.py diff --git a/setup.py b/setup.py index eb37cb0..2c6ecf0 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ from setuptools import find_packages, setup - from syncano import __version__ diff --git a/syncano/connection.py b/syncano/connection.py index 098002a..7031e9d 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -4,7 +4,6 @@ import requests import six - import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError diff --git a/syncano/models/__init__.py b/syncano/models/__init__.py index a63b8ae..5e82729 100644 --- a/syncano/models/__init__.py +++ b/syncano/models/__init__.py @@ -1,3 +1,8 @@ from .base import * # NOQA from .fields import * # NOQA +from .instances import * # NOQA +from .accounts import * # NOQA +from .billing import * # NOQA from .channels import * # NOQA +from .classes import * # NOQA +from .incentives import * # NOQA diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py new file mode 100644 index 0000000..522fc0c --- /dev/null +++ b/syncano/models/accounts.py @@ -0,0 +1,91 @@ +from __future__ import unicode_literals + +from . import fields +from .base import Model +from .instances import Instance + + +class Admin(Model): + """ + OO wrapper around instance admins `endpoint `_. + + :ivar first_name: :class:`~syncano.models.fields.StringField` + :ivar last_name: :class:`~syncano.models.fields.StringField` + :ivar email: :class:`~syncano.models.fields.EmailField` + :ivar role: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + ROLE_CHOICES = ( + {'display_name': 'full', 'value': 'full'}, + {'display_name': 'write', 'value': 'write'}, + {'display_name': 'read', 'value': 'read'}, + ) + + first_name = fields.StringField(read_only=True, required=False) + last_name = fields.StringField(read_only=True, required=False) + email = fields.EmailField(read_only=True, required=False) + role = fields.ChoiceField(choices=ROLE_CHOICES) + links = fields.HyperlinkedField(links=LINKS) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/admins/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/admins/', + } + } + + +class User(Model): + """ + OO wrapper around instances `endpoint `_. + + :ivar username: :class:`~syncano.models.fields.StringField` + :ivar password: :class:`~syncano.models.fields.StringField` + :ivar user_key: :class:`~syncano.models.fields.StringField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + username = fields.StringField(max_length=64, required=True) + password = fields.StringField(read_only=False, required=True) + user_key = fields.StringField(read_only=True, required=False) + + links = fields.HyperlinkedField(links=LINKS) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'patch', 'put', 'get'], + 'path': '/users/{id}/', + }, + 'reset_key': { + 'methods': ['post'], + 'path': '/users/{id}/reset_key/', + }, + 'list': { + 'methods': ['get'], + 'path': '/users/', + } + } + + def reset_key(self, **payload): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('reset_key', properties) + connection = self._get_connection(**payload) + return connection.request('POST', endpoint) diff --git a/syncano/models/base.py b/syncano/models/base.py index b14df08..60581eb 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -5,7 +5,6 @@ from copy import deepcopy import six - from syncano.exceptions import SyncanoDoesNotExist, SyncanoValidationError from syncano.utils import get_class_name @@ -16,8 +15,8 @@ class ModelMetaclass(type): - """Metaclass for all models.""" - + """Metaclass for all models. + """ def __new__(cls, name, bases, attrs): super_new = super(ModelMetaclass, cls).__new__ @@ -69,7 +68,8 @@ def create_error_class(cls): ) def build_doc(cls, name, meta): - """Give the class a docstring if it's not defined.""" + """Give the class a docstring if it's not defined. + """ if cls.__doc__ is not None: return @@ -78,25 +78,28 @@ def build_doc(cls, name, meta): class Model(six.with_metaclass(ModelMetaclass)): - """Base class for all models.""" - + """Base class for all models. + """ def __init__(self, **kwargs): self._raw_data = {} self.to_python(kwargs) def __repr__(self): - """Displays current instance class name and pk.""" + """Displays current instance class name and pk. + """ return '<{0}: {1}>'.format( self.__class__.__name__, self.pk ) def __str__(self): - """Wrapper around ```repr`` method.""" + """Wrapper around ```repr`` method. + """ return repr(self) def __unicode__(self): - """Wrapper around ```repr`` method with proper encoding.""" + """Wrapper around ```repr`` method with proper encoding. + """ return six.u(repr(self)) def __eq__(self, other): @@ -135,7 +138,8 @@ def save(self, **kwargs): return self def delete(self, **kwargs): - """Removes the current instance.""" + """Removes the current instance. + """ if self.is_new(): raise SyncanoValidationError('Method allowed only on existing model.') @@ -146,7 +150,8 @@ def delete(self, **kwargs): self._raw_data = {} def reload(self, **kwargs): - """Reloads the current instance.""" + """Reloads the current instance. + """ if self.is_new(): raise SyncanoValidationError('Method allowed only on existing model.') @@ -203,7 +208,8 @@ def to_python(self, data): def to_native(self): """Converts the current instance to raw data which - can be serialized to JSON and send to API.""" + can be serialized to JSON and send to API. + """ data = {} for field in self._meta.fields: if not field.read_only and field.has_data: @@ -224,921 +230,3 @@ def get_endpoint_data(self): if field.has_endpoint_data: properties[field.name] = getattr(self, field.name) return properties - - -class Coupon(Model): - """ - OO wrapper around coupons `endpoint `_. - - :ivar name: :class:`~syncano.models.fields.StringField` - :ivar redeem_by: :class:`~syncano.models.fields.DateField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar percent_off: :class:`~syncano.models.fields.IntegerField` - :ivar amount_off: :class:`~syncano.models.fields.FloatField` - :ivar currency: :class:`~syncano.models.fields.ChoiceField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'redeem'}, - ) - CURRENCY_CHOICES = ( - {'display_name': 'USD', 'value': 'usd'}, - ) - - name = fields.StringField(max_length=32, primary_key=True) - redeem_by = fields.DateField() - links = fields.HyperlinkedField(links=LINKS) - percent_off = fields.IntegerField(required=False) - amount_off = fields.FloatField(required=False) - currency = fields.ChoiceField(choices=CURRENCY_CHOICES) - duration = fields.IntegerField(default=0) - - class Meta: - endpoints = { - 'detail': { - 'methods': ['get', 'delete'], - 'path': '/v1/billing/coupons/{name}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/v1/billing/coupons/', - } - } - - -class Discount(Model): - """ - OO wrapper around discounts `endpoint `_. - - :ivar instance: :class:`~syncano.models.fields.ModelField` - :ivar coupon: :class:`~syncano.models.fields.ModelField` - :ivar start: :class:`~syncano.models.fields.DateField` - :ivar end: :class:`~syncano.models.fields.DateField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - """ - - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - instance = fields.ModelField('Instance') - coupon = fields.ModelField('Coupon') - start = fields.DateField(read_only=True, required=False) - end = fields.DateField(read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - - class Meta: - endpoints = { - 'detail': { - 'methods': ['get'], - 'path': '/v1/billing/discounts/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/v1/billing/discounts/', - } - } - - -class Instance(Model): - """ - OO wrapper around instances `endpoint `_. - - :ivar name: :class:`~syncano.models.fields.StringField` - :ivar description: :class:`~syncano.models.fields.StringField` - :ivar role: :class:`~syncano.models.fields.Field` - :ivar owner: :class:`~syncano.models.fields.ModelField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar metadata: :class:`~syncano.models.fields.JSONField` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - """ - - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'admins'}, - {'type': 'list', 'name': 'classes'}, - {'type': 'list', 'name': 'codeboxes'}, - {'type': 'list', 'name': 'invitations'}, - {'type': 'list', 'name': 'runtimes'}, - {'type': 'list', 'name': 'api_keys'}, - {'type': 'list', 'name': 'triggers'}, - {'type': 'list', 'name': 'users'}, - {'type': 'list', 'name': 'webhooks'}, - {'type': 'list', 'name': 'schedules'}, - ) - - name = fields.StringField(max_length=64, primary_key=True) - description = fields.StringField(read_only=False, required=False) - role = fields.Field(read_only=True, required=False) - owner = fields.ModelField('Admin', read_only=True) - links = fields.HyperlinkedField(links=LINKS) - metadata = fields.JSONField(read_only=False, required=False) - created_at = fields.DateTimeField(read_only=True, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) - - class Meta: - endpoints = { - 'detail': { - 'methods': ['delete', 'patch', 'put', 'get'], - 'path': '/v1/instances/{name}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/v1/instances/', - } - } - - -class ApiKey(Model): - """ - OO wrapper around instance api keys `endpoint `_. - - :ivar api_key: :class:`~syncano.models.fields.StringField` - :ivar allow_user_create: :class:`~syncano.models.fields.BooleanField` - :ivar ignore_acl: :class:`~syncano.models.fields.BooleanField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - """ - LINKS = [ - {'type': 'detail', 'name': 'self'}, - ] - - api_key = fields.StringField(read_only=True, required=False) - allow_user_create = fields.BooleanField(required=False, default=False) - ignore_acl = fields.BooleanField(required=False, default=False) - links = fields.HyperlinkedField(links=LINKS) - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['get', 'delete'], - 'path': '/api_keys/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/api_keys/', - } - } - - -class Class(Model): - """ - OO wrapper around instance classes `endpoint `_. - - :ivar name: :class:`~syncano.models.fields.StringField` - :ivar description: :class:`~syncano.models.fields.StringField` - :ivar objects_count: :class:`~syncano.models.fields.Field` - :ivar schema: :class:`~syncano.models.fields.SchemaField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar status: :class:`~syncano.models.fields.Field` - :ivar metadata: :class:`~syncano.models.fields.JSONField` - :ivar revision: :class:`~syncano.models.fields.IntegerField` - :ivar expected_revision: :class:`~syncano.models.fields.IntegerField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar group: :class:`~syncano.models.fields.IntegerField` - :ivar group_permissions: :class:`~syncano.models.fields.ChoiceField` - :ivar other_permissions: :class:`~syncano.models.fields.ChoiceField` - - .. note:: - This model is special because each related :class:`~syncano.models.base.Object` will be - **dynamically populated** with fields defined in schema attribute. - """ - - LINKS = [ - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'objects'}, - ] - - PERMISSIONS_CHOICES = ( - {'display_name': 'None', 'value': 'none'}, - {'display_name': 'Read', 'value': 'read'}, - {'display_name': 'Create objects', 'value': 'create_objects'}, - ) - - name = fields.StringField(max_length=64, primary_key=True) - description = fields.StringField(read_only=False, required=False) - objects_count = fields.Field(read_only=True, required=False) - - schema = fields.SchemaField(read_only=False, required=True) - links = fields.HyperlinkedField(links=LINKS) - status = fields.Field() - metadata = fields.JSONField(read_only=False, required=False) - - revision = fields.IntegerField(read_only=True, required=False) - expected_revision = fields.IntegerField(read_only=False, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) - created_at = fields.DateTimeField(read_only=True, required=False) - - group = fields.IntegerField(label='group id', required=False) - group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - - class Meta: - parent = Instance - plural_name = 'Classes' - endpoints = { - 'detail': { - 'methods': ['get', 'put', 'patch', 'delete'], - 'path': '/classes/{name}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/classes/', - } - } - - -class CodeBox(Model): - """ - OO wrapper around codeboxes `endpoint `_. - - :ivar label: :class:`~syncano.models.fields.StringField` - :ivar description: :class:`~syncano.models.fields.StringField` - :ivar source: :class:`~syncano.models.fields.StringField` - :ivar runtime_name: :class:`~syncano.models.fields.ChoiceField` - :ivar config: :class:`~syncano.models.fields.Field` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - - .. note:: - **CodeBox** has special method called ``run`` which will execute attached source code:: - - >>> CodeBox.please.run('instance-name', 1234) - >>> CodeBox.please.run('instance-name', 1234, payload={'variable_one': 1, 'variable_two': 2}) - >>> CodeBox.please.run('instance-name', 1234, payload="{\"variable_one\": 1, \"variable_two\": 2}") - - or via instance:: - - >>> cb = CodeBox.please.get('instance-name', 1234) - >>> cb.run() - >>> cb.run(variable_one=1, variable_two=2) - """ - - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'runtimes'}, - # This will cause name collision between model run method - # and HyperlinkedField dynamic methods. - # {'type': 'detail', 'name': 'run'}, - {'type': 'detail', 'name': 'traces'}, - ) - RUNTIME_CHOICES = ( - {'display_name': 'nodejs', 'value': 'nodejs'}, - {'display_name': 'python', 'value': 'python'}, - {'display_name': 'ruby', 'value': 'ruby'}, - ) - - label = fields.StringField(max_length=80) - description = fields.StringField(required=False) - source = fields.StringField() - runtime_name = fields.ChoiceField(choices=RUNTIME_CHOICES) - config = fields.Field(required=False) - links = fields.HyperlinkedField(links=LINKS) - created_at = fields.DateTimeField(read_only=True, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) - - please = CodeBoxManager() - - class Meta: - parent = Instance - name = 'Codebox' - plural_name = 'Codeboxes' - endpoints = { - 'detail': { - 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/codeboxes/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/codeboxes/', - }, - 'run': { - 'methods': ['post'], - 'path': '/codeboxes/{id}/run/', - }, - } - - def run(self, **payload): - """ - Usage:: - - >>> cb = CodeBox.please.get('instance-name', 1234) - >>> cb.run() - >>> cb.run(variable_one=1, variable_two=2) - """ - if self.is_new(): - raise SyncanoValidationError('Method allowed only on existing model.') - - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('run', properties) - connection = self._get_connection(**payload) - request = { - 'data': { - 'payload': json.dumps(payload) - } - } - response = connection.request('POST', endpoint, **request) - response.update({'instance_name': self.instance_name, 'codebox_id': self.id}) - return CodeBoxTrace(**response) - - -class CodeBoxTrace(Model): - """ - :ivar status: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` - :ivar result: :class:`~syncano.models.fields.StringField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - - STATUS_CHOICES = ( - {'display_name': 'Success', 'value': 'success'}, - {'display_name': 'Failure', 'value': 'failure'}, - {'display_name': 'Timeout', 'value': 'timeout'}, - {'display_name': 'Pending', 'value': 'pending'}, - ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) - duration = fields.IntegerField(read_only=True, required=False) - - class Meta: - parent = CodeBox - endpoints = { - 'detail': { - 'methods': ['get'], - 'path': '/traces/{id}/', - }, - 'list': { - 'methods': ['get'], - 'path': '/traces/', - } - } - - -class Schedule(Model): - """ - OO wrapper around codebox schedules `endpoint `_. - - :ivar label: :class:`~syncano.models.fields.StringField` - :ivar codebox: :class:`~syncano.models.fields.IntegerField` - :ivar interval_sec: :class:`~syncano.models.fields.IntegerField` - :ivar crontab: :class:`~syncano.models.fields.StringField` - :ivar payload: :class:`~syncano.models.fields.HyperliStringFieldnkedField` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar scheduled_next: :class:`~syncano.models.fields.DateTimeField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - """ - - LINKS = [ - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'traces'}, - {'type': 'list', 'name': 'codebox'}, - ] - - label = fields.StringField(max_length=80) - codebox = fields.IntegerField(label='codebox id') - interval_sec = fields.IntegerField(read_only=False, required=False) - crontab = fields.StringField(max_length=40, required=False) - payload = fields.StringField(required=False) - created_at = fields.DateTimeField(read_only=True, required=False) - scheduled_next = fields.DateTimeField(read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/schedules/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/schedules/', - } - } - - -class ScheduleTrace(Model): - """ - :ivar status: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` - :ivar result: :class:`~syncano.models.fields.StringField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - - STATUS_CHOICES = ( - {'display_name': 'Success', 'value': 'success'}, - {'display_name': 'Failure', 'value': 'failure'}, - {'display_name': 'Timeout', 'value': 'timeout'}, - {'display_name': 'Pending', 'value': 'pending'}, - ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) - duration = fields.IntegerField(read_only=True, required=False) - - class Meta: - parent = Schedule - endpoints = { - 'detail': { - 'methods': ['get'], - 'path': '/traces/{id}/', - }, - 'list': { - 'methods': ['get'], - 'path': '/traces/', - } - } - - -class Admin(Model): - """ - OO wrapper around instance admins `endpoint `_. - - :ivar first_name: :class:`~syncano.models.fields.StringField` - :ivar last_name: :class:`~syncano.models.fields.StringField` - :ivar email: :class:`~syncano.models.fields.EmailField` - :ivar role: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - """ - - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - ROLE_CHOICES = ( - {'display_name': 'full', 'value': 'full'}, - {'display_name': 'write', 'value': 'write'}, - {'display_name': 'read', 'value': 'read'}, - ) - - first_name = fields.StringField(read_only=True, required=False) - last_name = fields.StringField(read_only=True, required=False) - email = fields.EmailField(read_only=True, required=False) - role = fields.ChoiceField(choices=ROLE_CHOICES) - links = fields.HyperlinkedField(links=LINKS) - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/admins/{id}/', - }, - 'list': { - 'methods': ['get'], - 'path': '/admins/', - } - } - - -class InstanceInvitation(Model): - """ - OO wrapper around instance invitations - `endpoint `_. - - :ivar email: :class:`~syncano.models.fields.EmailField` - :ivar role: :class:`~syncano.models.fields.ChoiceField` - :ivar key: :class:`~syncano.models.fields.StringField` - :ivar state: :class:`~syncano.models.fields.StringField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - """ - - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - email = fields.EmailField(max_length=254) - role = fields.ChoiceField(choices=Admin.ROLE_CHOICES) - key = fields.StringField(read_only=True, required=False) - state = fields.StringField(read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - created_at = fields.DateTimeField(read_only=True, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) - - class Meta: - parent = Instance - name = 'Invitation' - endpoints = { - 'detail': { - 'methods': ['get', 'delete'], - 'path': '/invitations/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/invitations/', - } - } - - -class Object(Model): - """ - OO wrapper around data objects `endpoint `_. - - :ivar revision: :class:`~syncano.models.fields.IntegerField` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - :ivar owner: :class:`~syncano.models.fields.IntegerField` - :ivar owner_permissions: :class:`~syncano.models.fields.ChoiceField` - :ivar group: :class:`~syncano.models.fields.IntegerField` - :ivar group_permissions: :class:`~syncano.models.fields.ChoiceField` - :ivar other_permissions: :class:`~syncano.models.fields.ChoiceField` - :ivar channel: :class:`~syncano.models.fields.StringField` - :ivar channel_room: :class:`~syncano.models.fields.StringField` - - .. note:: - This model is special because each instance will be **dynamically populated** - with fields defined in related :class:`~syncano.models.base.Class` schema attribute. - """ - - PERMISSIONS_CHOICES = ( - {'display_name': 'None', 'value': 'none'}, - {'display_name': 'Read', 'value': 'read'}, - {'display_name': 'Write', 'value': 'write'}, - {'display_name': 'Full', 'value': 'full'}, - ) - - revision = fields.IntegerField(read_only=True, required=False) - created_at = fields.DateTimeField(read_only=True, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) - - owner = fields.IntegerField(label='owner id', required=False, read_only=True) - owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - group = fields.IntegerField(label='group id', required=False) - group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - channel = fields.StringField(required=False) - channel_room = fields.StringField(required=False, max_length=64) - - please = ObjectManager() - - class Meta: - parent = Class - endpoints = { - 'detail': { - 'methods': ['delete', 'post', 'patch', 'get'], - 'path': '/objects/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/objects/', - } - } - - @staticmethod - def __new__(cls, **kwargs): - instance_name = kwargs.get('instance_name') - class_name = kwargs.get('class_name') - - if not instance_name: - raise SyncanoValidationError('Field "instance_name" is required.') - - if not class_name: - raise SyncanoValidationError('Field "class_name" is required.') - - model = cls.get_subclass_model(instance_name, class_name) - return model(**kwargs) - - @classmethod - def create_subclass(cls, name, schema): - attrs = { - 'Meta': deepcopy(Object._meta), - '__new__': Model.__new__, # We don't want to have maximum recursion depth exceeded error - } - - for field in schema: - field_type = field.get('type') - field_class = fields.MAPPING[field_type] - query_allowed = ('order_index' in field or 'filter_index' in field) - attrs[field['name']] = field_class(required=False, read_only=False, - query_allowed=query_allowed) - - return type(str(name), (Object, ), attrs) - - @classmethod - def get_or_create_subclass(cls, name, schema): - try: - subclass = registry.get_model_by_name(name) - except LookupError: - subclass = cls.create_subclass(name, schema) - registry.add(name, subclass) - - return subclass - - @classmethod - def get_subclass_name(cls, instance_name, class_name): - return get_class_name(instance_name, class_name, 'object') - - @classmethod - def get_class_schema(cls, instance_name, class_name): - parent = cls._meta.parent - class_ = parent.please.get(instance_name, class_name) - return class_.schema - - @classmethod - def get_subclass_model(cls, instance_name, class_name, **kwargs): - """ - Creates custom :class:`~syncano.models.base.Object` sub-class definition based - on passed **instance_name** and **class_name**. - """ - model_name = cls.get_subclass_name(instance_name, class_name) - - if cls.__name__ == model_name: - return cls - - try: - model = registry.get_model_by_name(model_name) - except LookupError: - schema = cls.get_class_schema(instance_name, class_name) - model = cls.create_subclass(model_name, schema) - registry.add(model_name, model) - - return model - - -class Trigger(Model): - """ - OO wrapper around triggers `endpoint `_. - - :ivar label: :class:`~syncano.models.fields.StringField` - :ivar codebox: :class:`~syncano.models.fields.IntegerField` - :ivar class_name: :class:`~syncano.models.fields.StringField` - :ivar signal: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - """ - - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'detail', 'name': 'codebox'}, - {'type': 'detail', 'name': 'class_name'}, - {'type': 'detail', 'name': 'traces'}, - ) - SIGNAL_CHOICES = ( - {'display_name': 'post_update', 'value': 'post_update'}, - {'display_name': 'post_create', 'value': 'post_create'}, - {'display_name': 'post_delete', 'value': 'post_delete'}, - ) - - label = fields.StringField(max_length=80) - codebox = fields.IntegerField(label='codebox id') - class_name = fields.StringField(label='class name', mapping='class') - signal = fields.ChoiceField(choices=SIGNAL_CHOICES) - links = fields.HyperlinkedField(links=LINKS) - created_at = fields.DateTimeField(read_only=True, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/triggers/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/triggers/', - } - } - - -class TriggerTrace(Model): - """ - :ivar status: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` - :ivar result: :class:`~syncano.models.fields.StringField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - - STATUS_CHOICES = ( - {'display_name': 'Success', 'value': 'success'}, - {'display_name': 'Failure', 'value': 'failure'}, - {'display_name': 'Timeout', 'value': 'timeout'}, - {'display_name': 'Pending', 'value': 'pending'}, - ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) - duration = fields.IntegerField(read_only=True, required=False) - - class Meta: - parent = Trigger - endpoints = { - 'detail': { - 'methods': ['get'], - 'path': '/traces/{id}/', - }, - 'list': { - 'methods': ['get'], - 'path': '/traces/', - } - } - - -class Webhook(Model): - """ - OO wrapper around webhooks `endpoint `_. - - :ivar name: :class:`~syncano.models.fields.SlugField` - :ivar codebox: :class:`~syncano.models.fields.IntegerField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - - .. note:: - **WebHook** has special method called ``run`` which will execute related codebox:: - - >>> Webhook.please.run('instance-name', 'webhook-name') - >>> Webhook.please.run('instance-name', 'webhook-name', payload={'variable_one': 1, 'variable_two': 2}) - >>> Webhook.please.run('instance-name', 'webhook-name', - payload="{\"variable_one\": 1, \"variable_two\": 2}") - - or via instance:: - - >>> wh = Webhook.please.get('instance-name', 'webhook-name') - >>> wh.run() - >>> wh.run(variable_one=1, variable_two=2) - - """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'detail', 'name': 'codebox'}, - ) - - name = fields.SlugField(max_length=50, primary_key=True) - codebox = fields.IntegerField(label='codebox id') - public = fields.BooleanField(required=False, default=False) - public_link = fields.ChoiceField(required=False, read_only=True) - links = fields.HyperlinkedField(links=LINKS) - - please = WebhookManager() - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/webhooks/{name}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/webhooks/', - }, - 'run': { - 'methods': ['post'], - 'path': '/webhooks/{name}/run/', - }, - 'reset': { - 'methods': ['post'], - 'path': '/webhooks/{name}/reset_link/', - }, - 'public': { - 'methods': ['get'], - 'path': '/webhooks/p/{public_link}/', - } - } - - def run(self, **payload): - """ - Usage:: - - >>> wh = Webhook.please.get('instance-name', 'webhook-name') - >>> wh.run() - >>> wh.run(variable_one=1, variable_two=2) - """ - if self.is_new(): - raise SyncanoValidationError('Method allowed only on existing model.') - - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('run', properties) - connection = self._get_connection(**payload) - request = { - 'data': { - 'payload': json.dumps(payload) - } - } - response = connection.request('POST', endpoint, **request) - response.update({'instance_name': self.instance_name, 'webhook_name': self.name}) - return WebhookTrace(**response) - - def reset(self, **payload): - """ - Usage:: - - >>> wh = Webhook.please.get('instance-name', 'webhook-name') - >>> wh.reset() - """ - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('reset', properties) - connection = self._get_connection(**payload) - return connection.request('POST', endpoint) - - -class WebhookTrace(Model): - """ - :ivar status: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` - :ivar result: :class:`~syncano.models.fields.StringField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - - STATUS_CHOICES = ( - {'display_name': 'Success', 'value': 'success'}, - {'display_name': 'Failure', 'value': 'failure'}, - {'display_name': 'Timeout', 'value': 'timeout'}, - {'display_name': 'Pending', 'value': 'pending'}, - ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) - duration = fields.IntegerField(read_only=True, required=False) - - class Meta: - parent = Webhook - endpoints = { - 'detail': { - 'methods': ['get'], - 'path': '/traces/{id}/', - }, - 'list': { - 'methods': ['get'], - 'path': '/traces/', - } - } - - -class User(Model): - """ - OO wrapper around instances `endpoint `_. - - :ivar username: :class:`~syncano.models.fields.StringField` - :ivar password: :class:`~syncano.models.fields.StringField` - :ivar user_key: :class:`~syncano.models.fields.StringField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - """ - - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - username = fields.StringField(max_length=64, required=True) - password = fields.StringField(read_only=False, required=True) - user_key = fields.StringField(read_only=True, required=False) - - links = fields.HyperlinkedField(links=LINKS) - created_at = fields.DateTimeField(read_only=True, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['delete', 'patch', 'put', 'get'], - 'path': '/users/{id}/', - }, - 'reset_key': { - 'methods': ['post'], - 'path': '/users/{id}/reset_key/', - }, - 'list': { - 'methods': ['get'], - 'path': '/users/', - } - } - - def reset_key(self, **payload): - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('reset_key', properties) - connection = self._get_connection(**payload) - return connection.request('POST', endpoint) diff --git a/syncano/models/billing.py b/syncano/models/billing.py new file mode 100644 index 0000000..5fd9fd2 --- /dev/null +++ b/syncano/models/billing.py @@ -0,0 +1,80 @@ +from __future__ import unicode_literals + +from . import fields +from .base import Model + + +class Coupon(Model): + """ + OO wrapper around coupons `endpoint `_. + + :ivar name: :class:`~syncano.models.fields.StringField` + :ivar redeem_by: :class:`~syncano.models.fields.DateField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar percent_off: :class:`~syncano.models.fields.IntegerField` + :ivar amount_off: :class:`~syncano.models.fields.FloatField` + :ivar currency: :class:`~syncano.models.fields.ChoiceField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + + LINKS = ( + {'type': 'detail', 'name': 'self'}, + {'type': 'list', 'name': 'redeem'}, + ) + CURRENCY_CHOICES = ( + {'display_name': 'USD', 'value': 'usd'}, + ) + + name = fields.StringField(max_length=32, primary_key=True) + redeem_by = fields.DateField() + links = fields.HyperlinkedField(links=LINKS) + percent_off = fields.IntegerField(required=False) + amount_off = fields.FloatField(required=False) + currency = fields.ChoiceField(choices=CURRENCY_CHOICES) + duration = fields.IntegerField(default=0) + + class Meta: + endpoints = { + 'detail': { + 'methods': ['get', 'delete'], + 'path': '/v1/billing/coupons/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/v1/billing/coupons/', + } + } + + +class Discount(Model): + """ + OO wrapper around discounts `endpoint `_. + + :ivar instance: :class:`~syncano.models.fields.ModelField` + :ivar coupon: :class:`~syncano.models.fields.ModelField` + :ivar start: :class:`~syncano.models.fields.DateField` + :ivar end: :class:`~syncano.models.fields.DateField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + """ + + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + instance = fields.ModelField('Instance') + coupon = fields.ModelField('Coupon') + start = fields.DateField(read_only=True, required=False) + end = fields.DateField(read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + + class Meta: + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/v1/billing/discounts/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/v1/billing/discounts/', + } + } diff --git a/syncano/models/channels.py b/syncano/models/channels.py index ac8e658..1e2d0a4 100644 --- a/syncano/models/channels.py +++ b/syncano/models/channels.py @@ -2,10 +2,11 @@ import six from requests import Timeout - from syncano import logger -from .base import Instance, Model, fields +from . import fields +from .base import Model +from .instances import Instance class PollThread(Thread): diff --git a/syncano/models/classes.py b/syncano/models/classes.py new file mode 100644 index 0000000..38b78cd --- /dev/null +++ b/syncano/models/classes.py @@ -0,0 +1,204 @@ +from __future__ import unicode_literals + +from copy import deepcopy + +from syncano.exceptions import SyncanoValidationError +from syncano.utils import get_class_name + +from . import fields +from .base import Model +from .instances import Instance +from .manager import ObjectManager +from .registry import registry + + +class Class(Model): + """ + OO wrapper around instance classes `endpoint `_. + + :ivar name: :class:`~syncano.models.fields.StringField` + :ivar description: :class:`~syncano.models.fields.StringField` + :ivar objects_count: :class:`~syncano.models.fields.Field` + :ivar schema: :class:`~syncano.models.fields.SchemaField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar status: :class:`~syncano.models.fields.Field` + :ivar metadata: :class:`~syncano.models.fields.JSONField` + :ivar revision: :class:`~syncano.models.fields.IntegerField` + :ivar expected_revision: :class:`~syncano.models.fields.IntegerField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar group: :class:`~syncano.models.fields.IntegerField` + :ivar group_permissions: :class:`~syncano.models.fields.ChoiceField` + :ivar other_permissions: :class:`~syncano.models.fields.ChoiceField` + + .. note:: + This model is special because each related :class:`~syncano.models.base.Object` will be + **dynamically populated** with fields defined in schema attribute. + """ + + LINKS = [ + {'type': 'detail', 'name': 'self'}, + {'type': 'list', 'name': 'objects'}, + ] + + PERMISSIONS_CHOICES = ( + {'display_name': 'None', 'value': 'none'}, + {'display_name': 'Read', 'value': 'read'}, + {'display_name': 'Create objects', 'value': 'create_objects'}, + ) + + name = fields.StringField(max_length=64, primary_key=True) + description = fields.StringField(read_only=False, required=False) + objects_count = fields.Field(read_only=True, required=False) + + schema = fields.SchemaField(read_only=False, required=True) + links = fields.HyperlinkedField(links=LINKS) + status = fields.Field() + metadata = fields.JSONField(read_only=False, required=False) + + revision = fields.IntegerField(read_only=True, required=False) + expected_revision = fields.IntegerField(read_only=False, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + created_at = fields.DateTimeField(read_only=True, required=False) + + group = fields.IntegerField(label='group id', required=False) + group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + + class Meta: + parent = Instance + plural_name = 'Classes' + endpoints = { + 'detail': { + 'methods': ['get', 'put', 'patch', 'delete'], + 'path': '/classes/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/classes/', + } + } + + +class Object(Model): + """ + OO wrapper around data objects `endpoint `_. + + :ivar revision: :class:`~syncano.models.fields.IntegerField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + :ivar owner: :class:`~syncano.models.fields.IntegerField` + :ivar owner_permissions: :class:`~syncano.models.fields.ChoiceField` + :ivar group: :class:`~syncano.models.fields.IntegerField` + :ivar group_permissions: :class:`~syncano.models.fields.ChoiceField` + :ivar other_permissions: :class:`~syncano.models.fields.ChoiceField` + :ivar channel: :class:`~syncano.models.fields.StringField` + :ivar channel_room: :class:`~syncano.models.fields.StringField` + + .. note:: + This model is special because each instance will be **dynamically populated** + with fields defined in related :class:`~syncano.models.base.Class` schema attribute. + """ + PERMISSIONS_CHOICES = ( + {'display_name': 'None', 'value': 'none'}, + {'display_name': 'Read', 'value': 'read'}, + {'display_name': 'Write', 'value': 'write'}, + {'display_name': 'Full', 'value': 'full'}, + ) + + revision = fields.IntegerField(read_only=True, required=False) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + owner = fields.IntegerField(label='owner id', required=False, read_only=True) + owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + group = fields.IntegerField(label='group id', required=False) + group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + channel = fields.StringField(required=False) + channel_room = fields.StringField(required=False, max_length=64) + + please = ObjectManager() + + class Meta: + parent = Class + endpoints = { + 'detail': { + 'methods': ['delete', 'post', 'patch', 'get'], + 'path': '/objects/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/objects/', + } + } + + @staticmethod + def __new__(cls, **kwargs): + instance_name = kwargs.get('instance_name') + class_name = kwargs.get('class_name') + + if not instance_name: + raise SyncanoValidationError('Field "instance_name" is required.') + + if not class_name: + raise SyncanoValidationError('Field "class_name" is required.') + + model = cls.get_subclass_model(instance_name, class_name) + return model(**kwargs) + + @classmethod + def create_subclass(cls, name, schema): + attrs = { + 'Meta': deepcopy(Object._meta), + '__new__': Model.__new__, # We don't want to have maximum recursion depth exceeded error + } + + for field in schema: + field_type = field.get('type') + field_class = fields.MAPPING[field_type] + query_allowed = ('order_index' in field or 'filter_index' in field) + attrs[field['name']] = field_class(required=False, read_only=False, + query_allowed=query_allowed) + + return type(str(name), (Object, ), attrs) + + @classmethod + def get_or_create_subclass(cls, name, schema): + try: + subclass = registry.get_model_by_name(name) + except LookupError: + subclass = cls.create_subclass(name, schema) + registry.add(name, subclass) + + return subclass + + @classmethod + def get_subclass_name(cls, instance_name, class_name): + return get_class_name(instance_name, class_name, 'object') + + @classmethod + def get_class_schema(cls, instance_name, class_name): + parent = cls._meta.parent + class_ = parent.please.get(instance_name, class_name) + return class_.schema + + @classmethod + def get_subclass_model(cls, instance_name, class_name, **kwargs): + """ + Creates custom :class:`~syncano.models.base.Object` sub-class definition based + on passed **instance_name** and **class_name**. + """ + model_name = cls.get_subclass_name(instance_name, class_name) + + if cls.__name__ == model_name: + return cls + + try: + model = registry.get_model_by_name(model_name) + except LookupError: + schema = cls.get_class_schema(instance_name, class_name) + model = cls.create_subclass(model_name, schema) + registry.add(model_name, model) + + return model diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 28f9480..afb5af7 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -4,7 +4,6 @@ import six import validictory - from syncano import logger from syncano.exceptions import SyncanoFieldError, SyncanoValueError from syncano.utils import force_text diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py new file mode 100644 index 0000000..e82ea20 --- /dev/null +++ b/syncano/models/incentives.py @@ -0,0 +1,441 @@ +from __future__ import unicode_literals + +import json + +from syncano.exceptions import SyncanoValidationError + +from . import fields +from .base import Model +from .instances import Instance +from .manager import CodeBoxManager, WebhookManager + + +class CodeBox(Model): + """ + OO wrapper around codeboxes `endpoint `_. + + :ivar label: :class:`~syncano.models.fields.StringField` + :ivar description: :class:`~syncano.models.fields.StringField` + :ivar source: :class:`~syncano.models.fields.StringField` + :ivar runtime_name: :class:`~syncano.models.fields.ChoiceField` + :ivar config: :class:`~syncano.models.fields.Field` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + + .. note:: + **CodeBox** has special method called ``run`` which will execute attached source code:: + + >>> CodeBox.please.run('instance-name', 1234) + >>> CodeBox.please.run('instance-name', 1234, payload={'variable_one': 1, 'variable_two': 2}) + >>> CodeBox.please.run('instance-name', 1234, payload="{\"variable_one\": 1, \"variable_two\": 2}") + + or via instance:: + + >>> cb = CodeBox.please.get('instance-name', 1234) + >>> cb.run() + >>> cb.run(variable_one=1, variable_two=2) + """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + {'type': 'list', 'name': 'runtimes'}, + # This will cause name collision between model run method + # and HyperlinkedField dynamic methods. + # {'type': 'detail', 'name': 'run'}, + {'type': 'detail', 'name': 'traces'}, + ) + RUNTIME_CHOICES = ( + {'display_name': 'nodejs', 'value': 'nodejs'}, + {'display_name': 'python', 'value': 'python'}, + {'display_name': 'ruby', 'value': 'ruby'}, + ) + + label = fields.StringField(max_length=80) + description = fields.StringField(required=False) + source = fields.StringField() + runtime_name = fields.ChoiceField(choices=RUNTIME_CHOICES) + config = fields.Field(required=False) + links = fields.HyperlinkedField(links=LINKS) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + please = CodeBoxManager() + + class Meta: + parent = Instance + name = 'Codebox' + plural_name = 'Codeboxes' + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/codeboxes/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/codeboxes/', + }, + 'run': { + 'methods': ['post'], + 'path': '/codeboxes/{id}/run/', + }, + } + + def run(self, **payload): + """ + Usage:: + + >>> cb = CodeBox.please.get('instance-name', 1234) + >>> cb.run() + >>> cb.run(variable_one=1, variable_two=2) + """ + if self.is_new(): + raise SyncanoValidationError('Method allowed only on existing model.') + + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('run', properties) + connection = self._get_connection(**payload) + request = { + 'data': { + 'payload': json.dumps(payload) + } + } + response = connection.request('POST', endpoint, **request) + response.update({'instance_name': self.instance_name, 'codebox_id': self.id}) + return CodeBoxTrace(**response) + + +class CodeBoxTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + + class Meta: + parent = CodeBox + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } + + +class Schedule(Model): + """ + OO wrapper around codebox schedules `endpoint `_. + + :ivar label: :class:`~syncano.models.fields.StringField` + :ivar codebox: :class:`~syncano.models.fields.IntegerField` + :ivar interval_sec: :class:`~syncano.models.fields.IntegerField` + :ivar crontab: :class:`~syncano.models.fields.StringField` + :ivar payload: :class:`~syncano.models.fields.HyperliStringFieldnkedField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar scheduled_next: :class:`~syncano.models.fields.DateTimeField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + """ + LINKS = [ + {'type': 'detail', 'name': 'self'}, + {'type': 'list', 'name': 'traces'}, + {'type': 'list', 'name': 'codebox'}, + ] + + label = fields.StringField(max_length=80) + codebox = fields.IntegerField(label='codebox id') + interval_sec = fields.IntegerField(read_only=False, required=False) + crontab = fields.StringField(max_length=40, required=False) + payload = fields.StringField(required=False) + created_at = fields.DateTimeField(read_only=True, required=False) + scheduled_next = fields.DateTimeField(read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/schedules/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/schedules/', + } + } + + +class ScheduleTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + + class Meta: + parent = Schedule + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } + + +class Trigger(Model): + """ + OO wrapper around triggers `endpoint `_. + + :ivar label: :class:`~syncano.models.fields.StringField` + :ivar codebox: :class:`~syncano.models.fields.IntegerField` + :ivar class_name: :class:`~syncano.models.fields.StringField` + :ivar signal: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + {'type': 'detail', 'name': 'codebox'}, + {'type': 'detail', 'name': 'class_name'}, + {'type': 'detail', 'name': 'traces'}, + ) + SIGNAL_CHOICES = ( + {'display_name': 'post_update', 'value': 'post_update'}, + {'display_name': 'post_create', 'value': 'post_create'}, + {'display_name': 'post_delete', 'value': 'post_delete'}, + ) + + label = fields.StringField(max_length=80) + codebox = fields.IntegerField(label='codebox id') + class_name = fields.StringField(label='class name', mapping='class') + signal = fields.ChoiceField(choices=SIGNAL_CHOICES) + links = fields.HyperlinkedField(links=LINKS) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/triggers/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/triggers/', + } + } + + +class TriggerTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + + class Meta: + parent = Trigger + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } + + +class Webhook(Model): + """ + OO wrapper around webhooks `endpoint `_. + + :ivar name: :class:`~syncano.models.fields.SlugField` + :ivar codebox: :class:`~syncano.models.fields.IntegerField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + + .. note:: + **WebHook** has special method called ``run`` which will execute related codebox:: + + >>> Webhook.please.run('instance-name', 'webhook-name') + >>> Webhook.please.run('instance-name', 'webhook-name', payload={'variable_one': 1, 'variable_two': 2}) + >>> Webhook.please.run('instance-name', 'webhook-name', + payload="{\"variable_one\": 1, \"variable_two\": 2}") + + or via instance:: + + >>> wh = Webhook.please.get('instance-name', 'webhook-name') + >>> wh.run() + >>> wh.run(variable_one=1, variable_two=2) + + """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + {'type': 'detail', 'name': 'codebox'}, + ) + + name = fields.SlugField(max_length=50, primary_key=True) + codebox = fields.IntegerField(label='codebox id') + public = fields.BooleanField(required=False, default=False) + public_link = fields.ChoiceField(required=False, read_only=True) + links = fields.HyperlinkedField(links=LINKS) + + please = WebhookManager() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/webhooks/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/webhooks/', + }, + 'run': { + 'methods': ['post'], + 'path': '/webhooks/{name}/run/', + }, + 'reset': { + 'methods': ['post'], + 'path': '/webhooks/{name}/reset_link/', + }, + 'public': { + 'methods': ['get'], + 'path': '/webhooks/p/{public_link}/', + } + } + + def run(self, **payload): + """ + Usage:: + + >>> wh = Webhook.please.get('instance-name', 'webhook-name') + >>> wh.run() + >>> wh.run(variable_one=1, variable_two=2) + """ + if self.is_new(): + raise SyncanoValidationError('Method allowed only on existing model.') + + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('run', properties) + connection = self._get_connection(**payload) + request = { + 'data': { + 'payload': json.dumps(payload) + } + } + response = connection.request('POST', endpoint, **request) + response.update({'instance_name': self.instance_name, 'webhook_name': self.name}) + return WebhookTrace(**response) + + def reset(self, **payload): + """ + Usage:: + + >>> wh = Webhook.please.get('instance-name', 'webhook-name') + >>> wh.reset() + """ + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('reset', properties) + connection = self._get_connection(**payload) + return connection.request('POST', endpoint) + + +class WebhookTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + + class Meta: + parent = Webhook + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } diff --git a/syncano/models/instances.py b/syncano/models/instances.py new file mode 100644 index 0000000..33e1b0c --- /dev/null +++ b/syncano/models/instances.py @@ -0,0 +1,127 @@ +from __future__ import unicode_literals + +from . import fields +from .base import Model + + +class Instance(Model): + """ + OO wrapper around instances `endpoint `_. + + :ivar name: :class:`~syncano.models.fields.StringField` + :ivar description: :class:`~syncano.models.fields.StringField` + :ivar role: :class:`~syncano.models.fields.Field` + :ivar owner: :class:`~syncano.models.fields.ModelField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar metadata: :class:`~syncano.models.fields.JSONField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + + LINKS = ( + {'type': 'detail', 'name': 'self'}, + {'type': 'list', 'name': 'admins'}, + {'type': 'list', 'name': 'classes'}, + {'type': 'list', 'name': 'codeboxes'}, + {'type': 'list', 'name': 'invitations'}, + {'type': 'list', 'name': 'runtimes'}, + {'type': 'list', 'name': 'api_keys'}, + {'type': 'list', 'name': 'triggers'}, + {'type': 'list', 'name': 'users'}, + {'type': 'list', 'name': 'webhooks'}, + {'type': 'list', 'name': 'schedules'}, + ) + + name = fields.StringField(max_length=64, primary_key=True) + description = fields.StringField(read_only=False, required=False) + role = fields.Field(read_only=True, required=False) + owner = fields.ModelField('Admin', read_only=True) + links = fields.HyperlinkedField(links=LINKS) + metadata = fields.JSONField(read_only=False, required=False) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + endpoints = { + 'detail': { + 'methods': ['delete', 'patch', 'put', 'get'], + 'path': '/v1/instances/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/v1/instances/', + } + } + + +class ApiKey(Model): + """ + OO wrapper around instance api keys `endpoint `_. + + :ivar api_key: :class:`~syncano.models.fields.StringField` + :ivar allow_user_create: :class:`~syncano.models.fields.BooleanField` + :ivar ignore_acl: :class:`~syncano.models.fields.BooleanField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + """ + LINKS = [ + {'type': 'detail', 'name': 'self'}, + ] + + api_key = fields.StringField(read_only=True, required=False) + allow_user_create = fields.BooleanField(required=False, default=False) + ignore_acl = fields.BooleanField(required=False, default=False) + links = fields.HyperlinkedField(links=LINKS) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['get', 'delete'], + 'path': '/api_keys/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/api_keys/', + } + } + + +class InstanceInvitation(Model): + """ + OO wrapper around instance invitations + `endpoint `_. + + :ivar email: :class:`~syncano.models.fields.EmailField` + :ivar role: :class:`~syncano.models.fields.ChoiceField` + :ivar key: :class:`~syncano.models.fields.StringField` + :ivar state: :class:`~syncano.models.fields.StringField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + from .users import Admin + + email = fields.EmailField(max_length=254) + role = fields.ChoiceField(choices=Admin.ROLE_CHOICES) + key = fields.StringField(read_only=True, required=False) + state = fields.StringField(read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + name = 'Invitation' + endpoints = { + 'detail': { + 'methods': ['get', 'delete'], + 'path': '/invitations/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/invitations/', + } + } diff --git a/syncano/models/manager.py b/syncano/models/manager.py index b4a210e..e8e08c2 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -3,7 +3,6 @@ from functools import wraps import six - from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValueError diff --git a/syncano/models/options.py b/syncano/models/options.py index 1a4e9fa..0239a8c 100644 --- a/syncano/models/options.py +++ b/syncano/models/options.py @@ -3,7 +3,6 @@ from urlparse import urljoin import six - from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoValueError from syncano.models.registry import registry diff --git a/syncano/models/registry.py b/syncano/models/registry.py index 93f1a90..05cfdc3 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -3,13 +3,12 @@ import re import six - from syncano import logger class Registry(object): - """Models registry.""" - + """Models registry. + """ def __init__(self, models=None): self.models = models or {} self.patterns = [] diff --git a/tests/integration_test.py b/tests/integration_test.py index 6ca1cc6..35afe6c 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -6,7 +6,7 @@ from uuid import uuid4 import syncano -from syncano.exceptions import SyncanoValueError, SyncanoRequestError +from syncano.exceptions import SyncanoRequestError, SyncanoValueError from syncano.models import Class, CodeBox, Instance, Object, Webhook diff --git a/tests/test_manager.py b/tests/test_manager.py index 1969e4e..813e0e5 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1,10 +1,19 @@ import unittest from datetime import datetime -from syncano.exceptions import (SyncanoDoesNotExist, SyncanoRequestError, - SyncanoValueError) -from syncano.models.base import (CodeBox, CodeBoxTrace, Instance, Object, - Webhook, WebhookTrace) +from syncano.exceptions import ( + SyncanoDoesNotExist, + SyncanoRequestError, + SyncanoValueError +) +from syncano.models import ( + CodeBox, + CodeBoxTrace, + Instance, + Object, + Webhook, + WebhookTrace +) try: from unittest import mock @@ -379,6 +388,7 @@ def test_get_allowed_method(self): self.manager.endpoint = 'detail' result = self.manager.get_allowed_method('GET', 'POST') + self.assertEqual(result, 'GET') result = self.manager.get_allowed_method('DELETE', 'POST') @@ -463,7 +473,7 @@ def setUp(self): self.model = Object self.manager = Object.please - @mock.patch('syncano.models.base.Object.get_subclass_model') + @mock.patch('syncano.models.Object.get_subclass_model') def test_create(self, get_subclass_model_mock): model_mock = mock.MagicMock() model_mock.return_value = model_mock @@ -482,7 +492,7 @@ def test_create(self, get_subclass_model_mock): model_mock.save.assert_called_once_with() get_subclass_model_mock.assert_called_once_with(a=1, b=2) - @mock.patch('syncano.models.base.Object.get_subclass_model') + @mock.patch('syncano.models.Object.get_subclass_model') def test_serialize(self, get_subclass_model_mock): get_subclass_model_mock.return_value = mock.Mock self.manager.properties['instance_name'] = 'test' @@ -494,7 +504,7 @@ def test_serialize(self, get_subclass_model_mock): get_subclass_model_mock.assert_called_once_with(instance_name='test', class_name='test') @mock.patch('syncano.models.manager.ObjectManager._clone') - @mock.patch('syncano.models.base.Object.get_subclass_model') + @mock.patch('syncano.models.Object.get_subclass_model') def test_filter(self, get_subclass_model_mock, clone_mock): get_subclass_model_mock.return_value = Instance clone_mock.return_value = self.manager diff --git a/tests/test_models.py b/tests/test_models.py index 6c2ca31..9b64538 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -241,7 +241,7 @@ def setUp(self): } ] - @mock.patch('syncano.models.base.Object.get_subclass_model') + @mock.patch('syncano.models.Object.get_subclass_model') def test_new(self, get_subclass_model_mock): get_subclass_model_mock.return_value = Instance self.assertFalse(get_subclass_model_mock.called) @@ -271,8 +271,8 @@ def test_create_subclass(self): self.assertFalse(field.required) self.assertFalse(field.read_only) - @mock.patch('syncano.models.base.registry') - @mock.patch('syncano.models.base.Object.create_subclass') + @mock.patch('syncano.models.classes.registry') + @mock.patch('syncano.models.Object.create_subclass') def test_get_or_create_subclass(self, create_subclass_mock, registry_mock): create_subclass_mock.return_value = 1 registry_mock.get_model_by_name.side_effect = [2, LookupError] @@ -319,10 +319,10 @@ def test_get_class_schema(self, get_mock): self.assertEqual(result, get_mock.schema) get_mock.assert_called_once_with('dummy-instance', 'dummy-class') - @mock.patch('syncano.models.base.Object.create_subclass') - @mock.patch('syncano.models.base.Object.get_class_schema') + @mock.patch('syncano.models.Object.create_subclass') + @mock.patch('syncano.models.Object.get_class_schema') @mock.patch('syncano.models.manager.registry.get_model_by_name') - @mock.patch('syncano.models.base.Object.get_subclass_name') + @mock.patch('syncano.models.Object.get_subclass_name') def test_get_subclass_model(self, get_subclass_name_mock, get_model_by_name_mock, get_class_schema_mock, create_subclass_mock): diff --git a/tests/test_options.py b/tests/test_options.py index 438bc94..39514b3 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,8 +1,7 @@ import unittest from syncano.exceptions import SyncanoValueError -from syncano.models.base import Instance -from syncano.models.fields import Field +from syncano.models import Field, Instance from syncano.models.options import Options From 373445ec840a247c7132833982893e2aa1761db5 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 24 Jul 2015 14:35:55 +0200 Subject: [PATCH 057/558] fix unused imports --- syncano/models/base.py | 5 +---- syncano/models/instances.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 60581eb..1654897 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -1,15 +1,12 @@ from __future__ import unicode_literals import inspect -import json -from copy import deepcopy import six from syncano.exceptions import SyncanoDoesNotExist, SyncanoValidationError -from syncano.utils import get_class_name from . import fields -from .manager import CodeBoxManager, Manager, ObjectManager, WebhookManager +from .manager import Manager from .options import Options from .registry import registry diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 33e1b0c..782f7d5 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -102,7 +102,7 @@ class InstanceInvitation(Model): LINKS = ( {'type': 'detail', 'name': 'self'}, ) - from .users import Admin + from .accounts import Admin email = fields.EmailField(max_length=254) role = fields.ChoiceField(choices=Admin.ROLE_CHOICES) From 0fb1ba86a8b1b2418b92cc44a7d7c3b754b9a6d8 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 24 Jul 2015 14:52:22 +0200 Subject: [PATCH 058/558] separate traces from incentives --- syncano/models/__init__.py | 1 + syncano/models/incentives.py | 156 +--------------------------------- syncano/models/traces.py | 158 +++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 152 deletions(-) create mode 100644 syncano/models/traces.py diff --git a/syncano/models/__init__.py b/syncano/models/__init__.py index 5e82729..2292e96 100644 --- a/syncano/models/__init__.py +++ b/syncano/models/__init__.py @@ -6,3 +6,4 @@ from .channels import * # NOQA from .classes import * # NOQA from .incentives import * # NOQA +from .traces import * # NOQA diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index e82ea20..6aacbc7 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -88,6 +88,8 @@ def run(self, **payload): >>> cb.run() >>> cb.run(variable_one=1, variable_two=2) """ + from .traces import CodeBoxTrace + if self.is_new(): raise SyncanoValidationError('Method allowed only on existing model.') @@ -104,44 +106,6 @@ def run(self, **payload): return CodeBoxTrace(**response) -class CodeBoxTrace(Model): - """ - :ivar status: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` - :ivar result: :class:`~syncano.models.fields.StringField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - STATUS_CHOICES = ( - {'display_name': 'Success', 'value': 'success'}, - {'display_name': 'Failure', 'value': 'failure'}, - {'display_name': 'Timeout', 'value': 'timeout'}, - {'display_name': 'Pending', 'value': 'pending'}, - ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) - duration = fields.IntegerField(read_only=True, required=False) - - class Meta: - parent = CodeBox - endpoints = { - 'detail': { - 'methods': ['get'], - 'path': '/traces/{id}/', - }, - 'list': { - 'methods': ['get'], - 'path': '/traces/', - } - } - - class Schedule(Model): """ OO wrapper around codebox schedules `endpoint `_. @@ -184,44 +148,6 @@ class Meta: } -class ScheduleTrace(Model): - """ - :ivar status: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` - :ivar result: :class:`~syncano.models.fields.StringField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - STATUS_CHOICES = ( - {'display_name': 'Success', 'value': 'success'}, - {'display_name': 'Failure', 'value': 'failure'}, - {'display_name': 'Timeout', 'value': 'timeout'}, - {'display_name': 'Pending', 'value': 'pending'}, - ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) - duration = fields.IntegerField(read_only=True, required=False) - - class Meta: - parent = Schedule - endpoints = { - 'detail': { - 'methods': ['get'], - 'path': '/traces/{id}/', - }, - 'list': { - 'methods': ['get'], - 'path': '/traces/', - } - } - - class Trigger(Model): """ OO wrapper around triggers `endpoint `_. @@ -268,44 +194,6 @@ class Meta: } -class TriggerTrace(Model): - """ - :ivar status: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` - :ivar result: :class:`~syncano.models.fields.StringField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - STATUS_CHOICES = ( - {'display_name': 'Success', 'value': 'success'}, - {'display_name': 'Failure', 'value': 'failure'}, - {'display_name': 'Timeout', 'value': 'timeout'}, - {'display_name': 'Pending', 'value': 'pending'}, - ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) - duration = fields.IntegerField(read_only=True, required=False) - - class Meta: - parent = Trigger - endpoints = { - 'detail': { - 'methods': ['get'], - 'path': '/traces/{id}/', - }, - 'list': { - 'methods': ['get'], - 'path': '/traces/', - } - } - - class Webhook(Model): """ OO wrapper around webhooks `endpoint `_. @@ -375,6 +263,8 @@ def run(self, **payload): >>> wh.run() >>> wh.run(variable_one=1, variable_two=2) """ + from .traces import WebhookTrace + if self.is_new(): raise SyncanoValidationError('Method allowed only on existing model.') @@ -401,41 +291,3 @@ def reset(self, **payload): endpoint = self._meta.resolve_endpoint('reset', properties) connection = self._get_connection(**payload) return connection.request('POST', endpoint) - - -class WebhookTrace(Model): - """ - :ivar status: :class:`~syncano.models.fields.ChoiceField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` - :ivar result: :class:`~syncano.models.fields.StringField` - :ivar duration: :class:`~syncano.models.fields.IntegerField` - """ - STATUS_CHOICES = ( - {'display_name': 'Success', 'value': 'success'}, - {'display_name': 'Failure', 'value': 'failure'}, - {'display_name': 'Timeout', 'value': 'timeout'}, - {'display_name': 'Pending', 'value': 'pending'}, - ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) - executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) - duration = fields.IntegerField(read_only=True, required=False) - - class Meta: - parent = Webhook - endpoints = { - 'detail': { - 'methods': ['get'], - 'path': '/traces/{id}/', - }, - 'list': { - 'methods': ['get'], - 'path': '/traces/', - } - } diff --git a/syncano/models/traces.py b/syncano/models/traces.py new file mode 100644 index 0000000..b794796 --- /dev/null +++ b/syncano/models/traces.py @@ -0,0 +1,158 @@ +from __future__ import unicode_literals + + +from . import fields +from .base import Model +from .incentives import CodeBox, Schedule, Trigger, Webhook + + +class CodeBoxTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + + class Meta: + parent = CodeBox + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } + + +class ScheduleTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + + class Meta: + parent = Schedule + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } + + +class TriggerTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + + class Meta: + parent = Trigger + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } + + +class WebhookTrace(Model): + """ + :ivar status: :class:`~syncano.models.fields.ChoiceField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar executed_at: :class:`~syncano.models.fields.DateTimeField` + :ivar result: :class:`~syncano.models.fields.StringField` + :ivar duration: :class:`~syncano.models.fields.IntegerField` + """ + STATUS_CHOICES = ( + {'display_name': 'Success', 'value': 'success'}, + {'display_name': 'Failure', 'value': 'failure'}, + {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + links = fields.HyperlinkedField(links=LINKS) + executed_at = fields.DateTimeField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) + duration = fields.IntegerField(read_only=True, required=False) + + class Meta: + parent = Webhook + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/traces/', + } + } From ef91753c56f66104914acc6f09cbda250567ed8e Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 24 Jul 2015 18:22:19 +0200 Subject: [PATCH 059/558] add groups --- syncano/models/accounts.py | 65 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 3 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 522fc0c..7e551d6 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -46,7 +46,7 @@ class Meta: class User(Model): """ - OO wrapper around instances `endpoint `_. + OO wrapper around users `endpoint `_. :ivar username: :class:`~syncano.models.fields.StringField` :ivar password: :class:`~syncano.models.fields.StringField` @@ -84,8 +84,67 @@ class Meta: } } - def reset_key(self, **payload): + def reset_key(self): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('reset_key', properties) - connection = self._get_connection(**payload) + connection = self._get_connection() return connection.request('POST', endpoint) + + +class Group(Model): + """ + OO wrapper around users `endpoint `_. + + :ivar label: :class:`~syncano.models.fields.StringField` + :ivar description: :class:`~syncano.models.fields.StringField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + label = fields.StringField(max_length=64, required=True) + description = fields.StringField(read_only=False, required=False) + + links = fields.HyperlinkedField(links=LINKS) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'patch', 'put', 'get'], + 'path': '/groups/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/groups/', + }, + 'users': { + 'methods': ['get', 'post', 'delete'], + 'path': '/groups/{id}/users/', + } + } + + def group_users_method(self, user_id=None, method='GET'): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('reset_key', properties) + if user_id is not None: + endpoint += '{}/'.format(user_id) + connection = self._get_connection() + return connection.request(method, endpoint) + + def list_users(self): + return self.group_users_method() + + def add_user(self, user_id): + return self.group_users_method(user_id, method='POST') + + def get_user_details(self, user_id): + return self.group_users_method(user_id) + + def delete_user(self, user_id): + return self.group_users_method(user_id, method='DELETE') From ae453da415ec5bda76889ada3380b45aa415c396 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 24 Jul 2015 18:34:59 +0200 Subject: [PATCH 060/558] add user profile --- syncano/models/accounts.py | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 7e551d6..7243cfd 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -44,6 +44,51 @@ class Meta: } +class Profile(Model): + """ + """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + PERMISSIONS_CHOICES = ( + {'display_name': 'None', 'value': 'none'}, + {'display_name': 'Read', 'value': 'read'}, + {'display_name': 'Create users', 'value': 'create_users'}, + ) + + owner = fields.IntegerField(label='owner id', required=False, read_only=True) + owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + group = fields.IntegerField(label='group id', required=False) + group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + channel = fields.StringField(required=False) + channel_room = fields.StringField(required=False, max_length=64) + + schema = fields.SchemaField(read_only=False, required=True) + + links = fields.HyperlinkedField(links=LINKS) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'patch', 'put', 'get'], + 'path': '/users/{id}/', + }, + 'reset_key': { + 'methods': ['post'], + 'path': '/users/{id}/reset_key/', + }, + 'list': { + 'methods': ['get'], + 'path': '/users/', + } + } + + class User(Model): """ OO wrapper around users `endpoint `_. @@ -63,6 +108,8 @@ class User(Model): password = fields.StringField(read_only=False, required=True) user_key = fields.StringField(read_only=True, required=False) + profile = fields.ModelField('Profile') + links = fields.HyperlinkedField(links=LINKS) created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) From 99ba8345253fabc587766640591282a785d9ef62 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 27 Jul 2015 09:50:47 +0200 Subject: [PATCH 061/558] add group lookup for users --- syncano/models/accounts.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 7243cfd..11f4a04 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -65,8 +65,6 @@ class Profile(Model): channel = fields.StringField(required=False) channel_room = fields.StringField(required=False, max_length=64) - schema = fields.SchemaField(read_only=False, required=True) - links = fields.HyperlinkedField(links=LINKS) created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) @@ -76,15 +74,11 @@ class Meta: endpoints = { 'detail': { 'methods': ['delete', 'patch', 'put', 'get'], - 'path': '/users/{id}/', - }, - 'reset_key': { - 'methods': ['post'], - 'path': '/users/{id}/reset_key/', + 'path': '/user_profile/objects/{id}/', }, 'list': { 'methods': ['get'], - 'path': '/users/', + 'path': '/user_profile/objects/', } } @@ -128,6 +122,10 @@ class Meta: 'list': { 'methods': ['get'], 'path': '/users/', + }, + 'groups': { + 'methods': ['get', 'post'], + 'path': '/users/{id}/groups/', } } @@ -137,6 +135,20 @@ def reset_key(self): connection = self._get_connection() return connection.request('POST', endpoint) + def user_groups_method(self, group_id=None, method='GET'): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('groups', properties) + if group_id is not None: + endpoint += '{}/'.format(group_id) + connection = self._get_connection() + return connection.request(method, endpoint) + + def list_groups(self): + return self.user_groups_method() + + def group_details(self, group_id): + return self.user_groups_method(group_id) + class Group(Model): """ @@ -178,7 +190,7 @@ class Meta: def group_users_method(self, user_id=None, method='GET'): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('reset_key', properties) + endpoint = self._meta.resolve_endpoint('users', properties) if user_id is not None: endpoint += '{}/'.format(user_id) connection = self._get_connection() @@ -190,7 +202,7 @@ def list_users(self): def add_user(self, user_id): return self.group_users_method(user_id, method='POST') - def get_user_details(self, user_id): + def user_details(self, user_id): return self.group_users_method(user_id) def delete_user(self, user_id): From eeee94f5fc37e9a3578ca4b488c2f5153309a375 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 27 Jul 2015 10:38:26 +0200 Subject: [PATCH 062/558] separate models tests --- tests/test_classes.py | 147 +++++++++++++++++++++++++++ tests/test_incentives.py | 77 ++++++++++++++ tests/test_models.py | 210 +-------------------------------------- 3 files changed, 226 insertions(+), 208 deletions(-) create mode 100644 tests/test_classes.py create mode 100644 tests/test_incentives.py diff --git a/tests/test_classes.py b/tests/test_classes.py new file mode 100644 index 0000000..20fd23a --- /dev/null +++ b/tests/test_classes.py @@ -0,0 +1,147 @@ +import unittest + +from syncano.exceptions import SyncanoValueError +from syncano.models import Instance, Object + +try: + from unittest import mock +except ImportError: + import mock + + +class ObjectTestCase(unittest.TestCase): + + def setUp(self): + self.schema = [ + { + 'name': 'title', + 'type': 'string', + 'order_index': True, + 'filter_index': True + }, + { + 'name': 'release_year', + 'type': 'integer', + 'order_index': True, + 'filter_index': True + }, + { + 'name': 'price', + 'type': 'float', + 'order_index': True, + 'filter_index': True + }, + { + 'name': 'author', + 'type': 'reference', + 'order_index': True, + 'filter_index': True, + 'target': 'Author' + } + ] + + @mock.patch('syncano.models.Object.get_subclass_model') + def test_new(self, get_subclass_model_mock): + get_subclass_model_mock.return_value = Instance + self.assertFalse(get_subclass_model_mock.called) + + with self.assertRaises(SyncanoValueError): + Object() + + with self.assertRaises(SyncanoValueError): + Object(instance_name='dummy') + + self.assertFalse(get_subclass_model_mock.called) + o = Object(instance_name='dummy', class_name='dummy', x=1, y=2) + self.assertIsInstance(o, Instance) + self.assertTrue(get_subclass_model_mock.called) + get_subclass_model_mock.assert_called_once_with('dummy', 'dummy') + + def test_create_subclass(self): + SubClass = Object.create_subclass('Test', self.schema) + fields = [f for f in SubClass._meta.fields if f not in Object._meta.fields] + + self.assertEqual(SubClass.__name__, 'Test') + + for schema, field in zip(self.schema, fields): + query_allowed = ('order_index' in schema or 'filter_index' in schema) + self.assertEqual(schema['name'], field.name) + self.assertEqual(field.query_allowed, query_allowed) + self.assertFalse(field.required) + self.assertFalse(field.read_only) + + @mock.patch('syncano.models.classes.registry') + @mock.patch('syncano.models.Object.create_subclass') + def test_get_or_create_subclass(self, create_subclass_mock, registry_mock): + create_subclass_mock.return_value = 1 + registry_mock.get_model_by_name.side_effect = [2, LookupError] + + self.assertFalse(registry_mock.get_model_by_name.called) + self.assertFalse(registry_mock.add.called) + self.assertFalse(create_subclass_mock.called) + + model = Object.get_or_create_subclass('test', [{}, {}]) + self.assertEqual(model, 2) + + self.assertTrue(registry_mock.get_model_by_name.called) + self.assertFalse(registry_mock.add.called) + self.assertFalse(create_subclass_mock.called) + registry_mock.get_model_by_name.assert_called_with('test') + + model = Object.get_or_create_subclass('test', [{}, {}]) + self.assertEqual(model, 1) + + self.assertTrue(registry_mock.get_model_by_name.called) + self.assertTrue(registry_mock.add.called) + self.assertTrue(create_subclass_mock.called) + + registry_mock.get_model_by_name.assert_called_with('test') + create_subclass_mock.assert_called_with('test', [{}, {}]) + registry_mock.add.assert_called_with('test', 1) + + self.assertEqual(registry_mock.get_model_by_name.call_count, 2) + self.assertEqual(registry_mock.add.call_count, 1) + self.assertEqual(create_subclass_mock.call_count, 1) + + def test_get_subclass_name(self): + self.assertEqual(Object.get_subclass_name('', ''), 'Object') + self.assertEqual(Object.get_subclass_name('duMMY', ''), 'DummyObject') + self.assertEqual(Object.get_subclass_name('', 'ClS'), 'ClsObject') + self.assertEqual(Object.get_subclass_name('duMMy', 'CLS'), 'DummyClsObject') + + @mock.patch('syncano.models.Manager.get') + def test_get_class_schema(self, get_mock): + get_mock.return_value = get_mock + self.assertFalse(get_mock.called) + result = Object.get_class_schema('dummy-instance', 'dummy-class') + self.assertTrue(get_mock.called) + self.assertEqual(result, get_mock.schema) + get_mock.assert_called_once_with('dummy-instance', 'dummy-class') + + @mock.patch('syncano.models.Object.create_subclass') + @mock.patch('syncano.models.Object.get_class_schema') + @mock.patch('syncano.models.manager.registry.get_model_by_name') + @mock.patch('syncano.models.Object.get_subclass_name') + def test_get_subclass_model(self, get_subclass_name_mock, get_model_by_name_mock, + get_class_schema_mock, create_subclass_mock): + + create_subclass_mock.return_value = create_subclass_mock + get_subclass_name_mock.side_effect = [ + 'Object', + 'DummyObject', + 'DummyObject', + ] + + get_model_by_name_mock.side_effect = [ + Object, + LookupError + ] + + result = Object.get_subclass_model('', '') + self.assertEqual(Object, result) + + result = Object.get_subclass_model('', '') + self.assertEqual(Object, result) + + result = Object.get_subclass_model('', '') + self.assertEqual(create_subclass_mock, result) diff --git a/tests/test_incentives.py b/tests/test_incentives.py new file mode 100644 index 0000000..084e2d7 --- /dev/null +++ b/tests/test_incentives.py @@ -0,0 +1,77 @@ +import unittest +from datetime import datetime + +from syncano.exceptions import SyncanoValidationError +from syncano.models import CodeBox, CodeBoxTrace, Webhook, WebhookTrace + + +try: + from unittest import mock +except ImportError: + import mock + + +class CodeBoxTestCase(unittest.TestCase): + + def setUp(self): + self.model = CodeBox() + + @mock.patch('syncano.models.CodeBox._get_connection') + def test_run(self, connection_mock): + model = CodeBox(instance_name='test', id=10, links={'run': '/v1/instances/test/codeboxes/10/run/'}) + connection_mock.return_value = connection_mock + connection_mock.request.return_value = {'id': 10} + + self.assertFalse(connection_mock.called) + self.assertFalse(connection_mock.request.called) + result = model.run(a=1, b=2) + self.assertTrue(connection_mock.called) + self.assertTrue(connection_mock.request.called) + self.assertIsInstance(result, CodeBoxTrace) + + connection_mock.assert_called_once_with(a=1, b=2) + connection_mock.request.assert_called_once_with( + 'POST', '/v1/instances/test/codeboxes/10/run/', data={'payload': '{"a": 1, "b": 2}'} + ) + + model = CodeBox() + with self.assertRaises(SyncanoValidationError): + model.run() + + +class WebhookTestCase(unittest.TestCase): + def setUp(self): + self.model = Webhook() + + @mock.patch('syncano.models.Webhook._get_connection') + def test_run(self, connection_mock): + model = Webhook(instance_name='test', name='name', links={'run': '/v1/instances/test/webhooks/name/run/'}) + connection_mock.return_value = connection_mock + connection_mock.request.return_value = { + 'status': 'success', + 'duration': 937, + 'result': '1', + 'executed_at': '2015-03-16T11:52:14.172830Z' + } + + self.assertFalse(connection_mock.called) + self.assertFalse(connection_mock.request.called) + result = model.run(x=1, y=2) + self.assertTrue(connection_mock.called) + self.assertTrue(connection_mock.request.called) + self.assertIsInstance(result, WebhookTrace) + self.assertEqual(result.status, 'success') + self.assertEqual(result.duration, 937) + self.assertEqual(result.result, '1') + self.assertIsInstance(result.executed_at, datetime) + + connection_mock.assert_called_once_with(x=1, y=2) + connection_mock.request.assert_called_once_with( + 'POST', + '/v1/instances/test/webhooks/name/run/', + data={'payload': '{"y": 2, "x": 1}'} + ) + + model = Webhook() + with self.assertRaises(SyncanoValidationError): + model.run() diff --git a/tests/test_models.py b/tests/test_models.py index 9b64538..043d047 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,9 +1,7 @@ import unittest -from datetime import datetime -from syncano.exceptions import SyncanoValidationError, SyncanoValueError -from syncano.models import (CodeBox, CodeBoxTrace, Instance, Object, Webhook, - WebhookTrace) +from syncano.exceptions import SyncanoValidationError +from syncano.models import Instance try: from unittest import mock @@ -180,207 +178,3 @@ def test_to_native(self): self.model.description = 'desc' self.model.dummy = 'test' self.assertEqual(self.model.to_native(), {'name': 'test', 'description': 'desc'}) - - -class CodeBoxTestCase(unittest.TestCase): - - def setUp(self): - self.model = CodeBox() - - @mock.patch('syncano.models.CodeBox._get_connection') - def test_run(self, connection_mock): - model = CodeBox(instance_name='test', id=10, links={'run': '/v1/instances/test/codeboxes/10/run/'}) - connection_mock.return_value = connection_mock - connection_mock.request.return_value = {'id': 10} - - self.assertFalse(connection_mock.called) - self.assertFalse(connection_mock.request.called) - result = model.run(a=1, b=2) - self.assertTrue(connection_mock.called) - self.assertTrue(connection_mock.request.called) - self.assertIsInstance(result, CodeBoxTrace) - - connection_mock.assert_called_once_with(a=1, b=2) - connection_mock.request.assert_called_once_with( - 'POST', '/v1/instances/test/codeboxes/10/run/', data={'payload': '{"a": 1, "b": 2}'} - ) - - model = CodeBox() - with self.assertRaises(SyncanoValidationError): - model.run() - - -class ObjectTestCase(unittest.TestCase): - - def setUp(self): - self.schema = [ - { - 'name': 'title', - 'type': 'string', - 'order_index': True, - 'filter_index': True - }, - { - 'name': 'release_year', - 'type': 'integer', - 'order_index': True, - 'filter_index': True - }, - { - 'name': 'price', - 'type': 'float', - 'order_index': True, - 'filter_index': True - }, - { - 'name': 'author', - 'type': 'reference', - 'order_index': True, - 'filter_index': True, - 'target': 'Author' - } - ] - - @mock.patch('syncano.models.Object.get_subclass_model') - def test_new(self, get_subclass_model_mock): - get_subclass_model_mock.return_value = Instance - self.assertFalse(get_subclass_model_mock.called) - - with self.assertRaises(SyncanoValueError): - Object() - - with self.assertRaises(SyncanoValueError): - Object(instance_name='dummy') - - self.assertFalse(get_subclass_model_mock.called) - o = Object(instance_name='dummy', class_name='dummy', x=1, y=2) - self.assertIsInstance(o, Instance) - self.assertTrue(get_subclass_model_mock.called) - get_subclass_model_mock.assert_called_once_with('dummy', 'dummy') - - def test_create_subclass(self): - SubClass = Object.create_subclass('Test', self.schema) - fields = [f for f in SubClass._meta.fields if f not in Object._meta.fields] - - self.assertEqual(SubClass.__name__, 'Test') - - for schema, field in zip(self.schema, fields): - query_allowed = ('order_index' in schema or 'filter_index' in schema) - self.assertEqual(schema['name'], field.name) - self.assertEqual(field.query_allowed, query_allowed) - self.assertFalse(field.required) - self.assertFalse(field.read_only) - - @mock.patch('syncano.models.classes.registry') - @mock.patch('syncano.models.Object.create_subclass') - def test_get_or_create_subclass(self, create_subclass_mock, registry_mock): - create_subclass_mock.return_value = 1 - registry_mock.get_model_by_name.side_effect = [2, LookupError] - - self.assertFalse(registry_mock.get_model_by_name.called) - self.assertFalse(registry_mock.add.called) - self.assertFalse(create_subclass_mock.called) - - model = Object.get_or_create_subclass('test', [{}, {}]) - self.assertEqual(model, 2) - - self.assertTrue(registry_mock.get_model_by_name.called) - self.assertFalse(registry_mock.add.called) - self.assertFalse(create_subclass_mock.called) - registry_mock.get_model_by_name.assert_called_with('test') - - model = Object.get_or_create_subclass('test', [{}, {}]) - self.assertEqual(model, 1) - - self.assertTrue(registry_mock.get_model_by_name.called) - self.assertTrue(registry_mock.add.called) - self.assertTrue(create_subclass_mock.called) - - registry_mock.get_model_by_name.assert_called_with('test') - create_subclass_mock.assert_called_with('test', [{}, {}]) - registry_mock.add.assert_called_with('test', 1) - - self.assertEqual(registry_mock.get_model_by_name.call_count, 2) - self.assertEqual(registry_mock.add.call_count, 1) - self.assertEqual(create_subclass_mock.call_count, 1) - - def test_get_subclass_name(self): - self.assertEqual(Object.get_subclass_name('', ''), 'Object') - self.assertEqual(Object.get_subclass_name('duMMY', ''), 'DummyObject') - self.assertEqual(Object.get_subclass_name('', 'ClS'), 'ClsObject') - self.assertEqual(Object.get_subclass_name('duMMy', 'CLS'), 'DummyClsObject') - - @mock.patch('syncano.models.Manager.get') - def test_get_class_schema(self, get_mock): - get_mock.return_value = get_mock - self.assertFalse(get_mock.called) - result = Object.get_class_schema('dummy-instance', 'dummy-class') - self.assertTrue(get_mock.called) - self.assertEqual(result, get_mock.schema) - get_mock.assert_called_once_with('dummy-instance', 'dummy-class') - - @mock.patch('syncano.models.Object.create_subclass') - @mock.patch('syncano.models.Object.get_class_schema') - @mock.patch('syncano.models.manager.registry.get_model_by_name') - @mock.patch('syncano.models.Object.get_subclass_name') - def test_get_subclass_model(self, get_subclass_name_mock, get_model_by_name_mock, - get_class_schema_mock, create_subclass_mock): - - create_subclass_mock.return_value = create_subclass_mock - get_subclass_name_mock.side_effect = [ - 'Object', - 'DummyObject', - 'DummyObject', - ] - - get_model_by_name_mock.side_effect = [ - Object, - LookupError - ] - - result = Object.get_subclass_model('', '') - self.assertEqual(Object, result) - - result = Object.get_subclass_model('', '') - self.assertEqual(Object, result) - - result = Object.get_subclass_model('', '') - self.assertEqual(create_subclass_mock, result) - - -class WebhookTestCase(unittest.TestCase): - def setUp(self): - self.model = Webhook() - - @mock.patch('syncano.models.Webhook._get_connection') - def test_run(self, connection_mock): - model = Webhook(instance_name='test', name='name', links={'run': '/v1/instances/test/webhooks/name/run/'}) - connection_mock.return_value = connection_mock - connection_mock.request.return_value = { - 'status': 'success', - 'duration': 937, - 'result': '1', - 'executed_at': '2015-03-16T11:52:14.172830Z' - } - - self.assertFalse(connection_mock.called) - self.assertFalse(connection_mock.request.called) - result = model.run(x=1, y=2) - self.assertTrue(connection_mock.called) - self.assertTrue(connection_mock.request.called) - self.assertIsInstance(result, WebhookTrace) - self.assertEqual(result.status, 'success') - self.assertEqual(result.duration, 937) - self.assertEqual(result.result, '1') - self.assertIsInstance(result.executed_at, datetime) - - connection_mock.assert_called_once_with(x=1, y=2) - connection_mock.request.assert_called_once_with( - 'POST', - '/v1/instances/test/webhooks/name/run/', - data={'payload': '{"y": 2, "x": 1}'} - ) - - model = Webhook() - with self.assertRaises(SyncanoValidationError): - model.run() From ebf7f1401dfa4f7b8f6bdc7d9cf10c65daeae388 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 27 Jul 2015 11:22:29 +0200 Subject: [PATCH 063/558] simplify setting default instance --- syncano/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 7873f22..d43e41c 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -69,11 +69,9 @@ def connect(*args, **kwargs): from syncano.models import registry default_connection.open(*args, **kwargs) - instance = kwargs.get('instance_name') + instance = INSTANCE or kwargs.get('instance_name') - if INSTANCE: - registry.set_default_instance(INSTANCE) - elif instance: + if instance is not None: registry.set_default_instance(instance) return registry From d67daca00d70cf402a2a89533c06ae344950e807 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 27 Jul 2015 11:22:54 +0200 Subject: [PATCH 064/558] corrected docstring --- syncano/models/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 11f4a04..eff8014 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -152,7 +152,7 @@ def group_details(self, group_id): class Group(Model): """ - OO wrapper around users `endpoint `_. + OO wrapper around groups `endpoint `_. :ivar label: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` From e1d169942557edb70d397b52041a46aee5e658ac Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 27 Jul 2015 14:10:06 +0200 Subject: [PATCH 065/558] small fix --- syncano/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index d43e41c..84ca911 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -64,12 +64,14 @@ def connect(*args, **kwargs): # User login connection = syncano.connect(username='', password='', api_key='', instance_name='') + # OR + connection = syncano.connect(user_key='', api_key='', instance_name='') """ from syncano.connection import default_connection from syncano.models import registry default_connection.open(*args, **kwargs) - instance = INSTANCE or kwargs.get('instance_name') + instance = kwargs.get('instance_name', INSTANCE) if instance is not None: registry.set_default_instance(instance) @@ -113,7 +115,9 @@ def connect_instance(name=None, *args, **kwargs): # For User my_instance = syncano.connect_instance(username='', password='', api_key='', instance_name='') + # OR + my_instance = syncano.connect_instance(user_key='', api_key='', instance_name='') """ - name = name or kwargs.get('instance_name') or INSTANCE + name = name or kwargs.get('instance_name', INSTANCE) connection = connect(*args, **kwargs) return connection.Instance.please.get(name) From 8d36c7beea43f933b07196161a4ae155cd7b9406 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 27 Jul 2015 14:10:25 +0200 Subject: [PATCH 066/558] add integration tests for user login --- tests/integration_test_accounts.py | 68 ++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/integration_test_accounts.py diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py new file mode 100644 index 0000000..9e70bcd --- /dev/null +++ b/tests/integration_test_accounts.py @@ -0,0 +1,68 @@ +import os +import syncano +from integration_test import IntegrationTest + + +class LoginTest(IntegrationTest): + + @classmethod + def setUpClass(cls): + super(LoginTest, cls).setUpClass() + + cls.INSTANCE_NAME = os.getenv('INTEGRATION_INSTANCE_NAME') + cls.USER_NAME = os.getenv('INTEGRATION_USER_NAME') + cls.USER_PASSWORD = os.getenv('INTEGRATION_USER_PASSWORD') + cls.CLASS_NAME = cls.INSTANCE_NAME + + instance = cls.connection.Instance.please.create(name=cls.INSTANCE_NAME) + api_key = instance.api_keys.create(allow_user_create=True, + ignore_acl=True) + + user = instance.users.create(username=cls.USER_NAME, + password=cls.USER_PASSWORD) + + instance.classes.create(name=cls.CLASS_NAME, + schema='[{"name":"obj","type":"string"}]') + + cls.USER_KEY = user.user_key + cls.USER_API_KEY = api_key.api_key + cls.connection = None + + @classmethod + def tearDownClass(cls): + super(LoginTest, cls).setUpClass() + + cls.connection.Instance.please.delete(name=cls.INSTANCE_NAME) + cls.connection = None + + def check_connection(self, con): + obj_list = con.Class.please.list(instance_name=self.INSTANCE_NAME) + + self.assertEqual(len(list(obj_list)), 2) + self.assertItemsEqual([o.name for o in obj_list], ['user_profile', self.CLASS_NAME]) + + def test_admin_login(self): + con = syncano.connect(host=self.API_ROOT, + email=self.API_EMAIL, + password=self.API_PASSWORD) + con = self.check_connection(con) + + def test_admin_alt_login(self): + con = syncano.connect(host=self.API_ROOT, + api_key=self.API_KEY) + con = self.check_connection(con) + + def test_user_login(self): + con = syncano.connect(host=self.API_ROOT, + username=self.USER_NAME, + password=self.USER_PASSWORD, + user_key=self.USER_KEY, + instance_name=self.INSTANCE_NAME) + con = self.check_connection(con) + + def test_user_alt_login(self): + con = syncano.connect(host=self.API_ROOT, + api_key=self.USER_API_KEY, + user_key=self.USER_KEY, + instance_name=self.INSTANCE_NAME) + con = self.check_connection(con) From 3769132322b782c2f7986ba33f42644b56d6ffed Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 27 Jul 2015 17:50:19 +0200 Subject: [PATCH 067/558] correct connection testing --- tests/integration_test_accounts.py | 49 +++++++++++++++--------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py index 9e70bcd..7c5e792 100644 --- a/tests/integration_test_accounts.py +++ b/tests/integration_test_accounts.py @@ -1,5 +1,5 @@ import os -import syncano +from syncano.connection import Connection from integration_test import IntegrationTest @@ -26,43 +26,42 @@ def setUpClass(cls): cls.USER_KEY = user.user_key cls.USER_API_KEY = api_key.api_key - cls.connection = None @classmethod def tearDownClass(cls): - super(LoginTest, cls).setUpClass() - cls.connection.Instance.please.delete(name=cls.INSTANCE_NAME) cls.connection = None def check_connection(self, con): - obj_list = con.Class.please.list(instance_name=self.INSTANCE_NAME) + response = con.request('GET', '/v1/instances/test_login/classes/') + + obj_list = response['objects'] - self.assertEqual(len(list(obj_list)), 2) - self.assertItemsEqual([o.name for o in obj_list], ['user_profile', self.CLASS_NAME]) + self.assertEqual(len(obj_list), 2) + self.assertItemsEqual([o['name'] for o in obj_list], ['user_profile', self.CLASS_NAME]) def test_admin_login(self): - con = syncano.connect(host=self.API_ROOT, - email=self.API_EMAIL, - password=self.API_PASSWORD) - con = self.check_connection(con) + con = Connection(host=self.API_ROOT, + email=self.API_EMAIL, + password=self.API_PASSWORD) + self.check_connection(con) def test_admin_alt_login(self): - con = syncano.connect(host=self.API_ROOT, - api_key=self.API_KEY) - con = self.check_connection(con) + con = Connection(host=self.API_ROOT, + api_key=self.API_KEY) + self.check_connection(con) def test_user_login(self): - con = syncano.connect(host=self.API_ROOT, - username=self.USER_NAME, - password=self.USER_PASSWORD, - user_key=self.USER_KEY, - instance_name=self.INSTANCE_NAME) - con = self.check_connection(con) + con = Connection(host=self.API_ROOT, + username=self.USER_NAME, + password=self.USER_PASSWORD, + api_key=self.API_KEY, + instance_name=self.INSTANCE_NAME) + self.check_connection(con) def test_user_alt_login(self): - con = syncano.connect(host=self.API_ROOT, - api_key=self.USER_API_KEY, - user_key=self.USER_KEY, - instance_name=self.INSTANCE_NAME) - con = self.check_connection(con) + con = Connection(host=self.API_ROOT, + api_key=self.USER_API_KEY, + user_key=self.USER_KEY, + instance_name=self.INSTANCE_NAME) + self.check_connection(con) From c06896ab6010dd4ac6892904b2975fecfff3d7a6 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 28 Jul 2015 10:24:21 +0200 Subject: [PATCH 068/558] allow boolean to be null --- syncano/models/fields.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index afb5af7..49b5598 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -188,7 +188,7 @@ def to_python(self, value): value = super(IntegerField, self).to_python(value) if value is None: - return value + return try: return int(value) except (TypeError, ValueError): @@ -213,7 +213,7 @@ def to_python(self, value): value = super(FloatField, self).to_python(value) if value is None: - return value + return try: return float(value) except (TypeError, ValueError): @@ -225,13 +225,13 @@ class BooleanField(WritableField): def to_python(self, value): value = super(BooleanField, self).to_python(value) - if value in (True, False): - return bool(value) + if value is None: + return - if value in ('t', 'True', '1'): + if value in (True, 't', 'true', 'True', '1'): return True - if value in ('f', 'False', '0'): + if value in (False, 'f', 'false', 'False', '0'): return False raise self.ValidationError('Invalid value. Value should be a boolean.') @@ -289,7 +289,7 @@ def to_python(self, value): value = super(DateField, self).to_python(value) if value is None: - return value + return if isinstance(value, datetime): return value.date() @@ -328,7 +328,7 @@ class DateTimeField(DateField): def to_python(self, value): if value is None: - return value + return if isinstance(value, dict) and 'type' in value and 'value' in value: value = value['value'] @@ -374,7 +374,7 @@ def parse_from_date(self, value): def to_native(self, value): if value is None: - return value + return ret = value.isoformat() if ret.endswith('+00:00'): ret = ret[:-6] + 'Z' @@ -442,7 +442,7 @@ def validate(self, value, model_instance): def to_python(self, value): if value is None: - return value + return if isinstance(value, self.rel): return value @@ -454,7 +454,7 @@ def to_python(self, value): def to_native(self, value): if value is None: - return value + return if isinstance(value, self.rel): if not self.just_pk: @@ -492,7 +492,7 @@ def validate(self, value, model_instance): def to_python(self, value): if value is None: - return value + return if isinstance(value, six.string_types): value = json.loads(value) @@ -500,7 +500,7 @@ def to_python(self, value): def to_native(self, value): if value is None: - return value + return if not isinstance(value, six.string_types): value = json.dumps(value) From cd059594dd2b4fe69039658fa756a093ee1f988e Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 28 Jul 2015 10:53:56 +0200 Subject: [PATCH 069/558] raise 404 error with valuable message --- syncano/models/manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index e8e08c2..9a57a29 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -13,8 +13,8 @@ def clone(func): - """Decorator which will ensure that we are working on copy of ``self``.""" - + """Decorator which will ensure that we are working on copy of ``self``. + """ @wraps(func) def inner(self, *args, **kwargs): self = self._clone() @@ -489,7 +489,8 @@ def request(self, method=None, path=None, **request): response = self.connection.request(method, path, **request) except SyncanoRequestError as e: if e.status_code == 404: - raise self.model.DoesNotExist + obj_id = path.rsplit('/')[-2] + raise self.model.DoesNotExist("{} not found.".format(obj_id)) raise if 'next' not in response: From d35b5e98df847e40e2cbab053b7810e3f86a041a Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 28 Jul 2015 11:44:17 +0200 Subject: [PATCH 070/558] list traces for incentives --- syncano/models/incentives.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 6aacbc7..6be08e0 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -42,7 +42,7 @@ class CodeBox(Model): # This will cause name collision between model run method # and HyperlinkedField dynamic methods. # {'type': 'detail', 'name': 'run'}, - {'type': 'detail', 'name': 'traces'}, + {'type': 'list', 'name': 'traces'}, ) RUNTIME_CHOICES = ( {'display_name': 'nodejs', 'value': 'nodejs'}, @@ -121,8 +121,8 @@ class Schedule(Model): """ LINKS = [ {'type': 'detail', 'name': 'self'}, + {'type': 'detail', 'name': 'codebox'}, {'type': 'list', 'name': 'traces'}, - {'type': 'list', 'name': 'codebox'}, ] label = fields.StringField(max_length=80) @@ -164,7 +164,7 @@ class Trigger(Model): {'type': 'detail', 'name': 'self'}, {'type': 'detail', 'name': 'codebox'}, {'type': 'detail', 'name': 'class_name'}, - {'type': 'detail', 'name': 'traces'}, + {'type': 'list', 'name': 'traces'}, ) SIGNAL_CHOICES = ( {'display_name': 'post_update', 'value': 'post_update'}, @@ -220,6 +220,7 @@ class Webhook(Model): LINKS = ( {'type': 'detail', 'name': 'self'}, {'type': 'detail', 'name': 'codebox'}, + {'type': 'list', 'name': 'traces'}, ) name = fields.SlugField(max_length=50, primary_key=True) From 73875203a805331443dc45b487ccb3cb56019b71 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 28 Jul 2015 14:44:27 +0200 Subject: [PATCH 071/558] simplify setting property default value --- syncano/models/registry.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/syncano/models/registry.py b/syncano/models/registry.py index 05cfdc3..8b2de5a 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -64,14 +64,12 @@ def add(self, name, cls): def set_default_property(self, name, value): for model in self: - if name in model.__dict__: + if name not in model.__dict__: + continue - if name not in model.please.properties: - model.please.properties[name] = value - - for field in model._meta.fields: - if field.name == name: - field.default = value + for field in model._meta.fields: + if field.name == name: + field.default = value def set_default_instance(self, value): self.set_default_property('instance_name', value) From cd9e078ffa2958ab69f3ae69cf4cf561b1891266 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 28 Jul 2015 14:46:54 +0200 Subject: [PATCH 072/558] use default property values --- syncano/models/manager.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index e8e08c2..7d27776 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -473,7 +473,12 @@ def request(self, method=None, path=None, **request): meta = self.model._meta method = method or self.method allowed_methods = meta.get_endpoint_methods(self.endpoint) - path = path or meta.resolve_endpoint(self.endpoint, self.properties) + + if not path: + defaults = {f.name: f.default for f in self.model._meta.fields + if f.default is not None} + defaults.update(self.properties) + path = meta.resolve_endpoint(self.endpoint, defaults) if method.lower() not in allowed_methods: methods = ', '.join(allowed_methods) From fcc9379d9a33f7d3dfb4ffc68c75e87f13689875 Mon Sep 17 00:00:00 2001 From: Mariusz Wisniewski Date: Tue, 4 Aug 2015 16:42:44 -0400 Subject: [PATCH 073/558] Update readme v4 links to v1 (changed in our docs) removed v4 header - we're starting with v1 of syncano --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c09456..650410c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# Syncano v4.0 +# Syncano ## Python QuickStart Guide --- -You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/v4.0/docs/python). +You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/v1.0/docs/python). -For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/v4.0/docs/getting-stared-with-syncano) should be very helpful. +For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/v1.0/docs/getting-stared-with-syncano) should be very helpful. In case you need help working with the library - email us at libraries@syncano.com - we will be happy to help! From 6b3b14fff2043dbd03218716ec2fb2016754eafa Mon Sep 17 00:00:00 2001 From: Mariusz Wisniewski Date: Tue, 4 Aug 2015 16:46:02 -0400 Subject: [PATCH 074/558] Update README.md Links fixed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 650410c..3ddd757 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/v1.0/docs/python). -For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/v1.0/docs/getting-stared-with-syncano) should be very helpful. +For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/v1.0/docs/getting-started-with-syncano) should be very helpful. In case you need help working with the library - email us at libraries@syncano.com - we will be happy to help! From e1e4438e19b4ebabb9f3fe0ecdf446cb82bfcf58 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 11 Aug 2015 16:15:34 +0200 Subject: [PATCH 075/558] extract python reqs from circle config --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5409429 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +coverage>=3.7.1 +flake8==2.4.1 +isort==4.0.0 +mock>=1.0.1 From 64687df8a8b2233ed076ce661e896d7a16383ffc Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 11 Aug 2015 16:16:08 +0200 Subject: [PATCH 076/558] add isort test - extract tests from circle config --- run_tests.sh | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 run_tests.sh diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 0000000..1c92906 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +flake8 . +isort --recursive --check-only . + +coverage run -m unittest discover -p 'test*.py' +coverage html -d coverage/unittest +coverage run -m unittest discover -p 'integration_test*.py' +coverage html -d coverage/integration From ebbf24113229cc1231a45e6fa1c5a4c59fd884fd Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 11 Aug 2015 16:16:39 +0200 Subject: [PATCH 077/558] add autorelease after merge to master --- .pypirc.template | 14 ++++++++++++++ circle.yml | 14 +++++--------- release.sh | 5 +++++ 3 files changed, 24 insertions(+), 9 deletions(-) create mode 100644 .pypirc.template create mode 100644 release.sh diff --git a/.pypirc.template b/.pypirc.template new file mode 100644 index 0000000..b9800bf --- /dev/null +++ b/.pypirc.template @@ -0,0 +1,14 @@ +[distutils] # this tells distutils what package indexes you can push to +index-servers = + pypi + pypitest + +[pypi] +repository: https://pypi.python.org/pypi +username: {PYPI_USER} +password: {PYPI_PASSWORD} + +[pypitest] +repository: https://testpypi.python.org/pypi +username: {PYPI_USER} +password: {PYPI_PASSWORD} diff --git a/circle.yml b/circle.yml index b150713..8954b5d 100644 --- a/circle.yml +++ b/circle.yml @@ -4,17 +4,12 @@ machine: dependencies: pre: - - pip install coverage>=3.7.1 - - pip install mock>=1.0.1 - - pip install flake8 + pre: + - pip install -r requirements.txt test: override: - - flake8 . - - coverage run -m unittest discover -p 'test*.py' - - coverage html -d coverage/unittest - - coverage run -m unittest discover -p 'integration_test*.py' - - coverage html -d coverage/integration + - ./run_tests.sh general: artifacts: @@ -30,4 +25,5 @@ deployment: - pip install -r requirements-docs.txt - git config --global user.email "ci@circleci.com" - git config --global user.name "CircleCI" - - "cd docs && make gh-pages" \ No newline at end of file + - "cd docs && make gh-pages" + - ./release.sh diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..0682dbc --- /dev/null +++ b/release.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +sed "s//$PYPI_USER/;s//$PYPI_PASSWORD/" < ~/syncano-python/.pypirc.template > ~/.pypirc +python setup.py register -r pypi +python setup.py sdist upload -r pypi From 16f975318a7168beb0a7d9eb69a27fa257ed2836 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 11 Aug 2015 16:18:46 +0200 Subject: [PATCH 078/558] isort --- syncano/models/traces.py | 1 - tests/integration_test_accounts.py | 2 ++ tests/test_incentives.py | 1 - tests/test_manager.py | 17 ++++------------- 4 files changed, 6 insertions(+), 15 deletions(-) diff --git a/syncano/models/traces.py b/syncano/models/traces.py index b794796..78a3212 100644 --- a/syncano/models/traces.py +++ b/syncano/models/traces.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals - from . import fields from .base import Model from .incentives import CodeBox, Schedule, Trigger, Webhook diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py index 7c5e792..0a6ca69 100644 --- a/tests/integration_test_accounts.py +++ b/tests/integration_test_accounts.py @@ -1,5 +1,7 @@ import os + from syncano.connection import Connection + from integration_test import IntegrationTest diff --git a/tests/test_incentives.py b/tests/test_incentives.py index 084e2d7..7cfb39d 100644 --- a/tests/test_incentives.py +++ b/tests/test_incentives.py @@ -4,7 +4,6 @@ from syncano.exceptions import SyncanoValidationError from syncano.models import CodeBox, CodeBoxTrace, Webhook, WebhookTrace - try: from unittest import mock except ImportError: diff --git a/tests/test_manager.py b/tests/test_manager.py index 813e0e5..94e1d33 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1,19 +1,10 @@ import unittest from datetime import datetime -from syncano.exceptions import ( - SyncanoDoesNotExist, - SyncanoRequestError, - SyncanoValueError -) -from syncano.models import ( - CodeBox, - CodeBoxTrace, - Instance, - Object, - Webhook, - WebhookTrace -) +from syncano.exceptions import (SyncanoDoesNotExist, SyncanoRequestError, + SyncanoValueError) +from syncano.models import (CodeBox, CodeBoxTrace, Instance, Object, Webhook, + WebhookTrace) try: from unittest import mock From cd1d194d8097454c71de39114f820b3d92b65167 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 11 Aug 2015 16:19:41 +0200 Subject: [PATCH 079/558] remove unused --- .pypirc.template | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.pypirc.template b/.pypirc.template index b9800bf..527026a 100644 --- a/.pypirc.template +++ b/.pypirc.template @@ -1,14 +1,7 @@ [distutils] # this tells distutils what package indexes you can push to -index-servers = - pypi - pypitest +index-servers = pypi [pypi] repository: https://pypi.python.org/pypi -username: {PYPI_USER} -password: {PYPI_PASSWORD} - -[pypitest] -repository: https://testpypi.python.org/pypi -username: {PYPI_USER} -password: {PYPI_PASSWORD} +username: +password: From e226de4b2876140d444d56c4a0776e23e7cacac7 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 11 Aug 2015 16:20:54 +0200 Subject: [PATCH 080/558] make exec --- release.sh | 0 run_tests.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 release.sh mode change 100644 => 100755 run_tests.sh diff --git a/release.sh b/release.sh old mode 100644 new mode 100755 diff --git a/run_tests.sh b/run_tests.sh old mode 100644 new mode 100755 From cda3206cb5f79d4dbd57aea67494cf5a15e6206b Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 12 Aug 2015 17:40:15 +0200 Subject: [PATCH 081/558] add instance to connection kwargs --- syncano/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syncano/__init__.py b/syncano/__init__.py index 84ca911..66d69ff 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -119,5 +119,6 @@ def connect_instance(name=None, *args, **kwargs): my_instance = syncano.connect_instance(user_key='', api_key='', instance_name='') """ name = name or kwargs.get('instance_name', INSTANCE) + kwargs['instance_name'] = name connection = connect(*args, **kwargs) return connection.Instance.please.get(name) From d2dcfffd29f64f5f2a6d7137b0cb3b3e24f5f75c Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 17 Aug 2015 12:04:04 +0200 Subject: [PATCH 082/558] isort config added --- .isort.cfg | 5 +++++ tests/integration_test_accounts.py | 3 +-- tests/test_manager.py | 6 ++---- 3 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 .isort.cfg diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..106b4bc --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,5 @@ +[settings] +line_length=120 +multi_line_output=3 +default_section=THIRDPARTY +skip=base.py diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py index 0a6ca69..cd0ad7d 100644 --- a/tests/integration_test_accounts.py +++ b/tests/integration_test_accounts.py @@ -1,8 +1,7 @@ import os -from syncano.connection import Connection - from integration_test import IntegrationTest +from syncano.connection import Connection class LoginTest(IntegrationTest): diff --git a/tests/test_manager.py b/tests/test_manager.py index 94e1d33..4c07c5f 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1,10 +1,8 @@ import unittest from datetime import datetime -from syncano.exceptions import (SyncanoDoesNotExist, SyncanoRequestError, - SyncanoValueError) -from syncano.models import (CodeBox, CodeBoxTrace, Instance, Object, Webhook, - WebhookTrace) +from syncano.exceptions import SyncanoDoesNotExist, SyncanoRequestError, SyncanoValueError +from syncano.models import CodeBox, CodeBoxTrace, Instance, Object, Webhook, WebhookTrace try: from unittest import mock From 679ecde8b819ca29f3e902eceeaf4f99a8f71120 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 17 Aug 2015 12:04:23 +0200 Subject: [PATCH 083/558] backward compatibility for imports from base --- syncano/models/__init__.py | 8 -- syncano/models/archetypes.py | 229 +++++++++++++++++++++++++++++++++ syncano/models/base.py | 238 ++--------------------------------- 3 files changed, 238 insertions(+), 237 deletions(-) create mode 100644 syncano/models/archetypes.py diff --git a/syncano/models/__init__.py b/syncano/models/__init__.py index 2292e96..530d26c 100644 --- a/syncano/models/__init__.py +++ b/syncano/models/__init__.py @@ -1,9 +1 @@ from .base import * # NOQA -from .fields import * # NOQA -from .instances import * # NOQA -from .accounts import * # NOQA -from .billing import * # NOQA -from .channels import * # NOQA -from .classes import * # NOQA -from .incentives import * # NOQA -from .traces import * # NOQA diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py new file mode 100644 index 0000000..1654897 --- /dev/null +++ b/syncano/models/archetypes.py @@ -0,0 +1,229 @@ +from __future__ import unicode_literals + +import inspect + +import six +from syncano.exceptions import SyncanoDoesNotExist, SyncanoValidationError + +from . import fields +from .manager import Manager +from .options import Options +from .registry import registry + + +class ModelMetaclass(type): + """Metaclass for all models. + """ + def __new__(cls, name, bases, attrs): + super_new = super(ModelMetaclass, cls).__new__ + + parents = [b for b in bases if isinstance(b, ModelMetaclass)] + if not parents: + return super_new(cls, name, bases, attrs) + + module = attrs.pop('__module__', None) + new_class = super_new(cls, name, bases, {'__module__': module}) + + meta = attrs.pop('Meta', None) or getattr(new_class, 'Meta', None) + meta = Options(meta) + new_class.add_to_class('_meta', meta) + + manager = attrs.pop('please', Manager()) + new_class.add_to_class('please', manager) + + error_class = new_class.create_error_class() + new_class.add_to_class('DoesNotExist', error_class) + + for n, v in six.iteritems(attrs): + new_class.add_to_class(n, v) + + if not meta.pk: + pk_field = fields.IntegerField(primary_key=True, read_only=True, + required=False) + new_class.add_to_class('id', pk_field) + + for field_name in meta.endpoint_fields: + if field_name not in meta.field_names: + endpoint_field = fields.EndpointField() + new_class.add_to_class(field_name, endpoint_field) + + new_class.build_doc(name, meta) + registry.add(name, new_class) + return new_class + + def add_to_class(cls, name, value): + if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'): + value.contribute_to_class(cls, name) + else: + setattr(cls, name, value) + + def create_error_class(cls): + return type( + str('{0}DoesNotExist'.format(cls.__name__)), + (SyncanoDoesNotExist, ), + {} + ) + + def build_doc(cls, name, meta): + """Give the class a docstring if it's not defined. + """ + if cls.__doc__ is not None: + return + + field_names = ['{0} = {1}'.format(f.name, f.__class__.__name__) for f in meta.fields] + cls.__doc__ = '{0}:\n\t{1}'.format(name, '\n\t'.join(field_names)) + + +class Model(six.with_metaclass(ModelMetaclass)): + """Base class for all models. + """ + def __init__(self, **kwargs): + self._raw_data = {} + self.to_python(kwargs) + + def __repr__(self): + """Displays current instance class name and pk. + """ + return '<{0}: {1}>'.format( + self.__class__.__name__, + self.pk + ) + + def __str__(self): + """Wrapper around ```repr`` method. + """ + return repr(self) + + def __unicode__(self): + """Wrapper around ```repr`` method with proper encoding. + """ + return six.u(repr(self)) + + def __eq__(self, other): + if isinstance(other, Model): + return self.pk == other.pk + return NotImplemented + + def _get_connection(self, **kwargs): + connection = kwargs.pop('connection', None) + return connection or self._meta.connection + + def save(self, **kwargs): + """ + Creates or updates the current instance. + Override this in a subclass if you want to control the saving process. + """ + self.validate() + data = self.to_native() + connection = self._get_connection(**kwargs) + properties = self.get_endpoint_data() + endpoint_name = 'list' + method = 'POST' + + if not self.is_new(): + endpoint_name = 'detail' + methods = self._meta.get_endpoint_methods(endpoint_name) + if 'put' in methods: + method = 'PUT' + + endpoint = self._meta.resolve_endpoint(endpoint_name, properties) + request = {'data': data} + + response = connection.request(method, endpoint, **request) + + self.to_python(response) + return self + + def delete(self, **kwargs): + """Removes the current instance. + """ + if self.is_new(): + raise SyncanoValidationError('Method allowed only on existing model.') + + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('detail', properties) + connection = self._get_connection(**kwargs) + connection.request('DELETE', endpoint) + self._raw_data = {} + + def reload(self, **kwargs): + """Reloads the current instance. + """ + if self.is_new(): + raise SyncanoValidationError('Method allowed only on existing model.') + + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('detail', properties) + connection = self._get_connection(**kwargs) + response = connection.request('GET', endpoint) + self.to_python(response) + + def validate(self): + """ + Validates the current instance. + + :raises: SyncanoValidationError, SyncanoFieldError + """ + for field in self._meta.fields: + if not field.read_only: + value = getattr(self, field.name) + field.validate(value, self) + + def is_valid(self): + try: + self.validate() + except SyncanoValidationError: + return False + else: + return True + + def is_new(self): + if 'links' in self._meta.field_names: + return not self.links + + if self._meta.pk.read_only and not self.pk: + return True + + return False + + def to_python(self, data): + """ + Converts raw data to python types and built-in objects. + + :type data: dict + :param data: Raw data + """ + for field in self._meta.fields: + field_name = field.name + + if field.mapping is not None and self.pk: + field_name = field.mapping + + if field_name in data: + value = data[field_name] + setattr(self, field.name, value) + + def to_native(self): + """Converts the current instance to raw data which + can be serialized to JSON and send to API. + """ + data = {} + 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: + 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) + return data + + def get_endpoint_data(self): + properties = {} + for field in self._meta.fields: + if field.has_endpoint_data: + properties[field.name] = getattr(self, field.name) + return properties diff --git a/syncano/models/base.py b/syncano/models/base.py index 1654897..0c61566 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -1,229 +1,9 @@ -from __future__ import unicode_literals - -import inspect - -import six -from syncano.exceptions import SyncanoDoesNotExist, SyncanoValidationError - -from . import fields -from .manager import Manager -from .options import Options -from .registry import registry - - -class ModelMetaclass(type): - """Metaclass for all models. - """ - def __new__(cls, name, bases, attrs): - super_new = super(ModelMetaclass, cls).__new__ - - parents = [b for b in bases if isinstance(b, ModelMetaclass)] - if not parents: - return super_new(cls, name, bases, attrs) - - module = attrs.pop('__module__', None) - new_class = super_new(cls, name, bases, {'__module__': module}) - - meta = attrs.pop('Meta', None) or getattr(new_class, 'Meta', None) - meta = Options(meta) - new_class.add_to_class('_meta', meta) - - manager = attrs.pop('please', Manager()) - new_class.add_to_class('please', manager) - - error_class = new_class.create_error_class() - new_class.add_to_class('DoesNotExist', error_class) - - for n, v in six.iteritems(attrs): - new_class.add_to_class(n, v) - - if not meta.pk: - pk_field = fields.IntegerField(primary_key=True, read_only=True, - required=False) - new_class.add_to_class('id', pk_field) - - for field_name in meta.endpoint_fields: - if field_name not in meta.field_names: - endpoint_field = fields.EndpointField() - new_class.add_to_class(field_name, endpoint_field) - - new_class.build_doc(name, meta) - registry.add(name, new_class) - return new_class - - def add_to_class(cls, name, value): - if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'): - value.contribute_to_class(cls, name) - else: - setattr(cls, name, value) - - def create_error_class(cls): - return type( - str('{0}DoesNotExist'.format(cls.__name__)), - (SyncanoDoesNotExist, ), - {} - ) - - def build_doc(cls, name, meta): - """Give the class a docstring if it's not defined. - """ - if cls.__doc__ is not None: - return - - field_names = ['{0} = {1}'.format(f.name, f.__class__.__name__) for f in meta.fields] - cls.__doc__ = '{0}:\n\t{1}'.format(name, '\n\t'.join(field_names)) - - -class Model(six.with_metaclass(ModelMetaclass)): - """Base class for all models. - """ - def __init__(self, **kwargs): - self._raw_data = {} - self.to_python(kwargs) - - def __repr__(self): - """Displays current instance class name and pk. - """ - return '<{0}: {1}>'.format( - self.__class__.__name__, - self.pk - ) - - def __str__(self): - """Wrapper around ```repr`` method. - """ - return repr(self) - - def __unicode__(self): - """Wrapper around ```repr`` method with proper encoding. - """ - return six.u(repr(self)) - - def __eq__(self, other): - if isinstance(other, Model): - return self.pk == other.pk - return NotImplemented - - def _get_connection(self, **kwargs): - connection = kwargs.pop('connection', None) - return connection or self._meta.connection - - def save(self, **kwargs): - """ - Creates or updates the current instance. - Override this in a subclass if you want to control the saving process. - """ - self.validate() - data = self.to_native() - connection = self._get_connection(**kwargs) - properties = self.get_endpoint_data() - endpoint_name = 'list' - method = 'POST' - - if not self.is_new(): - endpoint_name = 'detail' - methods = self._meta.get_endpoint_methods(endpoint_name) - if 'put' in methods: - method = 'PUT' - - endpoint = self._meta.resolve_endpoint(endpoint_name, properties) - request = {'data': data} - - response = connection.request(method, endpoint, **request) - - self.to_python(response) - return self - - def delete(self, **kwargs): - """Removes the current instance. - """ - if self.is_new(): - raise SyncanoValidationError('Method allowed only on existing model.') - - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('detail', properties) - connection = self._get_connection(**kwargs) - connection.request('DELETE', endpoint) - self._raw_data = {} - - def reload(self, **kwargs): - """Reloads the current instance. - """ - if self.is_new(): - raise SyncanoValidationError('Method allowed only on existing model.') - - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('detail', properties) - connection = self._get_connection(**kwargs) - response = connection.request('GET', endpoint) - self.to_python(response) - - def validate(self): - """ - Validates the current instance. - - :raises: SyncanoValidationError, SyncanoFieldError - """ - for field in self._meta.fields: - if not field.read_only: - value = getattr(self, field.name) - field.validate(value, self) - - def is_valid(self): - try: - self.validate() - except SyncanoValidationError: - return False - else: - return True - - def is_new(self): - if 'links' in self._meta.field_names: - return not self.links - - if self._meta.pk.read_only and not self.pk: - return True - - return False - - def to_python(self, data): - """ - Converts raw data to python types and built-in objects. - - :type data: dict - :param data: Raw data - """ - for field in self._meta.fields: - field_name = field.name - - if field.mapping is not None and self.pk: - field_name = field.mapping - - if field_name in data: - value = data[field_name] - setattr(self, field.name, value) - - def to_native(self): - """Converts the current instance to raw data which - can be serialized to JSON and send to API. - """ - data = {} - 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: - 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) - return data - - def get_endpoint_data(self): - properties = {} - for field in self._meta.fields: - if field.has_endpoint_data: - properties[field.name] = getattr(self, field.name) - return properties +from .archetypes import * # NOQA +from .fields import * # NOQA +from .instances import * # NOQA +from .accounts import * # NOQA +from .billing import * # NOQA +from .channels import * # NOQA +from .classes import * # NOQA +from .incentives import * # NOQA +from .traces import * # NOQA From 54c3067bff6e31d3639f39d6f03c507291f8c3d0 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 17 Aug 2015 15:03:21 +0200 Subject: [PATCH 084/558] break if any fails --- run_tests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/run_tests.sh b/run_tests.sh index 1c92906..ddc6f6b 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -e + flake8 . isort --recursive --check-only . From e4b4343fa0af4edbf77c8bd8ba44d55d0f82586e Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 19 Aug 2015 16:00:49 +0200 Subject: [PATCH 085/558] corrected --- tests/test_connection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index fe539c6..3dd2178 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -55,7 +55,7 @@ def test_connect_instance(self, connect_mock): self.assertTrue(connect_mock.called) self.assertTrue(get_mock.called) - connect_mock.assert_called_once_with(a=1, b=2) + connect_mock.assert_called_once_with(a=1, b=2, instance_name='test-name') get_mock.assert_called_once_with('test-name') self.assertEqual(instance, get_mock) @@ -74,7 +74,7 @@ def test_env_connect_instance(self, instance_mock, connect_mock): self.assertTrue(connect_mock.called) self.assertTrue(get_mock.called) - connect_mock.assert_called_once_with(a=1, b=2) + connect_mock.assert_called_once_with(a=1, b=2, instance_name=instance_mock) get_mock.assert_called_once_with(instance_mock) self.assertEqual(instance, get_mock) From 16102a2e5e83fe11f494c704e74bfde8c401d177 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 25 Aug 2015 11:20:12 +0200 Subject: [PATCH 086/558] [SYNPYTH-143] allow adding user to group from User object --- syncano/models/accounts.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index eff8014..a2f6891 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -135,7 +135,7 @@ def reset_key(self): connection = self._get_connection() return connection.request('POST', endpoint) - def user_groups_method(self, group_id=None, method='GET'): + def _user_groups_method(self, group_id=None, method='GET'): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('groups', properties) if group_id is not None: @@ -143,11 +143,14 @@ def user_groups_method(self, group_id=None, method='GET'): connection = self._get_connection() return connection.request(method, endpoint) + def add_to_group(self, group_id): + return self._user_groups_method(group_id, method='POST') + def list_groups(self): - return self.user_groups_method() + return self._user_groups_method() def group_details(self, group_id): - return self.user_groups_method(group_id) + return self._user_groups_method(group_id) class Group(Model): @@ -188,7 +191,7 @@ class Meta: } } - def group_users_method(self, user_id=None, method='GET'): + def _group_users_method(self, user_id=None, method='GET'): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('users', properties) if user_id is not None: @@ -197,13 +200,13 @@ def group_users_method(self, user_id=None, method='GET'): return connection.request(method, endpoint) def list_users(self): - return self.group_users_method() + return self._group_users_method() def add_user(self, user_id): - return self.group_users_method(user_id, method='POST') + return self._group_users_method(user_id, method='POST') def user_details(self, user_id): - return self.group_users_method(user_id) + return self._group_users_method(user_id) def delete_user(self, user_id): - return self.group_users_method(user_id, method='DELETE') + return self._group_users_method(user_id, method='DELETE') From ddb6a842743deb4ed01fe0703111db23e2b1fac9 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 25 Aug 2015 11:24:44 +0200 Subject: [PATCH 087/558] [SYNPYTH-143] allow removing user from group from User object --- syncano/models/accounts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index a2f6891..116747e 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -152,6 +152,9 @@ def list_groups(self): def group_details(self, group_id): return self._user_groups_method(group_id) + def remove_from_group(self, group_id): + return self._user_groups_method(group_id, method='DELETE') + class Group(Model): """ From dd4bb37619689d2ee836665d820551eb534730c3 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 25 Aug 2015 13:21:03 +0200 Subject: [PATCH 088/558] [SYNPYTH-143] sort --- .gitignore | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index f8395e9..5cefc19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,15 @@ -*.pyc -.DS_STORE -build -dist *.egg-info +*.pyc *.pyo -.coverage -coverage -reports -run_it.py *.sublime-project *.sublime-workspace +.coverage +.DS_STORE .idea -junit build +coverage dist +junit +reports +run_it.py syncano.egg-info From d20d62acba343e7aefd4d8219bfea88dd71b00a6 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 25 Aug 2015 13:21:20 +0200 Subject: [PATCH 089/558] [SYNPYTH-143] increase version --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 66d69ff..c27defb 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.0' +__version__ = '4.0.1' __author__ = 'Daniel Kopka' __license__ = 'MIT' __copyright__ = 'Copyright 2015 Syncano' From 9869c80df282e75f8124b055dfcffbe55120adc2 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Wed, 26 Aug 2015 13:34:21 +0200 Subject: [PATCH 090/558] Dependency fix. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2c6ecf0..33cb4f3 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def readme(): 'requests==2.7.0', 'certifi', 'ndg-httpsclient==0.4.0', - 'pyasn1==0.1.7', + 'pyasn1==0.1.8', 'pyOpenSSL==0.15.1', 'python-slugify==0.1.0', 'six==1.9.0', From 61152eeb6a5dfe23dcf9a3f4f1dbf09b2dc86912 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Wed, 26 Aug 2015 14:08:40 +0200 Subject: [PATCH 091/558] Update README.md --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3ddd757..49a0af5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -# Syncano +Syncano +======= -## Python QuickStart Guide ---- +Python QuickStart Guide +----------------------- You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/v1.0/docs/python). @@ -11,8 +12,8 @@ In case you need help working with the library - email us at libraries@syncano.c You can also find library reference hosted on GitHub pages [here](http://syncano.github.io/syncano-python/). -## Backwards incompatible changes ---- +Backwards incompatible changes +------------------------------ Version 4.0 is designed for new release of Syncano platform and it's **not compatible** with any previous releases. @@ -20,6 +21,4 @@ it's **not compatible** with any previous releases. Code from `0.6.x` release is avalable on [stable/0.6.x](https://github.com/Syncano/syncano-python/tree/stable/0.6.x) branch and it can be installed directly from pip via: -``` -pip install syncano==0.6.2 --pre -``` +``pip install syncano==0.6.2 --pre`` From 77c8574b9922a1ae0b625c2dca943d0c529be902 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Wed, 26 Aug 2015 14:11:21 +0200 Subject: [PATCH 092/558] Readme update --- README.rst | 24 ++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 README.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..49a0af5 --- /dev/null +++ b/README.rst @@ -0,0 +1,24 @@ +Syncano +======= + +Python QuickStart Guide +----------------------- + +You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/v1.0/docs/python). + +For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/v1.0/docs/getting-started-with-syncano) should be very helpful. + +In case you need help working with the library - email us at libraries@syncano.com - we will be happy to help! + +You can also find library reference hosted on GitHub pages [here](http://syncano.github.io/syncano-python/). + +Backwards incompatible changes +------------------------------ + +Version 4.0 is designed for new release of Syncano platform and +it's **not compatible** with any previous releases. + +Code from `0.6.x` release is avalable on [stable/0.6.x](https://github.com/Syncano/syncano-python/tree/stable/0.6.x) branch +and it can be installed directly from pip via: + +``pip install syncano==0.6.2 --pre`` diff --git a/setup.py b/setup.py index 33cb4f3..12fc06c 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ def readme(): - with open('README.md') as f: + with open('README.rst') as f: return f.read() setup( From f6edefb5c2e9344643241c994636a47ae41c4fff Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Wed, 26 Aug 2015 14:12:05 +0200 Subject: [PATCH 093/558] delete old readme --- README.md | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index 49a0af5..0000000 --- a/README.md +++ /dev/null @@ -1,24 +0,0 @@ -Syncano -======= - -Python QuickStart Guide ------------------------ - -You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/v1.0/docs/python). - -For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/v1.0/docs/getting-started-with-syncano) should be very helpful. - -In case you need help working with the library - email us at libraries@syncano.com - we will be happy to help! - -You can also find library reference hosted on GitHub pages [here](http://syncano.github.io/syncano-python/). - -Backwards incompatible changes ------------------------------- - -Version 4.0 is designed for new release of Syncano platform and -it's **not compatible** with any previous releases. - -Code from `0.6.x` release is avalable on [stable/0.6.x](https://github.com/Syncano/syncano-python/tree/stable/0.6.x) branch -and it can be installed directly from pip via: - -``pip install syncano==0.6.2 --pre`` From 9977f8c95fc86a563574d0d141c8b6b79ff5aa9b Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Wed, 26 Aug 2015 14:44:26 +0200 Subject: [PATCH 094/558] package fixes --- README.rst => README | 0 setup.py | 5 ++--- syncano/__init__.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) rename README.rst => README (100%) diff --git a/README.rst b/README similarity index 100% rename from README.rst rename to README diff --git a/setup.py b/setup.py index 12fc06c..9ebd138 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ def readme(): - with open('README.rst') as f: + with open('README') as f: return f.read() setup( @@ -14,8 +14,7 @@ def readme(): author='Daniel Kopka', author_email='daniel.kopka@syncano.com', url='http://syncano.com', - packages=find_packages(), - test_suite='tests', + packages=find_packages(exclude=['tests']), zip_safe=False, classifiers=[ 'Development Status :: 4 - Beta', diff --git a/syncano/__init__.py b/syncano/__init__.py index c27defb..d9bb0e2 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.1' +__version__ = '4.0.2' __author__ = 'Daniel Kopka' __license__ = 'MIT' __copyright__ = 'Copyright 2015 Syncano' From b62c5ff1498b859c00d766b80dedeeeed187ae29 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Wed, 26 Aug 2015 14:49:08 +0200 Subject: [PATCH 095/558] added manifest --- MANIFEST.in | 1 + README => README.rst | 0 setup.py | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 MANIFEST.in rename README => README.rst (100%) diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..9561fb1 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.rst diff --git a/README b/README.rst similarity index 100% rename from README rename to README.rst diff --git a/setup.py b/setup.py index 9ebd138..a918009 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ def readme(): - with open('README') as f: + with open('README.rst') as f: return f.read() setup( From 3961b74cbb56c2976629c558543f1252b0434104 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 28 Aug 2015 14:53:06 +0200 Subject: [PATCH 096/558] [SYNPYTH-130] add fields and exclude methods for filtering objects --- syncano/models/manager.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index d011363..c19a152 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -645,6 +645,42 @@ def filter(self, **kwargs): self.endpoint = 'list' return self + @clone + def fields(self, *args): + """ + Special method just for data object :class:`~syncano.models.base.Object` model. + + Usage:: + + objects = Object.please.list('instance-name', 'class-name').fields('name', 'id') + """ + self.query['fields'] = ','.join(args) + self.method = 'GET' + self.endpoint = 'list' + return self + + @clone + def exclude(self, *args): + """ + Special method just for data object :class:`~syncano.models.base.Object` model. + + Usage:: + + objects = Object.please.list('instance-name', 'class-name').exclude('avatar') + """ + default_fields = ['id', 'created_at', 'updated_at', + 'revision', 'owner', 'group'] + + schema = self.model.get_class_schema(**self.properties) + fields = default_fields + [i['name'] for i in schema.schema] + + fields = [f for f in fields if f not in args] + + self.query['fields'] = ','.join(fields) + self.method = 'GET' + self.endpoint = 'list' + return self + class SchemaManager(object): """ From 757520ffc1230e17f3a3c288da818d50a93e266a Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Sun, 30 Aug 2015 21:22:20 +0200 Subject: [PATCH 097/558] [SYNPYTH-130] add field validation --- syncano/models/manager.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index c19a152..51d77c0 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -4,7 +4,7 @@ import six from syncano.connection import ConnectionMixin -from syncano.exceptions import SyncanoRequestError, SyncanoValueError +from syncano.exceptions import SyncanoRequestError, SyncanoValidationError, SyncanoValueError from .registry import registry @@ -645,6 +645,19 @@ def filter(self, **kwargs): self.endpoint = 'list' return self + def _get_model_field_names(self): + object_fields = [f.name for f in self.model._meta.fields] + schema = self.model.get_class_schema(**self.properties) + + return object_fields + [i['name'] for i in schema.schema] + + def _validate_fields(self, model_fields, args): + for arg in args: + if arg not in model_fields: + msg = 'Field "{0}" does not exist in class {1}.' + raise SyncanoValidationError( + msg.format(arg, self.properties['class_name'])) + @clone def fields(self, *args): """ @@ -654,6 +667,8 @@ def fields(self, *args): objects = Object.please.list('instance-name', 'class-name').fields('name', 'id') """ + model_fields = self._get_model_field_names() + self._validate_fields(model_fields, args) self.query['fields'] = ','.join(args) self.method = 'GET' self.endpoint = 'list' @@ -668,13 +683,10 @@ def exclude(self, *args): objects = Object.please.list('instance-name', 'class-name').exclude('avatar') """ - default_fields = ['id', 'created_at', 'updated_at', - 'revision', 'owner', 'group'] - - schema = self.model.get_class_schema(**self.properties) - fields = default_fields + [i['name'] for i in schema.schema] + model_fields = self._get_model_field_names() + self._validate_fields(model_fields, args) - fields = [f for f in fields if f not in args] + fields = [f for f in model_fields if f not in args] self.query['fields'] = ','.join(fields) self.method = 'GET' From 95dd706f7ab04ae17b4a151eb45375f5cff90ec7 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 2 Sep 2015 17:28:03 +0200 Subject: [PATCH 098/558] [SYNPYTH-152] set default properties demanded for query --- syncano/models/manager.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index d011363..4ffaaa1 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -126,8 +126,16 @@ def __getitem__(self, k): manager.limit(k + 1) return list(manager)[k] - # Object actions + def _set_default_properties(self, endpoint_properties): + for field in self.model._meta.fields: + + is_demanded = field.name in endpoint_properties + has_default = field.default is not None + if is_demanded and has_default: + self.properties[field.name] = field.default + + # Object actions def create(self, **kwargs): """ A convenience method for creating an object and saving it all in one step. Thus:: @@ -420,9 +428,11 @@ def contribute_to_class(self, model, name): # pragma: no cover self.name = name def _filter(self, *args, **kwargs): - if args and self.endpoint: - properties = self.model._meta.get_endpoint_properties(self.endpoint) + properties = self.model._meta.get_endpoint_properties(self.endpoint) + + self._set_default_properties(properties) + if args and self.endpoint: # let user get object by 'id' too_much_properties = len(args) < len(properties) id_specified = 'id' in properties From e7f62c4eeeeca53f3c152a3e504e275ebb4649e4 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Thu, 3 Sep 2015 14:59:51 +0200 Subject: [PATCH 099/558] [SYNPYTH-154] set owner to be writable --- syncano/models/accounts.py | 2 +- syncano/models/classes.py | 2 +- syncano/models/instances.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 116747e..bef962b 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -57,7 +57,7 @@ class Profile(Model): {'display_name': 'Create users', 'value': 'create_users'}, ) - owner = fields.IntegerField(label='owner id', required=False, read_only=True) + owner = fields.IntegerField(label='owner id', required=False, read_only=False) owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') group = fields.IntegerField(label='group id', required=False) group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 38b78cd..331d8e4 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -110,7 +110,7 @@ class Object(Model): created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) - owner = fields.IntegerField(label='owner id', required=False, read_only=True) + owner = fields.IntegerField(label='owner id', required=False, read_only=False) owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') group = fields.IntegerField(label='group id', required=False) group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 782f7d5..42f5bc7 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -35,7 +35,7 @@ class Instance(Model): name = fields.StringField(max_length=64, primary_key=True) description = fields.StringField(read_only=False, required=False) role = fields.Field(read_only=True, required=False) - owner = fields.ModelField('Admin', read_only=True) + owner = fields.ModelField('Admin', read_only=False) links = fields.HyperlinkedField(links=LINKS) metadata = fields.JSONField(read_only=False, required=False) created_at = fields.DateTimeField(read_only=True, required=False) From a8709baf44278624014035690c0addc5336ba56f Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Thu, 3 Sep 2015 17:09:34 +0200 Subject: [PATCH 100/558] [SYNPYTH-152] add checking set_default_properties --- tests/test_manager.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_manager.py b/tests/test_manager.py index 4c07c5f..2bd240f 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -236,6 +236,19 @@ def test_list(self, clone_mock, filter_mock): self.assertEqual(self.manager.method, 'GET') self.assertEqual(self.manager.endpoint, 'list') + @mock.patch('syncano.models.options.Options.get_endpoint_properties') + @mock.patch('syncano.models.manager.Manager._clone') + def test_set_default_properties(self, get_endpoint_mock, clone_mock): + get_endpoint_mock.return_value = ['a', 'b', 'name'] + clone_mock.return_value = self.manager + + instance_name = [f for f in self.model._meta.fields + if f.name == 'name'][0] + instance_name.default = 'test' + + self.manager._set_default_properties(get_endpoint_mock()) + self.assertDictEqual(self.manager.properties, {'name': 'test'}) + @mock.patch('syncano.models.manager.Manager.list') def test_first(self, list_mock): list_mock.__getitem__.return_value = 1 From 45a2534793209274f3beaa2eef574095061b995b Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Thu, 3 Sep 2015 17:18:10 +0200 Subject: [PATCH 101/558] [SYNPYTH-154] correct: leave only Class owner writable --- syncano/models/accounts.py | 2 +- syncano/models/classes.py | 2 +- syncano/models/instances.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index bef962b..116747e 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -57,7 +57,7 @@ class Profile(Model): {'display_name': 'Create users', 'value': 'create_users'}, ) - owner = fields.IntegerField(label='owner id', required=False, read_only=False) + owner = fields.IntegerField(label='owner id', required=False, read_only=True) owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') group = fields.IntegerField(label='group id', required=False) group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 331d8e4..798e353 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -110,7 +110,7 @@ class Object(Model): created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) - owner = fields.IntegerField(label='owner id', required=False, read_only=False) + owner = fields.IntegerField(label='owner id', required=False) owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') group = fields.IntegerField(label='group id', required=False) group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 42f5bc7..782f7d5 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -35,7 +35,7 @@ class Instance(Model): name = fields.StringField(max_length=64, primary_key=True) description = fields.StringField(read_only=False, required=False) role = fields.Field(read_only=True, required=False) - owner = fields.ModelField('Admin', read_only=False) + owner = fields.ModelField('Admin', read_only=True) links = fields.HyperlinkedField(links=LINKS) metadata = fields.JSONField(read_only=False, required=False) created_at = fields.DateTimeField(read_only=True, required=False) From b5caf11d9e9da040805294b98967ada1dafe99c0 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 10:00:07 +0200 Subject: [PATCH 102/558] [SYNPYTH-152] remove redundant pre section --- circle.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/circle.yml b/circle.yml index 8954b5d..f80b57e 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,6 @@ machine: version: 2.7.5 dependencies: - pre: pre: - pip install -r requirements.txt From c4b9cf8e6d6bf1fb0a9b8664ae50ff40b92c75b5 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 10:01:12 +0200 Subject: [PATCH 103/558] [SYNPYTH-152] build fix --- circle.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/circle.yml b/circle.yml index f80b57e..f34cd99 100644 --- a/circle.yml +++ b/circle.yml @@ -4,6 +4,7 @@ machine: dependencies: pre: + - pip install -U setuptools - pip install -r requirements.txt test: From 708222ed912790677fa694d7162ac093fc754f9b Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 10:05:39 +0200 Subject: [PATCH 104/558] [SYNPYTH-152] mock dep fix: add funcsigs --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 5409429..4cb6c19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ coverage>=3.7.1 flake8==2.4.1 +funcsigs==0.4 isort==4.0.0 mock>=1.0.1 From d7896f9871023276950f0b7b52da47f265eaae7e Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 10:49:48 +0200 Subject: [PATCH 105/558] [SYNPYTH-154] add important deps --- requirements.txt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5409429..066fe50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,15 @@ -coverage>=3.7.1 +Unidecode==0.4.18 +coverage==3.7.1 flake8==2.4.1 isort==4.0.0 -mock>=1.0.1 +mccabe==0.3.1 +mock==1.3.0 +nose==1.3.7 +pbr==1.6.0 +pep8==1.5.7 +pyflakes==0.8.1 +python-slugify==0.1.0 +requests==2.7.0 +six==1.9.0 +validictory==1.0.0 +wsgiref==0.1.2 From cd8512a229867c719f52c7f06ad8fb2ab74c1273 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 10:50:27 +0200 Subject: [PATCH 106/558] [SYNPYTH-154] install setuptools --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 8954b5d..f34cd99 100644 --- a/circle.yml +++ b/circle.yml @@ -4,7 +4,7 @@ machine: dependencies: pre: - pre: + - pip install -U setuptools - pip install -r requirements.txt test: From 865d46fb5fbcf3ae089ecac22a7fa2c340d95e40 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 10:52:22 +0200 Subject: [PATCH 107/558] [SYNPYTH-154] add funcsigs --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 066fe50..fbaf632 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ Unidecode==0.4.18 coverage==3.7.1 flake8==2.4.1 +funcsigs==0.4 isort==4.0.0 mccabe==0.3.1 mock==1.3.0 From 4d1a201d5f0ffdc24cb376662764ab902eb7dcfc Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 10:54:56 +0200 Subject: [PATCH 108/558] [SYNPYTH-152] add necessary deps --- requirements.txt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4cb6c19..fbaf632 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,16 @@ -coverage>=3.7.1 +Unidecode==0.4.18 +coverage==3.7.1 flake8==2.4.1 funcsigs==0.4 isort==4.0.0 -mock>=1.0.1 +mccabe==0.3.1 +mock==1.3.0 +nose==1.3.7 +pbr==1.6.0 +pep8==1.5.7 +pyflakes==0.8.1 +python-slugify==0.1.0 +requests==2.7.0 +six==1.9.0 +validictory==1.0.0 +wsgiref==0.1.2 From 1f2825c006ce4dd5794fd339c598524138dbd767 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 11:02:47 +0200 Subject: [PATCH 109/558] [SYNPYTH-152] add teardown --- tests/test_manager.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_manager.py b/tests/test_manager.py index 2bd240f..faa9817 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -28,6 +28,10 @@ def setUp(self): self.model = Instance self.manager = Instance.please + def tearDown(self): + self.model = None + self.manager = None + def test_create(self): model_mock = mock.MagicMock() model_mock.return_value = model_mock From ecc98bc8081f6ad5070bec27032eb193a406e13f Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 11:22:42 +0200 Subject: [PATCH 110/558] [SYNPYTH-152] coverage test strange issue fix: unset default field value --- tests/test_manager.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index faa9817..ac461f3 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -29,9 +29,20 @@ def setUp(self): self.manager = Instance.please def tearDown(self): + field_name = self.get_name_from_fields() + if field_name is not None: + field_name.default = None + self.model = None self.manager = None + def get_name_from_fields(self): + names = [f for f in self.model._meta.fields + if f.name == 'name'] + if len(names) > 0: + return names[0] + return + def test_create(self): model_mock = mock.MagicMock() model_mock.return_value = model_mock @@ -246,12 +257,12 @@ def test_set_default_properties(self, get_endpoint_mock, clone_mock): get_endpoint_mock.return_value = ['a', 'b', 'name'] clone_mock.return_value = self.manager - instance_name = [f for f in self.model._meta.fields - if f.name == 'name'][0] - instance_name.default = 'test' + instance_name = self.get_name_from_fields() + instance_name.default = 'test_original' self.manager._set_default_properties(get_endpoint_mock()) - self.assertDictEqual(self.manager.properties, {'name': 'test'}) + self.assertDictEqual(self.manager.properties, + {'name': 'test_original'}) @mock.patch('syncano.models.manager.Manager.list') def test_first(self, list_mock): From 6e6f0fdce2f05527f0df478db6bf974693ee15f0 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 4 Sep 2015 15:32:13 +0200 Subject: [PATCH 111/558] [SYNPYTH-157] SYNCORE-806 update --- syncano/models/incentives.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 6be08e0..4645a85 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -252,7 +252,7 @@ class Meta: }, 'public': { 'methods': ['get'], - 'path': '/webhooks/p/{public_link}/', + 'path': '/webhooks/p/{public_link}/{name}/', } } @@ -278,7 +278,8 @@ def run(self, **payload): } } response = connection.request('POST', endpoint, **request) - response.update({'instance_name': self.instance_name, 'webhook_name': self.name}) + response.update({'instance_name': self.instance_name, + 'webhook_name': self.name}) return WebhookTrace(**response) def reset(self, **payload): From 726e5c1679ad7f174ca4200452f9e85907bbe434 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 8 Sep 2015 16:58:14 +0200 Subject: [PATCH 112/558] [develop] upgrade version --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index d9bb0e2..a78fb20 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.2' +__version__ = '4.0.3' __author__ = 'Daniel Kopka' __license__ = 'MIT' __copyright__ = 'Copyright 2015 Syncano' From d3a389071f28749b7d5a5584398440e1a0998506 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 15 Sep 2015 16:22:24 +0200 Subject: [PATCH 113/558] [SYNPYTH-158] quickfix for serializing datetime field during update --- syncano/connection.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/syncano/connection.py b/syncano/connection.py index 7031e9d..4c377d1 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -232,6 +232,25 @@ def make_request(self, method_name, path, **kwargs): # Encode request payload if 'data' in params and not isinstance(params['data'], six.string_types): + # TODO(mk): quickfix - need to + from datetime import datetime + + def to_native(value): + if value is None: + return + ret = value.isoformat() + if ret.endswith('+00:00'): + ret = ret[:-6] + 'Z' + + if not ret.endswith('Z'): + ret = ret + 'Z' + + return ret + + for k, v in params['data'].iteritems(): + if isinstance(v, datetime): + params['data'][k] = to_native(v) + # params['data'] = json.dumps(params['data']) url = self.build_url(path) response = method(url, **params) From 4374875bcbf1f3cedb888dd261e62225c7852308 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 15 Sep 2015 16:29:56 +0200 Subject: [PATCH 114/558] [SYNPYTH-158] version update --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index a78fb20..79692b4 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.3' +__version__ = '4.0.4' __author__ = 'Daniel Kopka' __license__ = 'MIT' __copyright__ = 'Copyright 2015 Syncano' From 4ba25dbc8f94483da70cabdfb5bd5015e729ce89 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 15 Sep 2015 16:31:33 +0200 Subject: [PATCH 115/558] [develop] switch off flake for quickfix --- run_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_tests.sh b/run_tests.sh index ddc6f6b..b4207fd 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -2,7 +2,7 @@ set -e -flake8 . +#flake8 . isort --recursive --check-only . coverage run -m unittest discover -p 'test*.py' From 2c9aeb66d2d3d89d20cef2a3f259c65b471ca7f7 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 16 Sep 2015 11:28:46 +0200 Subject: [PATCH 116/558] [SYNPYTH-158] update fix - serialize fields before sending request --- syncano/connection.py | 19 ------------------- syncano/models/manager.py | 10 +++++++++- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 4c377d1..7031e9d 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -232,25 +232,6 @@ def make_request(self, method_name, path, **kwargs): # Encode request payload if 'data' in params and not isinstance(params['data'], six.string_types): - # TODO(mk): quickfix - need to - from datetime import datetime - - def to_native(value): - if value is None: - return - ret = value.isoformat() - if ret.endswith('+00:00'): - ret = ret[:-6] + 'Z' - - if not ret.endswith('Z'): - ret = ret + 'Z' - - return ret - - for k, v in params['data'].iteritems(): - if isinstance(v, datetime): - params['data'][k] = to_native(v) - # params['data'] = json.dumps(params['data']) url = self.build_url(path) response = method(url, **params) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index d0d2b96..38318d4 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -264,6 +264,14 @@ def update(self, *args, **kwargs): self.endpoint = 'detail' self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') self.data = kwargs.pop('data', kwargs) + + model = self.serialize(self.data, self.model) + serialized = model.to_native() + + serialized = {k: v for k, v in serialized.iteritems() + if k in self.data} + + self.data.update(serialized) self._filter(*args, **kwargs) return self.request() @@ -614,7 +622,7 @@ def create(self, **kwargs): return instance def serialize(self, data, model=None): - model = self.model.get_subclass_model(**self.properties) + model = model or self.model.get_subclass_model(**self.properties) return super(ObjectManager, self).serialize(data, model) @clone From 6eece316271d18c6ec483994c8e6db65823cce04 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 16 Sep 2015 11:29:21 +0200 Subject: [PATCH 117/558] [SYNPYTH-158] version update --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 79692b4..ac323e1 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.4' +__version__ = '4.0.5' __author__ = 'Daniel Kopka' __license__ = 'MIT' __copyright__ = 'Copyright 2015 Syncano' From 2a4e90fcd6d1fdd816cb13eb925dd2f7e7956e75 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 16 Sep 2015 11:31:16 +0200 Subject: [PATCH 118/558] [SYNPYTH-158] switch flake check on --- run_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run_tests.sh b/run_tests.sh index b4207fd..ddc6f6b 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -2,7 +2,7 @@ set -e -#flake8 . +flake8 . isort --recursive --check-only . coverage run -m unittest discover -p 'test*.py' From 3b1c7a0f2fc76251b08891ab28e2323aee368713 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 18 Sep 2015 16:24:49 +0200 Subject: [PATCH 119/558] [SYNPYTH-145] check Python version before importing urljoin --- syncano/connection.py | 9 ++++++++- syncano/models/options.py | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 7031e9d..a718ab3 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -1,12 +1,19 @@ import json +import sys from copy import deepcopy -from urlparse import urljoin import requests import six import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError + +if sys.version_info < (3,): + from urlparse import urljoin +else: + from urllib.parse import urljoin + + __all__ = ['default_connection', 'Connection', 'ConnectionMixin'] diff --git a/syncano/models/options.py b/syncano/models/options.py index 0239a8c..25c8123 100644 --- a/syncano/models/options.py +++ b/syncano/models/options.py @@ -1,6 +1,6 @@ import re +import sys from bisect import bisect -from urlparse import urljoin import six from syncano.connection import ConnectionMixin @@ -9,6 +9,12 @@ from syncano.utils import camelcase_to_underscore +if sys.version_info < (3,): + from urlparse import urljoin +else: + from urllib.parse import urljoin + + class Options(ConnectionMixin): """Holds metadata related to model definition.""" From aaba61eb2876087ed557edf3cd9fdc4f65eb8002 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 18 Sep 2015 16:33:07 +0200 Subject: [PATCH 120/558] [SYNPYTH-145] use six to check Python version --- syncano/connection.py | 7 +++---- syncano/models/options.py | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index a718ab3..c30c1aa 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -1,5 +1,4 @@ import json -import sys from copy import deepcopy import requests @@ -8,10 +7,10 @@ from syncano.exceptions import SyncanoRequestError, SyncanoValueError -if sys.version_info < (3,): - from urlparse import urljoin -else: +if six.PY3: from urllib.parse import urljoin +else: + from urlparse import urljoin __all__ = ['default_connection', 'Connection', 'ConnectionMixin'] diff --git a/syncano/models/options.py b/syncano/models/options.py index 25c8123..676fb65 100644 --- a/syncano/models/options.py +++ b/syncano/models/options.py @@ -1,5 +1,4 @@ import re -import sys from bisect import bisect import six @@ -9,10 +8,10 @@ from syncano.utils import camelcase_to_underscore -if sys.version_info < (3,): - from urlparse import urljoin -else: +if six.PY3: from urllib.parse import urljoin +else: + from urlparse import urljoin class Options(ConnectionMixin): From 9eaa8d694a7a45693935e09e0edb02002b4522b9 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 18 Sep 2015 16:45:04 +0200 Subject: [PATCH 121/558] [SYNPYTH-145] isort --- syncano/connection.py | 1 - syncano/models/options.py | 1 - 2 files changed, 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index c30c1aa..2af69fa 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -6,7 +6,6 @@ import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError - if six.PY3: from urllib.parse import urljoin else: diff --git a/syncano/models/options.py b/syncano/models/options.py index 676fb65..f008be3 100644 --- a/syncano/models/options.py +++ b/syncano/models/options.py @@ -7,7 +7,6 @@ from syncano.models.registry import registry from syncano.utils import camelcase_to_underscore - if six.PY3: from urllib.parse import urljoin else: From 4abe3bbb2341a4b9d2d27b934aeb682bf82aab62 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Thu, 24 Sep 2015 16:35:44 +0200 Subject: [PATCH 122/558] [SYNPYTH-159] if field not specified - set to None --- syncano/models/archetypes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 1654897..7366439 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -125,6 +125,12 @@ def save(self, **kwargs): methods = self._meta.get_endpoint_methods(endpoint_name) if 'put' in methods: method = 'PUT' + elif hasattr(self, 'get_class_schema'): + schema = self.get_class_schema(self.instance_name, + self.class_name) + for field in schema.schema: + if field['name'] not in data: + data[field['name']] = None endpoint = self._meta.resolve_endpoint(endpoint_name, properties) request = {'data': data} From 2463405bea03714d7b3d457ef55b41660333f945 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 28 Sep 2015 17:20:54 +0200 Subject: [PATCH 123/558] [SYNPYTH-161] remove fix after platform serializer default value for integer was removed --- syncano/models/archetypes.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 7366439..1654897 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -125,12 +125,6 @@ def save(self, **kwargs): methods = self._meta.get_endpoint_methods(endpoint_name) if 'put' in methods: method = 'PUT' - elif hasattr(self, 'get_class_schema'): - schema = self.get_class_schema(self.instance_name, - self.class_name) - for field in schema.schema: - if field['name'] not in data: - data[field['name']] = None endpoint = self._meta.resolve_endpoint(endpoint_name, properties) request = {'data': data} From 6f5e32ea49882c0bfc8a66bc20cf6fcce88b75c0 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 2 Oct 2015 14:56:40 +0200 Subject: [PATCH 124/558] [SYNPYTH-160] add data views --- syncano/models/base.py | 1 + syncano/models/data_views.py | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 syncano/models/data_views.py diff --git a/syncano/models/base.py b/syncano/models/base.py index 0c61566..334539e 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -5,5 +5,6 @@ from .billing import * # NOQA from .channels import * # NOQA from .classes import * # NOQA +from .data_views import * # NOQA from .incentives import * # NOQA from .traces import * # NOQA diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py new file mode 100644 index 0000000..635c1d2 --- /dev/null +++ b/syncano/models/data_views.py @@ -0,0 +1,62 @@ +from __future__ import unicode_literals + +from copy import deepcopy + +from syncano.exceptions import SyncanoValidationError +from syncano.utils import get_class_name + +from . import fields +from .base import Model +from .instances import Instance +from .manager import ObjectManager +from .registry import registry + + +class DataView(Model): + + LINKS = [ + {'type': 'detail', 'name': 'self'}, + {'type': 'list', 'name': 'objects'}, + ] + + PERMISSIONS_CHOICES = ( + {'display_name': 'None', 'value': 'none'}, + {'display_name': 'Read', 'value': 'read'}, + {'display_name': 'Create objects', 'value': 'create_objects'}, + ) + + name = fields.StringField(max_length=64, primary_key=True) + description = fields.StringField(required=False) + + query = fields.SchemaField(read_only=False, required=True) + + class_name = fields.StringField(label='class name', mapping='class') + + excluded_fields = fields.StringField(required=False) + expand = fields.StringField(required=False) + order_by = fields.StringField(required=False) + page_size = fields.IntegerField(required=False) + + links = fields.HyperlinkedField(links=LINKS) + + class Meta: + parent = Instance + plural_name = 'DataViews' + endpoints = { + 'detail': { + 'methods': ['get', 'put', 'patch', 'delete'], + 'path': '/api/objects/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/api/objects/', + }, + 'get_api': { + 'methods': ['get'], + 'path': '/api/objects/{name}/get_api', + }, + 'rename': { + 'methods': ['post'], + 'path': '/api/objects/{name}/rename', + } + } From 09b463745354d89fae9b1532cd29af88485e88ca Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 2 Oct 2015 14:58:52 +0200 Subject: [PATCH 125/558] [SYNPYTH-160] clean up --- syncano/models/data_views.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 635c1d2..a4fdb02 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -1,15 +1,8 @@ from __future__ import unicode_literals -from copy import deepcopy - -from syncano.exceptions import SyncanoValidationError -from syncano.utils import get_class_name - from . import fields from .base import Model from .instances import Instance -from .manager import ObjectManager -from .registry import registry class DataView(Model): @@ -22,7 +15,8 @@ class DataView(Model): PERMISSIONS_CHOICES = ( {'display_name': 'None', 'value': 'none'}, {'display_name': 'Read', 'value': 'read'}, - {'display_name': 'Create objects', 'value': 'create_objects'}, + {'display_name': 'Write', 'value': 'write'}, + {'display_name': 'Full', 'value': 'full'}, ) name = fields.StringField(max_length=64, primary_key=True) From 77981f6307461ad1ed30ccf6b255ba831dca4568 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 2 Oct 2015 19:32:13 +0200 Subject: [PATCH 126/558] [SYNPYTH-160] add rename and get_api --- syncano/models/data_views.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index a4fdb02..0668bdf 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -9,7 +9,7 @@ class DataView(Model): LINKS = [ {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'objects'}, + {'type': 'list', 'name': 'data_views'}, ] PERMISSIONS_CHOICES = ( @@ -47,10 +47,24 @@ class Meta: }, 'get_api': { 'methods': ['get'], - 'path': '/api/objects/{name}/get_api', + 'path': '/api/objects/{name}/get_api/', }, 'rename': { 'methods': ['post'], - 'path': '/api/objects/{name}/rename', + 'path': '/api/objects/{name}/rename/', } } + + def rename(self, new_name): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('rename', properties) + connection = self._get_connection() + return connection.request('POST', + endpoint, + data={'new_name': new_name}) + + def get_api(self): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('get_api', properties) + connection = self._get_connection() + return connection.request('GET', endpoint) From f3ce3a926616626db4495f6f1562d34a953147ae Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 5 Oct 2015 09:47:30 +0200 Subject: [PATCH 127/558] [SYNPYTH-160] decorator for api objects --- syncano/models/data_views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 0668bdf..0292e76 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -67,4 +67,8 @@ def get_api(self): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('get_api', properties) connection = self._get_connection() - return connection.request('GET', endpoint) + while endpoint is not None: + response = connection.request('GET', endpoint) + endpoint = response['next'] + for obj in response['objects']: + yield obj From bd510b4d57c307d4fe56c12b6b8cf2b74aee7a58 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 5 Oct 2015 11:52:02 +0200 Subject: [PATCH 128/558] [SYNPYTH-160] handle not found next, add docstring --- syncano/models/data_views.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 0292e76..d2f1460 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -6,6 +6,17 @@ class DataView(Model): + """ + :ivar name: :class:`~syncano.models.fields.StringField` + :ivar description: :class:`~syncano.models.fields.StringField` + :ivar query: :class:`~syncano.models.fields.SchemaField` + :ivar class_name: :class:`~syncano.models.fields.StringField` + :ivar excluded_fields: :class:`~syncano.models.fields.StringField` + :ivar expand: :class:`~syncano.models.fields.StringField` + :ivar order_by: :class:`~syncano.models.fields.StringField` + :ivar page_size: :class:`~syncano.models.fields.IntegerField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + """ LINKS = [ {'type': 'detail', 'name': 'self'}, @@ -69,6 +80,6 @@ def get_api(self): connection = self._get_connection() while endpoint is not None: response = connection.request('GET', endpoint) - endpoint = response['next'] + endpoint = response.get('next') for obj in response['objects']: yield obj From fe8347da76e3f5344dc93037989c70e715db66f3 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 5 Oct 2015 15:49:54 +0200 Subject: [PATCH 129/558] [SYNPYTH-160] add clear_cache --- syncano/models/data_views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index d2f1460..fc2c9b5 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -63,6 +63,10 @@ class Meta: 'rename': { 'methods': ['post'], 'path': '/api/objects/{name}/rename/', + }, + 'clear_cache': { + 'methods': ['post'], + 'path': '/api/objects/{name}/clear_cache/', } } @@ -74,6 +78,12 @@ def rename(self, new_name): endpoint, data={'new_name': new_name}) + def clear_cache(self): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('clear_cache', properties) + connection = self._get_connection() + return connection.request('POST', endpoint) + def get_api(self): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('get_api', properties) From f7b849dafd538ceba239ac2ad962742208106b4d Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 5 Oct 2015 16:09:04 +0200 Subject: [PATCH 130/558] [SYNPYTH-160] correct --- syncano/models/data_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index fc2c9b5..eabf3b0 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -33,7 +33,7 @@ class DataView(Model): name = fields.StringField(max_length=64, primary_key=True) description = fields.StringField(required=False) - query = fields.SchemaField(read_only=False, required=True) + query = fields.JSONField(read_only=False, required=True) class_name = fields.StringField(label='class name', mapping='class') From a11904c081cb9960d0801b366b9189a44f1d3948 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 6 Oct 2015 14:32:54 +0200 Subject: [PATCH 131/558] [SYNPYTH-160] SYNCORE-845 changes --- syncano/models/data_views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index eabf3b0..22bd543 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -56,9 +56,9 @@ class Meta: 'methods': ['post', 'get'], 'path': '/api/objects/', }, - 'get_api': { + 'get': { 'methods': ['get'], - 'path': '/api/objects/{name}/get_api/', + 'path': '/api/objects/{name}/get/', }, 'rename': { 'methods': ['post'], @@ -84,9 +84,9 @@ def clear_cache(self): connection = self._get_connection() return connection.request('POST', endpoint) - def get_api(self): + def get(self): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('get_api', properties) + endpoint = self._meta.resolve_endpoint('get', properties) connection = self._get_connection() while endpoint is not None: response = connection.request('GET', endpoint) From b4cf4b1499654ba7e86f7bad86bc0e4a92ebb2bc Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 9 Oct 2015 17:41:19 +0200 Subject: [PATCH 132/558] [CORE-144] allow admin to login with social backend --- syncano/__init__.py | 7 +++++ syncano/connection.py | 71 +++++++++++++++++++++++++++++++------------ 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index ac323e1..ebdddcb 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -122,3 +122,10 @@ def connect_instance(name=None, *args, **kwargs): kwargs['instance_name'] = name connection = connect(*args, **kwargs) return connection.Instance.please.get(name) + + +def social_connect(): + # curl -X POST \ + # -H "Authorization: token BACKEND_PROVIDER_TOKEN" \ + # -H "X-API-KEY: API_KEY" \ + # "https://api.syncano.io/v1/instances/instance/user/auth/backend_name/" diff --git a/syncano/connection.py b/syncano/connection.py index 2af69fa..1196892 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -68,12 +68,24 @@ class Connection(object): CONTENT_TYPE = 'application/json' AUTH_SUFFIX = 'v1/account/auth' + SOCIAL_AUTH_SUFFIX = AUTH_SUFFIX + '{social_backend}/' + USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' - ADMIN_LOGIN_PARAMS = {'email', 'password'} - ADMIN_ALT_LOGIN_PARAMS = {'api_key'} - USER_LOGIN_PARAMS = {'username', 'password', 'api_key', 'instance_name'} - USER_ALT_LOGIN_PARAMS = {'user_key', 'api_key', 'instance_name'} + LOGIN_PARAMS = {'email', + 'password'} + ALT_LOGIN_PARAMS = {'api_key'} + + USER_LOGIN_PARAMS = {'username', + 'password', + 'api_key', + 'instance_name'} + USER_ALT_LOGIN_PARAMS = {'user_key', + 'api_key', + 'instance_name'} + + SOCIAL_LOGIN_PARAMS = {'token', + 'social_backend'} def __init__(self, host=None, **kwargs): self.host = host or syncano.API_ROOT @@ -88,6 +100,8 @@ def __init__(self, host=None, **kwargs): self.AUTH_SUFFIX = self.USER_AUTH_SUFFIX.format(name=self.instance_name) self.auth_method = self.authenticate_user else: + if self.is_social: + self.AUTH_SUFFIX = self.SOCIAL_AUTH_SUFFIX.format(social_backend=self.social_backend) self.auth_method = self.authenticate_admin self.session = requests.Session() @@ -99,9 +113,11 @@ def _set_value_or_default(param): value = login_kwargs.get(param, getattr(syncano, param_lib_default_name, None)) setattr(self, param, value) - map(_set_value_or_default, self.ADMIN_LOGIN_PARAMS.union(self.ADMIN_ALT_LOGIN_PARAMS, - self.USER_LOGIN_PARAMS, - self.USER_ALT_LOGIN_PARAMS)) + map(_set_value_or_default, + self.LOGIN_PARAMS.union(self.ALT_LOGIN_PARAMS, + self.USER_LOGIN_PARAMS, + self.USER_ALT_LOGIN_PARAMS, + self.SOCIAL_LOGIN_PARAMS)) def _are_params_ok(self, params): return all(getattr(self, p) for p in params) @@ -112,11 +128,15 @@ def is_user(self): alt_login_params_ok = self._are_params_ok(self.USER_ALT_LOGIN_PARAMS) return login_params_ok or alt_login_params_ok + @property + def is_social(self): + return self._are_params_ok(self.SOCIAL_LOGIN_PARAMS) + @property def is_alt_login(self): if self.is_user: return self._are_params_ok(self.USER_ALT_LOGIN_PARAMS) - return self._are_params_ok(self.ADMIN_ALT_LOGIN_PARAMS) + return self._are_params_ok(self.ALT_LOGIN_PARAMS) @property def auth_key(self): @@ -146,7 +166,10 @@ def build_params(self, params): 'X-API-KEY': self.api_key }) elif self.api_key and 'Authorization' not in params['headers']: - params['headers']['Authorization'] = 'ApiKey %s' % self.api_key + if self.is_social: + params['headers']['Authorization'] = 'token %s' % self.token + else: + params['headers']['Authorization'] = 'ApiKey %s' % self.api_key # We don't need to check SSL cert in DEBUG mode if syncano.DEBUG or not self.verify_ssl: @@ -316,15 +339,8 @@ def authenticate(self, **kwargs): self.logger.debug(msg.format(key)) return key - def validate_params(self, kwargs): - map_login_params = { - (False, False): self.ADMIN_LOGIN_PARAMS, - (True, False): self.ADMIN_ALT_LOGIN_PARAMS, - (False, True): self.USER_LOGIN_PARAMS, - (True, True): self.USER_ALT_LOGIN_PARAMS - } - - for k in map_login_params[(self.is_alt_login, self.is_user)]: + def validate_params(self, kwargs, params): + for k in params: kwargs[k] = kwargs.get(k, getattr(self, k)) if kwargs[k] is None: @@ -332,13 +348,28 @@ def validate_params(self, kwargs): return kwargs def authenticate_admin(self, **kwargs): - request_args = self.validate_params(kwargs) + if self.is_alt_login: + request_args = self.validate_params(kwargs, + self.ALT_LOGIN_PARAMS) + else: + if self.is_social: + request_args = self.validate_params(kwargs, + self.SOCIAL_LOGIN_PARAMS) + else: + request_args = self.validate_params(kwargs, + self.LOGIN_PARAMS) + response = self.make_request('POST', self.AUTH_SUFFIX, data=request_args) self.api_key = response.get('account_key') return self.api_key def authenticate_user(self, **kwargs): - request_args = self.validate_params(kwargs) + if self.is_alt_login: + request_args = self.validate_params(kwargs, + self.USER_ALT_LOGIN_PARAMS) + else: + request_args = self.validate_params(kwargs, + self.USER_LOGIN_PARAMS) headers = { 'content-type': self.CONTENT_TYPE, 'X-API-KEY': request_args.pop('api_key') From 313904615c159e2c024fab14ccaf79296c01df2d Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 9 Oct 2015 17:46:39 +0200 Subject: [PATCH 133/558] [CORE-144] update docstring --- syncano/__init__.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index ebdddcb..8b713f7 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -45,7 +45,10 @@ def connect(*args, **kwargs): :param api_key: Your Syncano account key or instance api_key :type username: string - :param username: Your Syncano username + :param username: Instance user name + + :type user_key: string + :param user_key: Instance user key :type instance_name: string :param instance_name: Your Syncano instance_name @@ -61,6 +64,9 @@ def connect(*args, **kwargs): connection = syncano.connect(email='', password='') # OR connection = syncano.connect(api_key='') + # OR + connection = syncano.connect(social_backend='github', + token='sfdsdfsdf') # User login connection = syncano.connect(username='', password='', api_key='', instance_name='') @@ -122,10 +128,3 @@ def connect_instance(name=None, *args, **kwargs): kwargs['instance_name'] = name connection = connect(*args, **kwargs) return connection.Instance.please.get(name) - - -def social_connect(): - # curl -X POST \ - # -H "Authorization: token BACKEND_PROVIDER_TOKEN" \ - # -H "X-API-KEY: API_KEY" \ - # "https://api.syncano.io/v1/instances/instance/user/auth/backend_name/" From 9d5139939c6d9d8e6743c04062cb0b3e2bc873b9 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 12 Oct 2015 16:31:12 +0200 Subject: [PATCH 134/558] [CORE-155] add sane boolean value in manager --- syncano/models/manager.py | 2 +- tests/integration_test.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 38318d4..a40b421 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -100,7 +100,7 @@ def __iter__(self): # pragma: no cover return iter(self.iterator()) def __bool__(self): # pragma: no cover - return bool(self.iterator()) + return bool(list(self.iterator())) def __nonzero__(self): # pragma: no cover return type(self).__bool__(self) diff --git a/tests/integration_test.py b/tests/integration_test.py index 35afe6c..dbbda95 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -69,10 +69,14 @@ def test_create(self): name = 'i%s' % self.generate_hash()[:10] description = 'IntegrationTest' + self.assertFalse(bool(self.model.please.list())) + instance = self.model.please.create(name=name, description=description) self.assertIsNotNone(instance.pk) self.assertEqual(instance.name, name) self.assertEqual(instance.description, description) + + self.assertTrue(bool(self.model.please.list())) instance.delete() instance = self.model(name=name, description=description) From 0b7ebd6e0f70e59563fa40aa4ee599260da38439 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 13 Oct 2015 12:36:51 +0200 Subject: [PATCH 135/558] [CORE-155] more efficient bool --- syncano/models/manager.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index a40b421..212d604 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -100,7 +100,11 @@ def __iter__(self): # pragma: no cover return iter(self.iterator()) def __bool__(self): # pragma: no cover - return bool(list(self.iterator())) + try: + self[0] + return True + except IndexError: + return False def __nonzero__(self): # pragma: no cover return type(self).__bool__(self) From 4e4f4fa4cd9ab1a6b7b44b1c2339edd49b848a84 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 16 Oct 2015 17:38:37 +0200 Subject: [PATCH 136/558] [CORE-373] add ordering SYNCORE-717 --- syncano/models/manager.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 38318d4..0314b13 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -388,10 +388,25 @@ def limit(self, value): return self @clone - def order_by(self, field): + def ordering(self, order='asc'): """ Sets order of returned objects. + Usage:: + + instances = Instance.please.ordering() + """ + if order not in ('asc', 'desc'): + raise SyncanoValueError('Invalid order value.') + + self.query['ordering'] = order + return self + + @clone + def order_by(self, field): + """ + Sets ordering field of returned objects. + Usage:: instances = Instance.please.order_by('name') From 7371de60835e23a59c4f174d4257f16e70520e21 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Fri, 16 Oct 2015 17:44:26 +0200 Subject: [PATCH 137/558] [CORE-373] add implicit ordering in order_by --- syncano/models/manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 0314b13..5aa6b1d 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -403,7 +403,7 @@ def ordering(self, order='asc'): return self @clone - def order_by(self, field): + def order_by(self, field, order='asc'): """ Sets ordering field of returned objects. @@ -415,7 +415,7 @@ def order_by(self, field): raise SyncanoValueError('Order by field needs to be a string.') self.query['order_by'] = field - return self + return self.ordering(order) @clone def raw(self): From c3d994acfae00fd3479ad12749b73c4cf5c44146 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Sun, 18 Oct 2015 21:54:19 +0200 Subject: [PATCH 138/558] [CORE-373] move order_by to DO class - override ordering there --- syncano/models/manager.py | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 5aa6b1d..a6727d9 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -402,21 +402,6 @@ def ordering(self, order='asc'): self.query['ordering'] = order return self - @clone - def order_by(self, field, order='asc'): - """ - Sets ordering field of returned objects. - - Usage:: - - instances = Instance.please.order_by('name') - """ - if not field or not isinstance(field, six.string_types): - raise SyncanoValueError('Order by field needs to be a string.') - - self.query['order_by'] = field - return self.ordering(order) - @clone def raw(self): """ @@ -726,6 +711,28 @@ def exclude(self, *args): self.endpoint = 'list' return self + def ordering(self, order=None): + pass + + @clone + def order_by(self, field): + """ + Sets ordering field of returned objects. + + Usage:: + + # ASC order + instances = Object.please.order_by('name') + + # DESC order + instances = Object.please.order_by('-name') + """ + if not field or not isinstance(field, six.string_types): + raise SyncanoValueError('Order by field needs to be a string.') + + self.query['order_by'] = field + return self + class SchemaManager(object): """ From 6e3d18a28f65acd11dfe1a76c3d902703553fdb1 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Sun, 18 Oct 2015 21:58:42 +0200 Subject: [PATCH 139/558] [CORE-373] move order_by test to DO manager tests --- tests/test_manager.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index ac461f3..d3a7a3c 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -304,16 +304,6 @@ def test_limit(self, clone_mock): with self.assertRaises(SyncanoValueError): self.manager.limit('invalid value') - @mock.patch('syncano.models.manager.Manager._clone') - def test_order_by(self, clone_mock): - clone_mock.return_value = self.manager - - self.manager.order_by('field') - self.assertEqual(self.manager.query['order_by'], 'field') - - with self.assertRaises(SyncanoValueError): - self.manager.order_by(10) - @mock.patch('syncano.models.manager.Manager._clone') def test_raw(self, clone_mock): clone_mock.return_value = self.manager @@ -544,6 +534,16 @@ def test_filter(self, get_subclass_model_mock, clone_mock): with self.assertRaises(SyncanoValueError): self.manager.filter(name__xx=4) + @mock.patch('syncano.models.manager.Manager._clone') + def test_order_by(self, clone_mock): + clone_mock.return_value = self.manager + + self.manager.order_by('field') + self.assertEqual(self.manager.query['order_by'], 'field') + + with self.assertRaises(SyncanoValueError): + self.manager.order_by(10) + # TODO class SchemaManagerTestCase(unittest.TestCase): From f8298b339ab34747f110cda68365d5f1364f17b5 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 19 Oct 2015 10:06:56 +0200 Subject: [PATCH 140/558] [CORE-373] do not fail silently --- syncano/models/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index a6727d9..d6449a6 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -712,7 +712,7 @@ def exclude(self, *args): return self def ordering(self, order=None): - pass + raise AttributeError('Ordering not implemented. Use order_by instead.') @clone def order_by(self, field): From 4120c3fc45a4748ceac0576690719d677ed40ec9 Mon Sep 17 00:00:00 2001 From: Mariusz Wisniewski Date: Wed, 21 Oct 2015 22:04:29 -0400 Subject: [PATCH 141/558] Update permission choices for Profile --- syncano/models/accounts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 116747e..0e3c214 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -54,7 +54,8 @@ class Profile(Model): PERMISSIONS_CHOICES = ( {'display_name': 'None', 'value': 'none'}, {'display_name': 'Read', 'value': 'read'}, - {'display_name': 'Create users', 'value': 'create_users'}, + {'display_name': 'Write', 'value': 'write'}, + {'display_name': 'Full', 'value': 'full'}, ) owner = fields.IntegerField(label='owner id', required=False, read_only=True) From 628cc63358950540b169375c8318ae7648964401 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 3 Nov 2015 13:40:23 +0100 Subject: [PATCH 142/558] [LIB-195] fix the authorization header creation in build_params in Connection --- syncano/connection.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 1196892..4b4e2c2 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -166,10 +166,7 @@ def build_params(self, params): 'X-API-KEY': self.api_key }) elif self.api_key and 'Authorization' not in params['headers']: - if self.is_social: - params['headers']['Authorization'] = 'token %s' % self.token - else: - params['headers']['Authorization'] = 'ApiKey %s' % self.api_key + params['headers']['Authorization'] = 'token %s' % (self.token if self.is_social else self.api_key) # We don't need to check SSL cert in DEBUG mode if syncano.DEBUG or not self.verify_ssl: @@ -286,7 +283,6 @@ 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.') From 718294bb2d6e1d5f4b0c9ab4aebacc3c48ff2049 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 3 Nov 2015 13:43:32 +0100 Subject: [PATCH 143/558] LIB-195 correct test_build_params according to previous changes --- tests/test_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 3dd2178..7edfd9d 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -160,7 +160,7 @@ def test_build_params(self): self.assertTrue('headers' in params) self.assertTrue('Authorization' in params['headers']) - self.assertEqual(params['headers']['Authorization'], 'ApiKey {0}'.format(self.connection.api_key)) + self.assertEqual(params['headers']['Authorization'], 'token {0}'.format(self.connection.api_key)) self.assertTrue('content-type' in params['headers']) self.assertEqual(params['headers']['content-type'], self.connection.CONTENT_TYPE) From f93dd35a5d955328267435d7b7f381549cd9cbee Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 3 Nov 2015 13:56:10 +0100 Subject: [PATCH 144/558] [LIB-195] change percent string formatting to format --- syncano/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 4b4e2c2..5cfd51a 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -166,7 +166,7 @@ def build_params(self, params): 'X-API-KEY': self.api_key }) elif self.api_key and 'Authorization' not in params['headers']: - params['headers']['Authorization'] = 'token %s' % (self.token if self.is_social else self.api_key) + params['headers']['Authorization'] = 'token {}'.format(self.token if self.is_social else self.api_key) # We don't need to check SSL cert in DEBUG mode if syncano.DEBUG or not self.verify_ssl: From 0fd2154ffccf287a4472e75e77f04837c21e5f62 Mon Sep 17 00:00:00 2001 From: Daniel Kopka Date: Wed, 4 Nov 2015 08:48:36 +0100 Subject: [PATCH 145/558] version bump --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 8b713f7..f70e6ba 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.5' +__version__ = '4.0.6' __author__ = 'Daniel Kopka' __license__ = 'MIT' __copyright__ = 'Copyright 2015 Syncano' From 9f1ca845fe6d9b0f33ccdcf9b10b68bff8f8b39b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 4 Nov 2015 15:48:45 +0100 Subject: [PATCH 146/558] [LIB-187] Change the Profile class definition; add links to Object - not sure if it should be this way; --- syncano/models/accounts.py | 47 ++++++++++++++++---------------------- syncano/models/classes.py | 5 ++++ 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 0e3c214..4c0f69b 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -1,8 +1,11 @@ from __future__ import unicode_literals + from . import fields from .base import Model from .instances import Instance +from .classes import Class, Object +from .manager import ObjectManager class Admin(Model): @@ -44,45 +47,35 @@ class Meta: } -class Profile(Model): +class Profile(Object): """ """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - - PERMISSIONS_CHOICES = ( - {'display_name': 'None', 'value': 'none'}, - {'display_name': 'Read', 'value': 'read'}, - {'display_name': 'Write', 'value': 'write'}, - {'display_name': 'Full', 'value': 'full'}, - ) - - owner = fields.IntegerField(label='owner id', required=False, read_only=True) - owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - group = fields.IntegerField(label='group id', required=False) - group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - channel = fields.StringField(required=False) - channel_room = fields.StringField(required=False, max_length=64) - - links = fields.HyperlinkedField(links=LINKS) - created_at = fields.DateTimeField(read_only=True, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) class Meta: - parent = Instance + parent = Class endpoints = { 'detail': { - 'methods': ['delete', 'patch', 'put', 'get'], - 'path': '/user_profile/objects/{id}/', + 'methods': ['delete', 'post', 'patch', 'get'], + 'path': '/objects/{id}/', }, 'list': { 'methods': ['get'], - 'path': '/user_profile/objects/', + 'path': '/objects/', } } + @staticmethod + def __new__(cls, **kwargs): + kwargs.update( + { + 'instance_name': cls._meta.get_field('instance_name').default, + 'class_name': 'user_profile' + } + ) + return super(Profile, cls).__new__(cls, **kwargs) + + please = ObjectManager() + class User(Model): """ diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 798e353..0d92d74 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -106,6 +106,11 @@ class Object(Model): {'display_name': 'Full', 'value': 'full'}, ) + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + links = fields.HyperlinkedField(links=LINKS) + revision = fields.IntegerField(read_only=True, required=False) created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) From fdfd39d42bddec1bdafbc3c151ffcd69e5094789 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 4 Nov 2015 15:52:11 +0100 Subject: [PATCH 147/558] [LIB-187] correct import ordering --- syncano/models/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 4c0f69b..22c402c 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -3,8 +3,8 @@ from . import fields from .base import Model -from .instances import Instance from .classes import Class, Object +from .instances import Instance from .manager import ObjectManager From 02ed47afa447c8e0c413ec52ba31a5d8353f8089 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 4 Nov 2015 15:58:47 +0100 Subject: [PATCH 148/558] [LIB-187] correct import ordering --- syncano/models/accounts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 22c402c..5dd1159 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals - from . import fields from .base import Model from .classes import Class, Object From 65075f366de187bfbfe53a2a3fe2c562754c415d Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 5 Nov 2015 09:18:07 +0100 Subject: [PATCH 149/558] [LIB-187] user profile - another approach; Leave as it is, but correct the endpoint paths; --- syncano/models/accounts.py | 45 +++++++++++++++++++++++--------------- syncano/models/classes.py | 5 ----- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 5dd1159..59a492b 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -2,9 +2,7 @@ from . import fields from .base import Model -from .classes import Class, Object from .instances import Instance -from .manager import ObjectManager class Admin(Model): @@ -46,35 +44,46 @@ class Meta: } -class Profile(Object): +class Profile(Model): """ """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + PERMISSIONS_CHOICES = ( + {'display_name': 'None', 'value': 'none'}, + {'display_name': 'Read', 'value': 'read'}, + {'display_name': 'Write', 'value': 'write'}, + {'display_name': 'Full', 'value': 'full'}, + ) + + owner = fields.IntegerField(label='owner id', required=False, read_only=True) + owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + group = fields.IntegerField(label='group id', required=False) + group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + channel = fields.StringField(required=False) + channel_room = fields.StringField(required=False, max_length=64) + + links = fields.HyperlinkedField(links=LINKS) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + class Meta: - parent = Class + parent = Instance endpoints = { 'detail': { 'methods': ['delete', 'post', 'patch', 'get'], - 'path': '/objects/{id}/', + 'path': '/classes/user_profile/objects/{id}/', }, 'list': { 'methods': ['get'], - 'path': '/objects/', + 'path': '/classes/user_profile/objects/', } } - @staticmethod - def __new__(cls, **kwargs): - kwargs.update( - { - 'instance_name': cls._meta.get_field('instance_name').default, - 'class_name': 'user_profile' - } - ) - return super(Profile, cls).__new__(cls, **kwargs) - - please = ObjectManager() - class User(Model): """ diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 0d92d74..798e353 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -106,11 +106,6 @@ class Object(Model): {'display_name': 'Full', 'value': 'full'}, ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - links = fields.HyperlinkedField(links=LINKS) - revision = fields.IntegerField(read_only=True, required=False) created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) From b52b894e1e4e13fc9b2fb3a2196e0dbe3f4aafcc Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 9 Nov 2015 12:21:51 +0100 Subject: [PATCH 150/558] [LIB-186] remove default from the _permissions field in Object class; --- syncano/models/classes.py | 6 +++--- syncano/models/fields.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 798e353..7704faa 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -111,10 +111,10 @@ class Object(Model): updated_at = fields.DateTimeField(read_only=True, required=False) owner = fields.IntegerField(label='owner id', required=False) - owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, required=False) group = fields.IntegerField(label='group id', required=False) - group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') - other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, required=False) + other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, required=False) channel = fields.StringField(required=False) channel_room = fields.StringField(required=False, max_length=64) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 49b5598..6d993a4 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -276,7 +276,7 @@ def __init__(self, *args, **kwargs): def validate(self, value, model_instance): super(ChoiceField, self).validate(value, model_instance) - if self.choices and value not in self.allowed_values: + if self.choices and value and value not in self.allowed_values: raise self.ValidationError("Value '{0}' is not a valid choice.".format(value)) From 149d77a5ff96ffc941dbbc280001f90a83f32373 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 9 Nov 2015 13:14:05 +0100 Subject: [PATCH 151/558] [LIB-186] check if value is not None; --- syncano/models/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 6d993a4..6c9f2f6 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -276,7 +276,7 @@ def __init__(self, *args, **kwargs): def validate(self, value, model_instance): super(ChoiceField, self).validate(value, model_instance) - if self.choices and value and value not in self.allowed_values: + if self.choices and value is not None and value not in self.allowed_values: raise self.ValidationError("Value '{0}' is not a valid choice.".format(value)) From 0abfa34418a469c23d5b73b6001aed5765d6de19 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 9 Nov 2015 15:21:19 +0100 Subject: [PATCH 152/558] [LIB-201] first attempt to make proxy to the Profile class; --- syncano/models/accounts.py | 14 ++++++++++---- syncano/models/classes.py | 28 +++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 59a492b..b180aef 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -2,7 +2,9 @@ from . import fields from .base import Model +from .classes import Class, Object, DataObjectMixin from .instances import Instance +from .manager import ObjectManager class Admin(Model): @@ -44,10 +46,12 @@ class Meta: } -class Profile(Model): +class Profile(DataObjectMixin, Object): """ """ + PREDEFINED_CLASS_NAME = 'user_profile' + LINKS = ( {'type': 'detail', 'name': 'self'}, ) @@ -72,18 +76,20 @@ class Profile(Model): updated_at = fields.DateTimeField(read_only=True, required=False) class Meta: - parent = Instance + parent = Class endpoints = { 'detail': { 'methods': ['delete', 'post', 'patch', 'get'], - 'path': '/classes/user_profile/objects/{id}/', + 'path': '/objects/{id}/', }, 'list': { 'methods': ['get'], - 'path': '/classes/user_profile/objects/', + 'path': '/objects/', } } + please = ObjectManager() + class User(Model): """ diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 798e353..4f5c005 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -135,9 +135,8 @@ class Meta: @staticmethod def __new__(cls, **kwargs): - instance_name = kwargs.get('instance_name') - class_name = kwargs.get('class_name') - + instance_name = cls._get_instance_name(kwargs) + class_name = cls._get_class_name(kwargs) if not instance_name: raise SyncanoValidationError('Field "instance_name" is required.') @@ -145,8 +144,20 @@ def __new__(cls, **kwargs): raise SyncanoValidationError('Field "class_name" is required.') model = cls.get_subclass_model(instance_name, class_name) + + for field in model._meta.fields: + if field.has_endpoint_data and field.name == 'class_name': + setattr(model, field.name, getattr(cls, 'PREDEFINED_CLASS_NAME', None)) # TODO: think of refactor; return model(**kwargs) + @classmethod + def _get_instance_name(cls, kwargs): + return kwargs.get('instance_name') + + @classmethod + def _get_class_name(cls, kwargs): + return kwargs.get('class_name') + @classmethod def create_subclass(cls, name, schema): attrs = { @@ -202,3 +213,14 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): registry.add(model_name, model) return model + + +class DataObjectMixin(object): + + @classmethod + def _get_instance_name(cls, kwargs): + return cls.please.properties.get('instance_name') + + @classmethod + def _get_class_name(cls, kwargs): + return cls.PREDEFINED_CLASS_NAME From 859483bed9bb786600b9d3740547809d44a70f94 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 10 Nov 2015 10:24:09 +0100 Subject: [PATCH 153/558] [LIB-201] make Profile (user profile) fully Data Object; provide a method on this class: get_class which returns the class definition of user profile - lib user can now modify the schema easily; --- syncano/models/accounts.py | 2 +- syncano/models/classes.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index b180aef..f024f90 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -2,7 +2,7 @@ from . import fields from .base import Model -from .classes import Class, Object, DataObjectMixin +from .classes import Class, DataObjectMixin, Object from .instances import Instance from .manager import ObjectManager diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 4f5c005..ba2d126 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -144,12 +144,13 @@ def __new__(cls, **kwargs): raise SyncanoValidationError('Field "class_name" is required.') model = cls.get_subclass_model(instance_name, class_name) - - for field in model._meta.fields: - if field.has_endpoint_data and field.name == 'class_name': - setattr(model, field.name, getattr(cls, 'PREDEFINED_CLASS_NAME', None)) # TODO: think of refactor; + cls._set_up_object_class(model) return model(**kwargs) + @classmethod + def _set_up_object_class(cls, model): + pass + @classmethod def _get_instance_name(cls, kwargs): return kwargs.get('instance_name') @@ -224,3 +225,15 @@ def _get_instance_name(cls, kwargs): @classmethod def _get_class_name(cls, kwargs): return cls.PREDEFINED_CLASS_NAME + + @classmethod + def get_class(cls): + return Class.please.get(name=cls.PREDEFINED_CLASS_NAME) + + @classmethod + def _set_up_object_class(cls, model): + for field in model._meta.fields: + if field.has_endpoint_data and field.name == 'class_name': + if not getattr(model, field.name, None): + setattr(model, field.name, getattr(cls, 'PREDEFINED_CLASS_NAME', None)) + setattr(model, 'get_class', cls.get_class) From 301fc5492a8545d9871eeb41230be00a71422a32 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 10 Nov 2015 11:19:59 +0100 Subject: [PATCH 154/558] [LIB-201] correct one integration test; --- tests/integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index dbbda95..bb36642 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -69,7 +69,7 @@ def test_create(self): name = 'i%s' % self.generate_hash()[:10] description = 'IntegrationTest' - self.assertFalse(bool(self.model.please.list())) + self.assertEqual(len(self.model.please.list()), 1) # auto create first instance; instance = self.model.please.create(name=name, description=description) self.assertIsNotNone(instance.pk) From f7f454792b597199224d36f5eac2241b97b0c3a1 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 10 Nov 2015 11:28:46 +0100 Subject: [PATCH 155/558] [LIB-201] correct integration tests; --- syncano/models/classes.py | 2 +- tests/integration_test.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index ba2d126..7bd75f5 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -220,7 +220,7 @@ class DataObjectMixin(object): @classmethod def _get_instance_name(cls, kwargs): - return cls.please.properties.get('instance_name') + return cls.please.properties.get('instance_name') or kwargs.get('instance_name') @classmethod def _get_class_name(cls, kwargs): diff --git a/tests/integration_test.py b/tests/integration_test.py index bb36642..b4f9c2c 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -69,7 +69,7 @@ def test_create(self): name = 'i%s' % self.generate_hash()[:10] description = 'IntegrationTest' - self.assertEqual(len(self.model.please.list()), 1) # auto create first instance; + self.assertEqual(len(list(self.model.please.list())), 1) # auto create first instance; instance = self.model.please.create(name=name, description=description) self.assertIsNotNone(instance.pk) From e13bc1f5d7308ea576c8ddd3a97c104038c4faf5 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 10 Nov 2015 12:16:28 +0100 Subject: [PATCH 156/558] [LIB-201] rename get_class method to get_class_object; move setup class before saving to the registry; --- syncano/models/classes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 7bd75f5..0342636 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -144,7 +144,6 @@ def __new__(cls, **kwargs): raise SyncanoValidationError('Field "class_name" is required.') model = cls.get_subclass_model(instance_name, class_name) - cls._set_up_object_class(model) return model(**kwargs) @classmethod @@ -211,6 +210,7 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): except LookupError: schema = cls.get_class_schema(instance_name, class_name) model = cls.create_subclass(model_name, schema) + cls._set_up_object_class(model) registry.add(model_name, model) return model @@ -227,7 +227,7 @@ def _get_class_name(cls, kwargs): return cls.PREDEFINED_CLASS_NAME @classmethod - def get_class(cls): + def get_class_object(cls): return Class.please.get(name=cls.PREDEFINED_CLASS_NAME) @classmethod @@ -236,4 +236,4 @@ def _set_up_object_class(cls, model): if field.has_endpoint_data and field.name == 'class_name': if not getattr(model, field.name, None): setattr(model, field.name, getattr(cls, 'PREDEFINED_CLASS_NAME', None)) - setattr(model, 'get_class', cls.get_class) + setattr(model, 'get_class_object', cls.get_class_object) From 0e411c042769350c714d0869f85f339b23b471f1 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 10 Nov 2015 12:23:34 +0100 Subject: [PATCH 157/558] [LIB-201] move setup class just after model creation; --- syncano/models/classes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 0342636..d977acf 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -171,8 +171,9 @@ def create_subclass(cls, name, schema): query_allowed = ('order_index' in field or 'filter_index' in field) attrs[field['name']] = field_class(required=False, read_only=False, query_allowed=query_allowed) - - return type(str(name), (Object, ), attrs) + model = type(str(name), (Object, ), attrs) + cls._set_up_object_class(model) + return model @classmethod def get_or_create_subclass(cls, name, schema): @@ -210,7 +211,6 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): except LookupError: schema = cls.get_class_schema(instance_name, class_name) model = cls.create_subclass(model_name, schema) - cls._set_up_object_class(model) registry.add(model_name, model) return model From 32b3e91a50c97b0e891c112b690db224698930fd Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 10 Nov 2015 12:31:38 +0100 Subject: [PATCH 158/558] [LIB-201] correct class setup; --- syncano/models/classes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index d977acf..fafedf3 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -237,3 +237,5 @@ def _set_up_object_class(cls, model): if not getattr(model, field.name, None): setattr(model, field.name, getattr(cls, 'PREDEFINED_CLASS_NAME', None)) setattr(model, 'get_class_object', cls.get_class_object) + setattr(model, '_get_instance_name', cls._get_instance_name) + setattr(model, '_get_class_name', cls._get_class_name) From d6d89972f663c96c9a0b6767878d0910603ed672 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 12 Nov 2015 12:07:08 +0100 Subject: [PATCH 159/558] [LIB-201] correct RelatedManager for better handling the multiple instances; --- syncano/models/manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index cb06fbc..91527b4 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -58,6 +58,8 @@ def __get__(self, instance, owner=None): method = getattr(Model.please, self.endpoint, Model.please.all) properties = instance._meta.get_endpoint_properties('detail') + if 'name' in properties: # instance name here; + registry.set_default_instance(getattr(instance, 'name')) # update the registry with last used instance; properties = [getattr(instance, prop) for prop in properties] return method(*properties) From f5351d68c3a9f9d62175b767eaba177010f9b176 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 12 Nov 2015 12:44:16 +0100 Subject: [PATCH 160/558] [LIB-201] trying to resolve good flow of instance_name between queries... --- syncano/models/manager.py | 6 ++++-- syncano/models/registry.py | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 91527b4..962b83a 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -3,6 +3,7 @@ from functools import wraps import six + from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValidationError, SyncanoValueError @@ -58,8 +59,7 @@ def __get__(self, instance, owner=None): method = getattr(Model.please, self.endpoint, Model.please.all) properties = instance._meta.get_endpoint_properties('detail') - if 'name' in properties: # instance name here; - registry.set_default_instance(getattr(instance, 'name')) # update the registry with last used instance; + registry.set_last_used_instance(getattr(instance, 'name', None)) properties = [getattr(instance, prop) for prop in properties] return method(*properties) @@ -158,6 +158,8 @@ def create(self, **kwargs): attrs = kwargs.copy() attrs.update(self.properties) instance = self.model(**attrs) + if instance.__class__.__name__ == 'Instance': # avoid circular import; + registry.set_last_used_instance(instance.name) instance.save() return instance diff --git a/syncano/models/registry.py b/syncano/models/registry.py index 8b2de5a..4f4f5a0 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -13,6 +13,7 @@ def __init__(self, models=None): self.models = models or {} self.patterns = [] self._pending_lookups = {} + self.last_used_instance = None def __str__(self): return 'Registry: {0}'.format(', '.join(self.models)) @@ -74,5 +75,10 @@ def set_default_property(self, name, value): def set_default_instance(self, value): self.set_default_property('instance_name', value) + def set_last_used_instance(self, instance): + if instance and self.last_used_instance != instance or registry.last_used_instance is None: + self.set_default_instance(instance) # update the registry with last used instance; + self.last_used_instance = instance + registry = Registry() From 32f3858b2921b95967595d534b83e5e9b788f441 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 12 Nov 2015 12:48:09 +0100 Subject: [PATCH 161/558] [LIB-201] correct imports... --- syncano/models/manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 962b83a..7969ffc 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -3,7 +3,6 @@ from functools import wraps import six - from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValidationError, SyncanoValueError From 10a98a7f0e67e791070f3ff6b2bc879f91f3f2f2 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 12 Nov 2015 14:19:03 +0100 Subject: [PATCH 162/558] [LIB-201] Implement method cleaning the register, when instance deleted; --- syncano/models/archetypes.py | 2 ++ syncano/models/manager.py | 2 -- syncano/models/registry.py | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 1654897..a720dac 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -144,6 +144,8 @@ def delete(self, **kwargs): endpoint = self._meta.resolve_endpoint('detail', properties) connection = self._get_connection(**kwargs) connection.request('DELETE', endpoint) + if self.__class__.__name__ == 'Instance': # avoid circular import; + registry.clear_instance_name() self._raw_data = {} def reload(self, **kwargs): diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 7969ffc..d763167 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -157,8 +157,6 @@ def create(self, **kwargs): attrs = kwargs.copy() attrs.update(self.properties) instance = self.model(**attrs) - if instance.__class__.__name__ == 'Instance': # avoid circular import; - registry.set_last_used_instance(instance.name) instance.save() return instance diff --git a/syncano/models/registry.py b/syncano/models/registry.py index 4f4f5a0..8e2199b 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -80,5 +80,7 @@ def set_last_used_instance(self, instance): self.set_default_instance(instance) # update the registry with last used instance; self.last_used_instance = instance + def clear_instance_name(self): + self.set_default_instance(None) registry = Registry() From e64b03e09e9db7866c71c58218995d692a58f702 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 12 Nov 2015 14:35:12 +0100 Subject: [PATCH 163/558] [LIB-201] restore old test code; --- tests/integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index b4f9c2c..dbbda95 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -69,7 +69,7 @@ def test_create(self): name = 'i%s' % self.generate_hash()[:10] description = 'IntegrationTest' - self.assertEqual(len(list(self.model.please.list())), 1) # auto create first instance; + self.assertFalse(bool(self.model.please.list())) instance = self.model.please.create(name=name, description=description) self.assertIsNotNone(instance.pk) From 6ab3c381af0d0398d14f39fba57b523e7c099d83 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 12 Nov 2015 15:53:02 +0100 Subject: [PATCH 164/558] [LIB-201] correct registry - add update method; correct Object - now refresh the registry when schema changed; add test; --- syncano/models/classes.py | 16 +++++++++- syncano/models/registry.py | 21 +++++++------ tests/integration_test_user_profile.py | 42 ++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 tests/integration_test_user_profile.py diff --git a/syncano/models/classes.py b/syncano/models/classes.py index fafedf3..5d99c15 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -182,7 +182,6 @@ def get_or_create_subclass(cls, name, schema): except LookupError: subclass = cls.create_subclass(name, schema) registry.add(name, subclass) - return subclass @classmethod @@ -213,6 +212,21 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): model = cls.create_subclass(model_name, schema) registry.add(model_name, model) + schema = cls.get_class_schema(instance_name, class_name) + + schema_changed = False + for field in schema: + try: + getattr(model, field['name']) + except AttributeError: + # schema changed, update the registry; + schema_changed = True + + if schema_changed: + schema = cls.get_class_schema(instance_name, class_name) + model = cls.create_subclass(model_name, schema) + registry.update(model_name, model) + return model diff --git a/syncano/models/registry.py b/syncano/models/registry.py index 8e2199b..b04a212 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -43,18 +43,21 @@ def get_model_by_path(self, path): def get_model_by_name(self, name): return self.models[name] - def add(self, name, cls): + def update(self, name, cls): + self.models[name] = cls + related_name = cls._meta.related_name + patterns = self.get_model_patterns(cls) + self.patterns.extend(patterns) - if name not in self.models: - self.models[name] = cls - related_name = cls._meta.related_name - patterns = self.get_model_patterns(cls) - self.patterns.extend(patterns) + setattr(self, str(name), cls) + setattr(self, str(related_name), cls.please.all()) - setattr(self, str(name), cls) - setattr(self, str(related_name), cls.please.all()) + logger.debug('New model: %s, %s', name, related_name) - logger.debug('New model: %s, %s', name, related_name) + def add(self, name, cls): + + if name not in self.models: + self.update(name, cls) if name in self._pending_lookups: lookups = self._pending_lookups.pop(name) diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user_profile.py new file mode 100644 index 0000000..2ce6c38 --- /dev/null +++ b/tests/integration_test_user_profile.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +from syncano.models import User +from tests.integration_test import InstanceMixin, IntegrationTest + + +class UserProfileTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(UserProfileTest, cls).setUpClass() + cls.user = cls.instance.users.create( + username='JozinZBazin', + password='jezioro', + ) + cls.SAMPLE_PROFILE_PIC = 'some_url_here' + + def test_profile(self): + self.assertTrue(self.user.profile) + self.assertEqual( + self.user.profile.__class__.__name__, + '{}UserProfileObject'.format(self.instance.name.title()) + ) + + def test_profile_klass(self): + klass = self.user.profile.get_class_object() + self.assertTrue(klass) + self.assertEqual(klass.instance_name, self.instance.name) + + def test_profile_change_schema(self): + klass = self.user.profile.get_class_object() + klass.schema = [ + {'name': 'profile_pic', 'type': 'string'} + ] + + klass.save() + self.user.reload() # force to refresh profile model; + + self.user.profile.profile_pic = self.SAMPLE_PROFILE_PIC + self.user.profile.save() + user = User.please.get(id=self.user.id) + user.reload() + self.assertEqual(user.profile.profile_pic, self.SAMPLE_PROFILE_PIC) From f1241c5680bc1be56977e234fbc600c1746f818c Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 13 Nov 2015 13:50:41 +0100 Subject: [PATCH 165/558] [LIB-201] small code fixes; --- syncano/models/classes.py | 7 +++---- syncano/models/registry.py | 1 + tests/integration_test_user_profile.py | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 5d99c15..42d1d6d 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -205,15 +205,14 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): if cls.__name__ == model_name: return cls + schema = cls.get_class_schema(instance_name, class_name) + try: model = registry.get_model_by_name(model_name) except LookupError: - schema = cls.get_class_schema(instance_name, class_name) model = cls.create_subclass(model_name, schema) registry.add(model_name, model) - schema = cls.get_class_schema(instance_name, class_name) - schema_changed = False for field in schema: try: @@ -221,9 +220,9 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): except AttributeError: # schema changed, update the registry; schema_changed = True + break if schema_changed: - schema = cls.get_class_schema(instance_name, class_name) model = cls.create_subclass(model_name, schema) registry.update(model_name, model) diff --git a/syncano/models/registry.py b/syncano/models/registry.py index b04a212..805b4b3 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -84,6 +84,7 @@ def set_last_used_instance(self, instance): self.last_used_instance = instance def clear_instance_name(self): + self.last_used_instance = None self.set_default_instance(None) registry = Registry() diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user_profile.py index 2ce6c38..37a8def 100644 --- a/tests/integration_test_user_profile.py +++ b/tests/integration_test_user_profile.py @@ -38,5 +38,4 @@ def test_profile_change_schema(self): self.user.profile.profile_pic = self.SAMPLE_PROFILE_PIC self.user.profile.save() user = User.please.get(id=self.user.id) - user.reload() self.assertEqual(user.profile.profile_pic, self.SAMPLE_PROFILE_PIC) From 1195637f6506c99320a125af5d1a02bce22f6a91 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 17 Nov 2015 11:45:50 +0100 Subject: [PATCH 166/558] [LIB-226] do not wrap payload in another dict & correct reset_link method --- syncano/models/incentives.py | 19 +++++++++---------- tests/test_incentives.py | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 4645a85..f48eefb 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -272,24 +272,23 @@ def run(self, **payload): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('run', properties) connection = self._get_connection(**payload) - request = { - 'data': { - 'payload': json.dumps(payload) - } - } - response = connection.request('POST', endpoint, **request) + + response = connection.request('POST', endpoint, **{'data': payload}) + response.update({'instance_name': self.instance_name, 'webhook_name': self.name}) return WebhookTrace(**response) - def reset(self, **payload): + def reset_link(self): """ Usage:: >>> wh = Webhook.please.get('instance-name', 'webhook-name') - >>> wh.reset() + >>> wh.reset_link() """ properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('reset', properties) - connection = self._get_connection(**payload) - return connection.request('POST', endpoint) + connection = self._get_connection() + + response = connection.request('POST', endpoint) + self.public_link = response['public_link'] diff --git a/tests/test_incentives.py b/tests/test_incentives.py index 7cfb39d..6d013ed 100644 --- a/tests/test_incentives.py +++ b/tests/test_incentives.py @@ -68,7 +68,7 @@ def test_run(self, connection_mock): connection_mock.request.assert_called_once_with( 'POST', '/v1/instances/test/webhooks/name/run/', - data={'payload': '{"y": 2, "x": 1}'} + data={"y": 2, "x": 1} ) model = Webhook() From f7444168cc702500df1f16cdd511d0e9f9ef3c6b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 17 Nov 2015 12:02:35 +0100 Subject: [PATCH 167/558] [LIB-154] provide possibility to make batch queries --- syncano/models/archetypes.py | 17 ++++++++-- syncano/models/classes.py | 11 +++++-- syncano/models/manager.py | 63 +++++++++++++++++++++++++++++------- syncano/models/registry.py | 10 ++++++ 4 files changed, 84 insertions(+), 17 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index a720dac..351284f 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -78,6 +78,7 @@ class Model(six.with_metaclass(ModelMetaclass)): """Base class for all models. """ def __init__(self, **kwargs): + self.is_lazy = kwargs.pop('is_lazy', False) self._raw_data = {} self.to_python(kwargs) @@ -129,10 +130,20 @@ def save(self, **kwargs): endpoint = self._meta.resolve_endpoint(endpoint_name, properties) request = {'data': data} - response = connection.request(method, endpoint, **request) + if not self.is_lazy: + response = connection.request(method, endpoint, **request) + self.to_python(response) + return self - self.to_python(response) - return self + else: + return self.batch_object(method=method, path=endpoint, body=request['data']) + + def batch_object(self, method, path, body): + return { + 'method': method, + 'path': path, + 'body': body, + } def delete(self, **kwargs): """Removes the current instance. diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 5bd0a20..4c80b31 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -79,6 +79,10 @@ class Meta: } } + def save(self, **kwargs): + registry.set_schema(self.name, self.schema.schema) # update the registry schema here; + return super(Class, self).save(**kwargs) + class Object(Model): """ @@ -191,8 +195,11 @@ def get_subclass_name(cls, instance_name, class_name): @classmethod def get_class_schema(cls, instance_name, class_name): parent = cls._meta.parent - class_ = parent.please.get(instance_name, class_name) - return class_.schema + schema = registry.get_schema(class_name) + if not schema: + schema = parent.please.get(instance_name, class_name).schema + registry.set_schema(class_name, schema) + return schema @classmethod def get_subclass_model(cls, instance_name, class_name, **kwargs): diff --git a/syncano/models/manager.py b/syncano/models/manager.py index d763167..f007265 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -67,6 +67,8 @@ def __get__(self, instance, owner=None): class Manager(ConnectionMixin): """Base class responsible for all ORM (``please``) actions.""" + BATCH_URI = '/v1/instances/{name}/batch/' + def __init__(self): self.name = None self.model = None @@ -77,6 +79,7 @@ def __init__(self): self.method = None self.query = {} self.data = {} + self.is_lazy = False self._limit = None self._serialize = True @@ -140,6 +143,21 @@ def _set_default_properties(self, endpoint_properties): if is_demanded and has_default: self.properties[field.name] = field.default + def as_batch(self): + self.is_lazy = True + return self + + def batch(self, *args): + # firstly turn off lazy mode: + self.is_lazy = False + response = self.connection.request( + 'POST', + self.BATCH_URI.format(name=self.properties.get('instance_name')), + **{'data': {'requests': args}} + ) + + return response + # Object actions def create(self, **kwargs): """ @@ -156,8 +174,9 @@ def create(self, **kwargs): """ attrs = kwargs.copy() attrs.update(self.properties) - instance = self.model(**attrs) - instance.save() + attrs.update({'is_lazy': self.is_lazy}) + instance = self._get_instance(attrs) + instance = instance.save() return instance @@ -173,7 +192,30 @@ def bulk_create(self, *objects): .. warning:: This method is not meant to be used with large data sets. """ - return [self.create(**o) for o in objects] + + if len(objects) > 50: + raise SyncanoValueError('Only 50 objects can be created at once.') + + class_name = objects[0].get('class_name') + for o in objects[1:]: + next_class_name = o.get('class_name') + if o.get('class_name') != class_name: + raise SyncanoValidationError('Bulk create can handle only objects of the same type.') + class_name = next_class_name + + response = self.batch(*[self.as_batch().create(**o) for o in objects]) + + instances = [] + for res in response: + if res['code'] == 201: + content_res = res['content'] + content_res.update(self.properties) + if class_name: + content_res.update({'class_name': class_name}) + model = self.model(**content_res) + model.to_python(content_res) + instances.append(model) + return instances @clone def get(self, *args, **kwargs): @@ -558,6 +600,9 @@ def iterator(self): response = self.request(path=next_url) + def _get_instance(self, attrs): + return self.model(**attrs) + class CodeBoxManager(Manager): """ @@ -616,15 +661,6 @@ class for :class:`~syncano.models.base.Object` model. 'eq', 'neq', 'exists', 'in', ] - def create(self, **kwargs): - attrs = kwargs.copy() - attrs.update(self.properties) - - model = self.model.get_subclass_model(**attrs) - instance = model(**attrs) - instance.save() - - return instance def serialize(self, data, model=None): model = model or self.model.get_subclass_model(**self.properties) @@ -668,6 +704,9 @@ def filter(self, **kwargs): self.endpoint = 'list' return self + def _get_instance(self, attrs): + return self.model.get_subclass_model(**attrs)(**attrs) + def _get_model_field_names(self): object_fields = [f.name for f in self.model._meta.fields] schema = self.model.get_class_schema(**self.properties) diff --git a/syncano/models/registry.py b/syncano/models/registry.py index 805b4b3..d21f270 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -11,6 +11,7 @@ class Registry(object): """ def __init__(self, models=None): self.models = models or {} + self.schemas = {} self.patterns = [] self._pending_lookups = {} self.last_used_instance = None @@ -87,4 +88,13 @@ def clear_instance_name(self): self.last_used_instance = None self.set_default_instance(None) + def get_schema(self, class_name): + return self.schemas.get(class_name) + + def set_schema(self, class_name, schema): + self.schemas[class_name] = schema + + def clear_schemas(self): + self.schemas = {} + registry = Registry() From b71e85c1d8731fa87bda059a67178c6b895ca065 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 17 Nov 2015 12:04:23 +0100 Subject: [PATCH 168/558] [LIB-154] correct flake --- syncano/models/manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index f007265..a65f9d7 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -661,7 +661,6 @@ class for :class:`~syncano.models.base.Object` model. 'eq', 'neq', 'exists', 'in', ] - def serialize(self, data, model=None): model = model or self.model.get_subclass_model(**self.properties) return super(ObjectManager, self).serialize(data, model) From 01528701ec74d540eae86d47eb4d5bbeb2c701aa Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 17 Nov 2015 13:41:22 +0100 Subject: [PATCH 169/558] [LIB-154] fixes after ci; --- syncano/models/archetypes.py | 3 +-- syncano/models/classes.py | 7 +------ syncano/models/manager.py | 7 ++++--- tests/test_manager.py | 7 +++---- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 351284f..1cbaf9d 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -135,8 +135,7 @@ def save(self, **kwargs): self.to_python(response) return self - else: - return self.batch_object(method=method, path=endpoint, body=request['data']) + return self.batch_object(method=method, path=endpoint, body=request['data']) def batch_object(self, method, path, body): return { diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 4c80b31..c876461 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -220,19 +220,14 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): model = cls.create_subclass(model_name, schema) registry.add(model_name, model) - schema_changed = False for field in schema: try: getattr(model, field['name']) except AttributeError: # schema changed, update the registry; - schema_changed = True + registry.update(model_name, model) break - if schema_changed: - model = cls.create_subclass(model_name, schema) - registry.update(model_name, model) - return model diff --git a/syncano/models/manager.py b/syncano/models/manager.py index a65f9d7..eb213d5 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -176,9 +176,10 @@ def create(self, **kwargs): attrs.update(self.properties) attrs.update({'is_lazy': self.is_lazy}) instance = self._get_instance(attrs) - instance = instance.save() - - return instance + saved_instance = instance.save() + if not self.is_lazy: + return instance + return saved_instance def bulk_create(self, *objects): """ diff --git a/tests/test_manager.py b/tests/test_manager.py index d3a7a3c..dea13fa 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -56,7 +56,7 @@ def test_create(self): self.assertTrue(model_mock.save.called) self.assertEqual(instance, model_mock) - model_mock.assert_called_once_with(a=1, b=2) + model_mock.assert_called_once_with(a=1, b=2, is_lazy=False) model_mock.save.assert_called_once_with() @mock.patch('syncano.models.manager.Manager.create') @@ -489,15 +489,14 @@ def test_create(self, get_subclass_model_mock): self.assertFalse(model_mock.called) self.assertFalse(get_subclass_model_mock.called) instance = self.manager.create(a=1, b=2) - self.assertTrue(model_mock.called) self.assertTrue(model_mock.save.called) self.assertTrue(get_subclass_model_mock.called) self.assertEqual(instance, model_mock) - model_mock.assert_called_once_with(a=1, b=2) + model_mock.assert_called_once_with(a=1, b=2, is_lazy=False) model_mock.save.assert_called_once_with() - get_subclass_model_mock.assert_called_once_with(a=1, b=2) + get_subclass_model_mock.assert_called_once_with(a=1, b=2, is_lazy=False) @mock.patch('syncano.models.Object.get_subclass_model') def test_serialize(self, get_subclass_model_mock): From 19f14dc6d3b0611c9ce557897383f53eb00cd67b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 17 Nov 2015 14:19:18 +0100 Subject: [PATCH 170/558] [LIB-154] correct fetching schema --- syncano/models/classes.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index c876461..6350f26 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -80,7 +80,10 @@ class Meta: } def save(self, **kwargs): - registry.set_schema(self.name, self.schema.schema) # update the registry schema here; + print(self.schema) + print(200*'#') + if self.schema: # do not allow add empty schema to registry; + registry.set_schema(self.name, self.schema) # update the registry schema here; return super(Class, self).save(**kwargs) @@ -192,13 +195,17 @@ def get_or_create_subclass(cls, name, schema): def get_subclass_name(cls, instance_name, class_name): return get_class_name(instance_name, class_name, 'object') + @classmethod + def fetch_schema(cls, instance_name, class_name): + return cls._meta.parent.please.get(instance_name, class_name).schema + @classmethod def get_class_schema(cls, instance_name, class_name): - parent = cls._meta.parent schema = registry.get_schema(class_name) if not schema: - schema = parent.please.get(instance_name, class_name).schema - registry.set_schema(class_name, schema) + schema = cls.fetch_schema(instance_name, class_name) + if schema: # do not allow to add to registry empty schema; + registry.set_schema(class_name, schema) return schema @classmethod @@ -212,19 +219,21 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): if cls.__name__ == model_name: return cls - schema = cls.get_class_schema(instance_name, class_name) - try: model = registry.get_model_by_name(model_name) except LookupError: + schema = cls.fetch_schema(instance_name, class_name) model = cls.create_subclass(model_name, schema) registry.add(model_name, model) + schema = cls.get_class_schema(instance_name, class_name) + for field in schema: try: getattr(model, field['name']) except AttributeError: # schema changed, update the registry; + model = cls.create_subclass(model_name, schema) registry.update(model_name, model) break From c0d020c3aaf887a5b8e1460791026ffb95e378c5 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 17 Nov 2015 14:24:39 +0100 Subject: [PATCH 171/558] [LIB-154] correct fetching schema --- syncano/models/classes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 6350f26..ba8720d 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -80,8 +80,6 @@ class Meta: } def save(self, **kwargs): - print(self.schema) - print(200*'#') if self.schema: # do not allow add empty schema to registry; registry.set_schema(self.name, self.schema) # update the registry schema here; return super(Class, self).save(**kwargs) From beb6aa033053a9c30a3a3be58eb0468f2008540e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 17 Nov 2015 14:30:23 +0100 Subject: [PATCH 172/558] [LIB-154] add one mote mock for tests --- tests/test_classes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_classes.py b/tests/test_classes.py index 20fd23a..e147c33 100644 --- a/tests/test_classes.py +++ b/tests/test_classes.py @@ -122,6 +122,7 @@ def test_get_class_schema(self, get_mock): @mock.patch('syncano.models.Object.get_class_schema') @mock.patch('syncano.models.manager.registry.get_model_by_name') @mock.patch('syncano.models.Object.get_subclass_name') + @mock.patch('syncano.models.Object.fetch_schema') def test_get_subclass_model(self, get_subclass_name_mock, get_model_by_name_mock, get_class_schema_mock, create_subclass_mock): From 829af77a2c830ea0f9a406bae5e2125672af00b8 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 17 Nov 2015 14:56:54 +0100 Subject: [PATCH 173/558] [LIB-154] remove fetch_schema method; --- syncano/models/classes.py | 10 ++++------ tests/test_classes.py | 1 - 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index ba8720d..3962fd7 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -193,15 +193,12 @@ def get_or_create_subclass(cls, name, schema): def get_subclass_name(cls, instance_name, class_name): return get_class_name(instance_name, class_name, 'object') - @classmethod - def fetch_schema(cls, instance_name, class_name): - return cls._meta.parent.please.get(instance_name, class_name).schema - @classmethod def get_class_schema(cls, instance_name, class_name): schema = registry.get_schema(class_name) if not schema: - schema = cls.fetch_schema(instance_name, class_name) + parent = cls._meta.parent + schema = parent.please.get(instance_name, class_name).schema if schema: # do not allow to add to registry empty schema; registry.set_schema(class_name, schema) return schema @@ -220,7 +217,8 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): try: model = registry.get_model_by_name(model_name) except LookupError: - schema = cls.fetch_schema(instance_name, class_name) + parent = cls._meta.parent + schema = parent.please.get(instance_name, class_name).schema model = cls.create_subclass(model_name, schema) registry.add(model_name, model) diff --git a/tests/test_classes.py b/tests/test_classes.py index e147c33..20fd23a 100644 --- a/tests/test_classes.py +++ b/tests/test_classes.py @@ -122,7 +122,6 @@ def test_get_class_schema(self, get_mock): @mock.patch('syncano.models.Object.get_class_schema') @mock.patch('syncano.models.manager.registry.get_model_by_name') @mock.patch('syncano.models.Object.get_subclass_name') - @mock.patch('syncano.models.Object.fetch_schema') def test_get_subclass_model(self, get_subclass_name_mock, get_model_by_name_mock, get_class_schema_mock, create_subclass_mock): From cdadf6296c6c7d9cc734ac590598ad5b1a123aac Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 17 Nov 2015 15:53:05 +0100 Subject: [PATCH 174/558] [LIB-154] correct test for get subclass --- tests/test_classes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_classes.py b/tests/test_classes.py index 20fd23a..13a6a50 100644 --- a/tests/test_classes.py +++ b/tests/test_classes.py @@ -122,9 +122,14 @@ def test_get_class_schema(self, get_mock): @mock.patch('syncano.models.Object.get_class_schema') @mock.patch('syncano.models.manager.registry.get_model_by_name') @mock.patch('syncano.models.Object.get_subclass_name') - def test_get_subclass_model(self, get_subclass_name_mock, get_model_by_name_mock, + @mock.patch('syncano.connection.default_connection') + @mock.patch('syncano.models.manager.Manager.serialize') + def test_get_subclass_model(self, serialize_mock, default_connection_mock, get_subclass_name_mock, get_model_by_name_mock, get_class_schema_mock, create_subclass_mock): + default_connection_mock.return_value = default_connection_mock + serialize_mock.return_value = serialize_mock + create_subclass_mock.return_value = create_subclass_mock get_subclass_name_mock.side_effect = [ 'Object', From eaa6827f24e91040517b6abc2e7db4d71b6b1ea6 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 17 Nov 2015 15:56:17 +0100 Subject: [PATCH 175/558] [LIB-154] correct test for get subclass --- tests/test_classes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_classes.py b/tests/test_classes.py index 13a6a50..64f5930 100644 --- a/tests/test_classes.py +++ b/tests/test_classes.py @@ -124,8 +124,8 @@ def test_get_class_schema(self, get_mock): @mock.patch('syncano.models.Object.get_subclass_name') @mock.patch('syncano.connection.default_connection') @mock.patch('syncano.models.manager.Manager.serialize') - def test_get_subclass_model(self, serialize_mock, default_connection_mock, get_subclass_name_mock, get_model_by_name_mock, - get_class_schema_mock, create_subclass_mock): + def test_get_subclass_model(self, serialize_mock, default_connection_mock, get_subclass_name_mock, + get_model_by_name_mock, get_class_schema_mock, create_subclass_mock): default_connection_mock.return_value = default_connection_mock serialize_mock.return_value = serialize_mock From b46bc5a1e2ea345f225975355a1d3ea4689c60a6 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 18 Nov 2015 15:14:09 +0100 Subject: [PATCH 176/558] [LIB-154] add batchin of update and delete --- syncano/models/archetypes.py | 21 +++++-- syncano/models/bulk.py | 93 +++++++++++++++++++++++++++++++ syncano/models/manager.py | 104 ++++++++++++++++++++++------------- tests/test_manager.py | 11 ++-- 4 files changed, 182 insertions(+), 47 deletions(-) create mode 100644 syncano/models/bulk.py diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 1cbaf9d..95be2db 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -135,15 +135,26 @@ def save(self, **kwargs): self.to_python(response) return self - return self.batch_object(method=method, path=endpoint, body=request['data']) + return self.batch_object(method=method, path=endpoint, body=request['data'], properties=data) - def batch_object(self, method, path, body): + @classmethod + def batch_object(cls, method, path, body, properties=None): + properties = properties if properties else {} return { - 'method': method, - 'path': path, - 'body': body, + 'body': { + 'method': method, + 'path': path, + 'body': body, + }, + 'meta': { + 'model': cls, + 'properties': properties + } } + def mark_for_batch(self): + self.is_lazy = True + def delete(self, **kwargs): """Removes the current instance. """ diff --git a/syncano/models/bulk.py b/syncano/models/bulk.py new file mode 100644 index 0000000..d8e08e0 --- /dev/null +++ b/syncano/models/bulk.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +from abc import ABCMeta, abstractmethod + +from syncano.exceptions import SyncanoValueError, SyncanoValidationError + + +class BaseBulkCreate(object): + """ + Helper class for making bulk create; + + Usage: + instances = ObjectBulkCreate(objects, manager).process() + """ + __metaclass__ = ABCMeta + + MAX_BATCH_SIZE = 50 + + @abstractmethod + def __init__(self, objects, manager): + self.objects = objects + self.manager = manager + self.response = None + self.validated = False + + def validate(self): + if len(self.objects) > self.MAX_BATCH_SIZE: + raise SyncanoValueError('Only 50 objects can be created at once.') + + def make_batch_request(self): + if not self.validated: + raise SyncanoValueError('Bulk create not validated') + self.response = self.manager.batch(*[o.save() for o in self.objects]) + + def update_response(self, content_reponse): + content_reponse.update(self.manager.properties) + + def process(self): + self.validate() + self.make_batch_request() + return self.response + + +class ObjectBulkCreate(BaseBulkCreate): + + def __init__(self, objects, manager): + super(ObjectBulkCreate, self).__init__(objects, manager) + + def validate(self): + super(ObjectBulkCreate, self).validate() + + class_names = [] + instance_names = [] + # mark objects as lazy & make some check btw; + for o in self.objects: + class_names.append(o.class_name) + instance_names.append(o.instance_name) + o.mark_for_batch() + + if len(set(class_names)) != 1: + raise SyncanoValidationError('Bulk create can handle only objects of the same type.') + + if len(set(instance_names)) != 1: + raise SyncanoValidationError('Bulk create can handle only one instance.') + self.validated = True + + def update_response(self, content_reponse): + super(ObjectBulkCreate, self).update_response(content_reponse) + content_reponse.update( + { + 'class_name': self.objects[0].class_name, + 'instance_name': self.objects[0].instance_name + } + ) + + +class ModelBulkCreate(BaseBulkCreate): + + def __init__(self, objects, manager): + super(ModelBulkCreate, self).__init__(objects, manager) + + def validate(self): + super(ModelBulkCreate, self).validate() + + class_names = [] + # mark objects as lazy & make some check btw; + for o in self.objects: + class_names.append(type(o)) + o.mark_for_batch() + + if len(set(class_names)) != 1: + raise SyncanoValidationError('Bulk create can handle only objects of the same type.') + + self.validated = True diff --git a/syncano/models/manager.py b/syncano/models/manager.py index eb213d5..7082692 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -5,7 +5,7 @@ import six from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValidationError, SyncanoValueError - +from syncano.models.bulk import ObjectBulkCreate, ModelBulkCreate, BaseBulkCreate from .registry import registry # The maximum number of items to display in a Manager.__repr__ @@ -58,7 +58,9 @@ def __get__(self, instance, owner=None): method = getattr(Model.please, self.endpoint, Model.please.all) properties = instance._meta.get_endpoint_properties('detail') - registry.set_last_used_instance(getattr(instance, 'name', None)) + + if instance.__class__.__name__ == 'Instance': + registry.set_last_used_instance(getattr(instance, 'name', None)) properties = [getattr(instance, prop) for prop in properties] return method(*properties) @@ -149,14 +151,28 @@ def as_batch(self): def batch(self, *args): # firstly turn off lazy mode: + meta = [arg['meta'] for arg in args] + self.is_lazy = False response = self.connection.request( 'POST', - self.BATCH_URI.format(name=self.properties.get('instance_name')), - **{'data': {'requests': args}} + self.BATCH_URI.format(name=registry.last_used_instance), + **{'data': {'requests': [arg['body'] for arg in args]}} ) - return response + populated_response = [] + + for meta, res in zip(meta, response): + if res['code'] in [200, 201]: # success response: update or create; + content = res['content'] + model = meta['model'] + properties = meta['properties'] + content.update(properties) + populated_response.append(model(**content)) + else: + populated_response.append(res) + + return populated_response # Object actions def create(self, **kwargs): @@ -186,37 +202,17 @@ def bulk_create(self, *objects): Creates many new instances based on provided list of objects. Usage:: + instance = Instance.please.get(name='instance_a') - objects = [{'name': 'test-one'}, {'name': 'test-two'}] - instances = Instance.please.bulk_create(objects) + instances = instance.users.bulk_create( + User(username='user_a', password='1234'), + User(username='user_b', password='4321') + ) .. warning:: - This method is not meant to be used with large data sets. + This method is restricted to handle 50 objects at once. """ - - if len(objects) > 50: - raise SyncanoValueError('Only 50 objects can be created at once.') - - class_name = objects[0].get('class_name') - for o in objects[1:]: - next_class_name = o.get('class_name') - if o.get('class_name') != class_name: - raise SyncanoValidationError('Bulk create can handle only objects of the same type.') - class_name = next_class_name - - response = self.batch(*[self.as_batch().create(**o) for o in objects]) - - instances = [] - for res in response: - if res['code'] == 201: - content_res = res['content'] - content_res.update(self.properties) - if class_name: - content_res.update({'class_name': class_name}) - model = self.model(**content_res) - model.to_python(content_res) - instances.append(model) - return instances + return ModelBulkCreate(objects, self).process() @clone def get(self, *args, **kwargs): @@ -289,7 +285,12 @@ def delete(self, *args, **kwargs): self.method = 'DELETE' self.endpoint = 'detail' self._filter(*args, **kwargs) - return self.request() + if not self.is_lazy: + return self.request() + + path, defaults = self._get_endpoint_properties() + + return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) @clone def update(self, *args, **kwargs): @@ -314,6 +315,7 @@ def update(self, *args, **kwargs): self.data = kwargs.pop('data', kwargs) model = self.serialize(self.data, self.model) + serialized = model.to_native() serialized = {k: v for k, v in serialized.iteritems() @@ -321,7 +323,13 @@ def update(self, *args, **kwargs): self.data.update(serialized) self._filter(*args, **kwargs) - return self.request() + + if not self.is_lazy: + return self.request() + + path, defaults = self._get_endpoint_properties() + + return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) def update_or_create(self, defaults=None, **kwargs): """ @@ -513,6 +521,7 @@ def _clone(self): manager.query = deepcopy(self.query) manager.data = deepcopy(self.data) manager._serialize = self._serialize + manager.is_lazy = self.is_lazy return manager @@ -541,10 +550,7 @@ def request(self, method=None, path=None, **request): allowed_methods = meta.get_endpoint_methods(self.endpoint) if not path: - defaults = {f.name: f.default for f in self.model._meta.fields - if f.default is not None} - defaults.update(self.properties) - path = meta.resolve_endpoint(self.endpoint, defaults) + path, defaults = self._get_endpoint_properties() if method.lower() not in allowed_methods: methods = ', '.join(allowed_methods) @@ -604,6 +610,14 @@ def iterator(self): def _get_instance(self, attrs): return self.model(**attrs) + def _get_endpoint_properties(self): + defaults = {f.name: f.default for f in self.model._meta.fields + if f.default is not None} + defaults.update(self.properties) + if defaults.get('instance_name'): + registry.set_last_used_instance(defaults['instance_name']) + return self.model._meta.resolve_endpoint(self.endpoint, defaults), defaults + class CodeBoxManager(Manager): """ @@ -704,6 +718,20 @@ def filter(self, **kwargs): self.endpoint = 'list' return self + def bulk_create(self, *objects): + """ + Creates many new objects. + Usage: + created_objects = Object.please.bulk_create( + Object(instance_name='instance_a', class_name='some_class', title='one'), + Object(instance_name='instance_a', class_name='some_class', title='two'), + Object(instance_name='instance_a', class_name='some_class', title='three') + ) + :param objects: a list of the instances of data objects to be created; + :return: a created and populated list of objects; When error occurs a plain dict is returned in that place; + """ + return ObjectBulkCreate(objects, self).process() + def _get_instance(self, attrs): return self.model.get_subclass_model(**attrs)(**attrs) diff --git a/tests/test_manager.py b/tests/test_manager.py index dea13fa..716d34e 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -2,7 +2,7 @@ from datetime import datetime from syncano.exceptions import SyncanoDoesNotExist, SyncanoRequestError, SyncanoValueError -from syncano.models import CodeBox, CodeBoxTrace, Instance, Object, Webhook, WebhookTrace +from syncano.models import CodeBox, CodeBoxTrace, Instance, Object, Webhook, WebhookTrace, User try: from unittest import mock @@ -59,12 +59,15 @@ def test_create(self): model_mock.assert_called_once_with(a=1, b=2, is_lazy=False) model_mock.save.assert_called_once_with() - @mock.patch('syncano.models.manager.Manager.create') + @mock.patch('syncano.models.bulk.ModelBulkCreate.make_batch_request') def test_bulk_create(self, create_mock): self.assertFalse(create_mock.called) - self.manager.bulk_create({'a': 1}, {'a': 2}) + self.manager.bulk_create( + User(instance_name='A', username='a', password='a'), + User(instance_name='A', username='b', password='b') + ) self.assertTrue(create_mock.called) - self.assertEqual(create_mock.call_count, 2) + self.assertEqual(create_mock.call_count, 1) @mock.patch('syncano.models.manager.Manager.request') @mock.patch('syncano.models.manager.Manager._filter') From e5a682222763493aedeed7a281280fcb03c5c8dc Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 18 Nov 2015 15:51:35 +0100 Subject: [PATCH 177/558] [LIB-154] add doc comments for batch method; --- syncano/models/manager.py | 52 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 7082692..5106916 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -150,6 +150,58 @@ def as_batch(self): return self def batch(self, *args): + """ + A convenience method for making a batch request. Only create, update and delete manager method are supported. + Batch request are limited to 50. So the args length should be equal or less than 50. + + Usage:: + + klass = instance.classes.get(name='some_class') + + Object.please.batch( + klass.objects.as_batch().delete(id=652), + klass.objects.as_batch().delete(id=653), + ... + ) + + and:: + + Object.please.batch( + klass.objects.as_batch().update(id=652, arg='some_b'), + klass.objects.as_batch().update(id=653, arg='some_b'), + ... + ) + + and:: + Object.please.batch( + klass.objects.as_batch().create(arg='some_c'), + klass.objects.as_batch().create(arg='some_c'), + ... + ) + and:: + + Object.please.batch( + klass.objects.as_batch().delete(id=653), + klass.objects.as_batch().update(id=652, arg='some_a'), + klass.objects.as_batch().create(arg='some_c'), + ... + ) + + are posible. + + But:: + Object.please.batch( + klass.objects.as_batch().get_or_create(id=653, arg='some_a') + ) + + will not work as expected. + + :param args: a arg is on of the: klass.objects.as_batch().create(...), klass.objects.as_batch().update(...), + klass.objects.as_batch().delete(...) + :return: a list with objects corresponding to batch arguments; update and create will return a populated Object, + when delete return a raw response from server (usually a dict: {'code': 204}, sometimes information about not + found resource to delete); + """ # firstly turn off lazy mode: meta = [arg['meta'] for arg in args] From 4b5877180c010f9656aa0b3c69bdea0edd25baf3 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 18 Nov 2015 15:57:05 +0100 Subject: [PATCH 178/558] [LIB-154] add doc comments for batch method; --- syncano/models/manager.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 5106916..df46043 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -196,6 +196,20 @@ def batch(self, *args): will not work as expected. + Some snippet for working with instance users: + + instance = Instance.please.get(name='Nabuchodonozor') + + model_users = instance.users.batch( + instance.users.as_batch().delete(id=7), + instance.users.as_batch().update(id=9, username='username_a'), + instance.users.as_batch().create(username='username_b', password='5432'), + ... + ) + + And sample response will be: + [{u'code': 204}, , , ...] + :param args: a arg is on of the: klass.objects.as_batch().create(...), klass.objects.as_batch().update(...), klass.objects.as_batch().delete(...) :return: a list with objects corresponding to batch arguments; update and create will return a populated Object, From 41793c6f8867893ca4af7f2278921c4d75469f84 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 18 Nov 2015 16:48:00 +0100 Subject: [PATCH 179/558] [LIB-154] add test for batches; --- tests/integration_test_batch.py | 103 ++++++++++++++++++++++++++++++++ tests/test_manager.py | 38 ++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 tests/integration_test_batch.py diff --git a/tests/integration_test_batch.py b/tests/integration_test_batch.py new file mode 100644 index 0000000..a5f947a --- /dev/null +++ b/tests/integration_test_batch.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +from syncano.models import User, Object +from tests.integration_test import InstanceMixin, IntegrationTest + + +class ManagerBatchTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(ManagerBatchTest, cls).setUpClass() + cls.klass = cls.instance.classes.create(name='class_a', schema=[{'name': 'title', 'type': 'string'}]) + cls.update1 = cls.klass.objects.create(title='update1') + cls.update2 = cls.klass.objects.create(title='update2') + cls.update3 = cls.klass.objects.create(title='update3') + cls.delete1 = cls.klass.objects.create(title='delete1') + cls.delete2 = cls.klass.objects.create(title='delete2') + cls.delete3 = cls.klass.objects.create(title='delete3') + + def test_batch_create(self): + + objects = [] + for i in range(5): + objects.append(Object(instance_name=self.instance.name, class_name=self.klass.name, title=str(i))) + + results = Object.please.bulk_create(*objects) + for r in results: + self.assertTrue(isinstance(r, Object)) + self.assertTrue(r.id) + self.assertTrue(r.title) + + # test batch now: + results = Object.please.batch( + self.klass.objects.as_batch().create(title='one'), + self.klass.objects.as_batch().create(title='two'), + self.klass.objects.as_batch().create(title='three'), + ) + + for r in results: + self.assertTrue(isinstance(r, Object)) + self.assertTrue(r.id) + self.assertTrue(r.title) + + def test_create_batch_users(self): + users = self.instance.users.bulk_create( + User(username='Terminator', password='skynet'), + User(username='Terminator2', password='skynet'), + ) + + self.assertEqual(len(set([u.username for u in users])), 2) + + for user in users: + self.assertTrue(isinstance(user, User)) + self.assertTrue(user.id) + self.assertTrue(user.username in ['Terminator', 'Terminator2']) + + # test batch now: + users = self.instance.users.batch( + self.instance.users.as_batch().create(username='Terminator3', password='SarahConnor'), + self.instance.users.as_batch().create(username='Terminator4', password='BigTruckOnRoad'), + ) + + for user in users: + self.assertTrue(isinstance(user, User)) + self.assertTrue(user.id) + self.assertTrue(user.username in ['Terminator3', 'Terminator4']) + + def test_batch_update(self): + updates = Object.please.batch( + self.klass.objects.as_batch().update(id=self.update1.id, title='FactoryChase'), + self.klass.objects.as_batch().update(id=self.update1.id, title='Photoplay'), + self.klass.objects.as_batch().update(id=self.update1.id, title='Intimacy'), + ) + + self.assertEqual(len(set([u.title for u in updates])), 3) + + for u in updates: + self.assertTrue(u.title in ['FactoryChase', 'Photoplay', 'Intimacy']) + + def test_batch_delete(self): + deletes = Object.please.batch( + self.klass.objects.as_batch().delete(id=self.delete1.id), + self.klass.objects.as_batch().delete(id=self.delete2.id), + ) + + for d in deletes: + self.assertTrue(d['code'], 204) + + def test_batch_mix(self): + mix_batches = Object.please.batch( + self.klass.objects.as_batch().create(title='four'), + self.klass.objects.as_batch().update(title='TerminatorArrival', id=self.update3.id), + self.klass.objects.as_batch().delete(id=self.delete3.id) + ) + + # assert create; + self.assertTrue(mix_batches[0].id) + self.assertEqual(mix_batches[0].title, 'four') + + # assert update; + self.assertEqual(mix_batches[1].title, 'TerminatorArrival') + + # assert delete; + self.assertEqual(mix_batches[2]['code'], 204) diff --git a/tests/test_manager.py b/tests/test_manager.py index 716d34e..2788875 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -69,6 +69,44 @@ def test_bulk_create(self, create_mock): self.assertTrue(create_mock.called) self.assertEqual(create_mock.call_count, 1) + @mock.patch('syncano.models.manager.Manager.create') + @mock.patch('syncano.models.manager.Manager.update') + @mock.patch('syncano.models.manager.Manager.delete') + def test_batch(self, delete_mock, update_mock, create_mock): + self.assertFalse(delete_mock.called) + self.assertFalse(update_mock.called) + self.assertFalse(create_mock.called) + self.assertFalse(self.manager.is_lazy) + self.manager.batch( + self.manager.as_batch().update(id=2, a=1, b=3, name='Nabuchodonozor'), + self.manager.as_batch().create(a=2, b=3, name='Nabuchodonozor'), + self.manager.as_batch().delete(id=3, name='Nabuchodonozor'), + ) + self.assertFalse(self.manager.is_lazy) + self.assertEqual(delete_mock.call_count, 1) + self.assertEqual(update_mock.call_count, 1) + self.assertEqual(create_mock.call_count, 1) + + @mock.patch('syncano.models.archetypes.Model.batch_object') + def test_batch_object(self, batch_mock): + self.assertFalse(batch_mock.called) + self.manager.batch( + self.manager.as_batch().create(a=2, b=3, name='Nabuchodonozor'), + ) + self.assertTrue(batch_mock.called) + self.assertEqual(batch_mock.call_count, 1) + + @mock.patch('syncano.models.manager.Manager.request') + def test_batch_request(self, request_mock): + self.assertFalse(request_mock.called) + self.manager.batch( + self.manager.as_batch().update(a=2, b=3, name='Nabuchodonozor'), + ) + self.assertFalse(request_mock.called) # shouldn't be called when batch mode is on; + self.manager.update(a=2, b=3, name='Nabuchodonozor') + self.assertTrue(request_mock.called) + self.assertEqual(request_mock.call_count, 1) + @mock.patch('syncano.models.manager.Manager.request') @mock.patch('syncano.models.manager.Manager._filter') @mock.patch('syncano.models.manager.Manager._clone') From ab6733736e422041b1be5f653e6206486870f763 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 18 Nov 2015 16:56:18 +0100 Subject: [PATCH 180/558] [LIB-154] correct isort and flake :) --- syncano/models/bulk.py | 6 +++--- syncano/models/manager.py | 6 +++--- tests/integration_test_batch.py | 2 +- tests/test_manager.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/syncano/models/bulk.py b/syncano/models/bulk.py index d8e08e0..ba95abf 100644 --- a/syncano/models/bulk.py +++ b/syncano/models/bulk.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from abc import ABCMeta, abstractmethod -from syncano.exceptions import SyncanoValueError, SyncanoValidationError +from syncano.exceptions import SyncanoValidationError, SyncanoValueError class BaseBulkCreate(object): @@ -41,10 +41,10 @@ def process(self): class ObjectBulkCreate(BaseBulkCreate): - + def __init__(self, objects, manager): super(ObjectBulkCreate, self).__init__(objects, manager) - + def validate(self): super(ObjectBulkCreate, self).validate() diff --git a/syncano/models/manager.py b/syncano/models/manager.py index df46043..e32aed8 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -5,7 +5,8 @@ import six from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValidationError, SyncanoValueError -from syncano.models.bulk import ObjectBulkCreate, ModelBulkCreate, BaseBulkCreate +from syncano.models.bulk import ModelBulkCreate, ObjectBulkCreate + from .registry import registry # The maximum number of items to display in a Manager.__repr__ @@ -677,8 +678,7 @@ def _get_instance(self, attrs): return self.model(**attrs) def _get_endpoint_properties(self): - defaults = {f.name: f.default for f in self.model._meta.fields - if f.default is not None} + defaults = {f.name: f.default for f in self.model._meta.fields if f.default is not None} defaults.update(self.properties) if defaults.get('instance_name'): registry.set_last_used_instance(defaults['instance_name']) diff --git a/tests/integration_test_batch.py b/tests/integration_test_batch.py index a5f947a..341ff30 100644 --- a/tests/integration_test_batch.py +++ b/tests/integration_test_batch.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from syncano.models import User, Object +from syncano.models import Object, User from tests.integration_test import InstanceMixin, IntegrationTest diff --git a/tests/test_manager.py b/tests/test_manager.py index 2788875..3392f67 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -2,7 +2,7 @@ from datetime import datetime from syncano.exceptions import SyncanoDoesNotExist, SyncanoRequestError, SyncanoValueError -from syncano.models import CodeBox, CodeBoxTrace, Instance, Object, Webhook, WebhookTrace, User +from syncano.models import CodeBox, CodeBoxTrace, Instance, Object, User, Webhook, WebhookTrace try: from unittest import mock From 4d6a910da97d30d90e6297b6e6f6260a5d7895cb Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 18 Nov 2015 17:08:10 +0100 Subject: [PATCH 181/558] [LIB-154] correct test for required fields; --- tests/integration_test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index dbbda95..999d3e1 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -7,7 +7,7 @@ import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError -from syncano.models import Class, CodeBox, Instance, Object, Webhook +from syncano.models import Class, CodeBox, Instance, Object, Webhook, registry class IntegrationTest(unittest.TestCase): @@ -290,6 +290,7 @@ def tearDownClass(cls): def test_required_fields(self): with self.assertRaises(SyncanoValueError): + registry.clear_instance_name() list(self.model.please.all()) def test_list(self): @@ -362,6 +363,7 @@ def tearDownClass(cls): def test_required_fields(self): with self.assertRaises(SyncanoValueError): + registry.clear_instance_name() list(self.model.please.all()) def test_list(self): From 1d4f29717a219090696f807b79cb8429d33b3d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 19 Nov 2015 17:09:05 +0100 Subject: [PATCH 182/558] [LIB-262] add missing description field on ApiKey model; --- syncano/models/instances.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 782f7d5..b5e59f6 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -68,6 +68,7 @@ class ApiKey(Model): ] api_key = fields.StringField(read_only=True, required=False) + description = fields.StringField(required=False) allow_user_create = fields.BooleanField(required=False, default=False) ignore_acl = fields.BooleanField(required=False, default=False) links = fields.HyperlinkedField(links=LINKS) From 06219b0b90d7de2d3ccdf50497a253522e8da3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 19 Nov 2015 20:18:31 +0100 Subject: [PATCH 183/558] [LIB-261] move default_connection creation to the connect method; store defaul_connection in registry; allow now to refresh the connection; --- syncano/__init__.py | 5 +++-- syncano/connection.py | 8 +++----- syncano/models/registry.py | 10 ++++++++++ tests/test_connection.py | 19 ++++++++----------- tests/test_models.py | 3 ++- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index f70e6ba..a1e5c20 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -73,10 +73,11 @@ def connect(*args, **kwargs): # OR connection = syncano.connect(user_key='', api_key='', instance_name='') """ - from syncano.connection import default_connection + from syncano.connection import DefaultConnection from syncano.models import registry - default_connection.open(*args, **kwargs) + registry.set_default_connection(DefaultConnection()) + registry.connection.open(*args, **kwargs) instance = kwargs.get('instance_name', INSTANCE) if instance is not None: diff --git a/syncano/connection.py b/syncano/connection.py index 5cfd51a..af27976 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -5,6 +5,7 @@ import six import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError +from syncano.models.registry import registry if six.PY3: from urllib.parse import urljoin @@ -12,7 +13,7 @@ from urlparse import urljoin -__all__ = ['default_connection', 'Connection', 'ConnectionMixin'] +__all__ = ['Connection', 'ConnectionMixin'] def is_success(code): @@ -48,9 +49,6 @@ def open(self, *args, **kwargs): return connection -default_connection = DefaultConnection() - - class Connection(object): """Base connection class. @@ -385,7 +383,7 @@ def __init__(self, *args, **kwargs): @property def connection(self): # Sometimes someone will not use super - return getattr(self, '_connection', None) or default_connection() + return getattr(self, '_connection', None) or registry.connection() @connection.setter def connection(self, value): diff --git a/syncano/models/registry.py b/syncano/models/registry.py index 805b4b3..4a98318 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -14,6 +14,7 @@ def __init__(self, models=None): self.patterns = [] self._pending_lookups = {} self.last_used_instance = None + self._default_connection = None def __str__(self): return 'Registry: {0}'.format(', '.join(self.models)) @@ -87,4 +88,13 @@ def clear_instance_name(self): self.last_used_instance = None self.set_default_instance(None) + def set_default_connection(self, default_connection): + self._default_connection = default_connection + + @property + def connection(self): + if not self._default_connection: + raise Exception('Set the default connection first.') + return self._default_connection + registry = Registry() diff --git a/tests/test_connection.py b/tests/test_connection.py index 7edfd9d..0a591a9 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -2,8 +2,9 @@ from urlparse import urljoin from syncano import connect, connect_instance -from syncano.connection import Connection, ConnectionMixin, default_connection +from syncano.connection import Connection, ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValueError +from syncano.models.registry import registry try: from unittest import mock @@ -13,21 +14,17 @@ class ConnectTestCase(unittest.TestCase): - @mock.patch('syncano.models.registry') - @mock.patch('syncano.connection.default_connection.open') - def test_connect(self, open_mock, registry_mock): - registry_mock.return_value = registry_mock - - self.assertFalse(registry_mock.called) + @mock.patch('syncano.connection.DefaultConnection.open') + def test_connect(self, open_mock): self.assertFalse(open_mock.called) connection = connect(1, 2, 3, a=1, b=2, c=3) open_mock.assert_called_once_with(1, 2, 3, a=1, b=2, c=3) self.assertTrue(open_mock.called) - self.assertEqual(connection, registry_mock) + self.assertEqual(connection, registry) - @mock.patch('syncano.connection.default_connection.open') + @mock.patch('syncano.models.registry.connection.open') @mock.patch('syncano.models.registry') @mock.patch('syncano.INSTANCE') def test_env_instance(self, instance_mock, registry_mock, *args): @@ -298,7 +295,7 @@ def test_successful_authentication(self, make_request): class DefaultConnectionTestCase(unittest.TestCase): def setUp(self): - self.connection = default_connection + self.connection = registry.connection self.connection._connection = None def test_call(self): @@ -324,7 +321,7 @@ class ConnectionMixinTestCase(unittest.TestCase): def setUp(self): self.mixin = ConnectionMixin() - @mock.patch('syncano.connection.default_connection') + @mock.patch('syncano.models.registry._default_connection') def test_getter(self, default_connection_mock): default_connection_mock.return_value = default_connection_mock diff --git a/tests/test_models.py b/tests/test_models.py index 043d047..d63f900 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,7 @@ import unittest from syncano.exceptions import SyncanoValidationError -from syncano.models import Instance +from syncano.models import Instance, registry try: from unittest import mock @@ -13,6 +13,7 @@ class ModelTestCase(unittest.TestCase): def setUp(self): self.model = Instance() + registry.connection.open() def test_init(self): self.assertTrue(hasattr(self.model, '_raw_data')) From 620e35b3d8cdcb017bb1b81ebfa30b977775f615 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 20 Nov 2015 10:43:40 +0100 Subject: [PATCH 184/558] [LIB-154] correct imports --- syncano/connection.py | 2 +- tests/test_classes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index af27976..111712e 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -5,7 +5,6 @@ import six import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError -from syncano.models.registry import registry if six.PY3: from urllib.parse import urljoin @@ -383,6 +382,7 @@ def __init__(self, *args, **kwargs): @property def connection(self): # Sometimes someone will not use super + from syncano.models.registry import registry # TODO: refactor this; return getattr(self, '_connection', None) or registry.connection() @connection.setter diff --git a/tests/test_classes.py b/tests/test_classes.py index 64f5930..8c89579 100644 --- a/tests/test_classes.py +++ b/tests/test_classes.py @@ -122,7 +122,7 @@ def test_get_class_schema(self, get_mock): @mock.patch('syncano.models.Object.get_class_schema') @mock.patch('syncano.models.manager.registry.get_model_by_name') @mock.patch('syncano.models.Object.get_subclass_name') - @mock.patch('syncano.connection.default_connection') + @mock.patch('syncano.models.registry._default_connection') @mock.patch('syncano.models.manager.Manager.serialize') def test_get_subclass_model(self, serialize_mock, default_connection_mock, get_subclass_name_mock, get_model_by_name_mock, get_class_schema_mock, create_subclass_mock): From 0fb69ac8977b189c65825b7c0712a1dc373b46b4 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 20 Nov 2015 12:59:54 +0100 Subject: [PATCH 185/558] [LIB-196] quick fix for social auth in --- syncano/connection.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index af27976..68f9d1a 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -65,7 +65,7 @@ class Connection(object): CONTENT_TYPE = 'application/json' - AUTH_SUFFIX = 'v1/account/auth' + AUTH_SUFFIX = 'v1/account/auth/' SOCIAL_AUTH_SUFFIX = AUTH_SUFFIX + '{social_backend}/' USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' @@ -164,7 +164,7 @@ def build_params(self, params): 'X-API-KEY': self.api_key }) elif self.api_key and 'Authorization' not in params['headers']: - params['headers']['Authorization'] = 'token {}'.format(self.token if self.is_social else self.api_key) + params['headers']['Authorization'] = 'token {}'.format(self.api_key) # We don't need to check SSL cert in DEBUG mode if syncano.DEBUG or not self.verify_ssl: @@ -349,6 +349,7 @@ def authenticate_admin(self, **kwargs): if self.is_social: request_args = self.validate_params(kwargs, self.SOCIAL_LOGIN_PARAMS) + request_args['access_token'] = request_args.pop('token') # core expects a access_token field; else: request_args = self.validate_params(kwargs, self.LOGIN_PARAMS) From c4004b44335ab2e96bbce3ae6348188bb35a27a7 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 20 Nov 2015 13:17:34 +0100 Subject: [PATCH 186/558] [LIB-196] quick fix for socials in LIB --- syncano/connection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 68f9d1a..0dca161 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -5,7 +5,6 @@ import six import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError -from syncano.models.registry import registry if six.PY3: from urllib.parse import urljoin @@ -65,8 +64,8 @@ class Connection(object): CONTENT_TYPE = 'application/json' - AUTH_SUFFIX = 'v1/account/auth/' - SOCIAL_AUTH_SUFFIX = AUTH_SUFFIX + '{social_backend}/' + AUTH_SUFFIX = 'v1/account/auth' + SOCIAL_AUTH_SUFFIX = AUTH_SUFFIX + '/{social_backend}/' USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' @@ -353,7 +352,7 @@ def authenticate_admin(self, **kwargs): else: request_args = self.validate_params(kwargs, self.LOGIN_PARAMS) - + print(request_args) response = self.make_request('POST', self.AUTH_SUFFIX, data=request_args) self.api_key = response.get('account_key') return self.api_key @@ -384,6 +383,7 @@ def __init__(self, *args, **kwargs): @property def connection(self): # Sometimes someone will not use super + from syncano.models.registry import registry return getattr(self, '_connection', None) or registry.connection() @connection.setter From a04899f88d072132c4f76df76fe6581e18932cd4 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 20 Nov 2015 13:40:15 +0100 Subject: [PATCH 187/558] [LIB-196] quick fix for socials in LIB --- syncano/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 0dca161..4e05612 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -352,7 +352,7 @@ def authenticate_admin(self, **kwargs): else: request_args = self.validate_params(kwargs, self.LOGIN_PARAMS) - print(request_args) + response = self.make_request('POST', self.AUTH_SUFFIX, data=request_args) self.api_key = response.get('account_key') return self.api_key From 6b27980b552f32230d9efcc431c4de3147f66c38 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 24 Nov 2015 11:29:34 +0100 Subject: [PATCH 188/558] [LIB-278] add count to object manager --- syncano/models/incentives.py | 1 + syncano/models/manager.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index f48eefb..a012ab3 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -48,6 +48,7 @@ class CodeBox(Model): {'display_name': 'nodejs', 'value': 'nodejs'}, {'display_name': 'python', 'value': 'python'}, {'display_name': 'ruby', 'value': 'ruby'}, + {'display_name': 'golang', 'value': 'golang'}, ) label = fields.StringField(max_length=80) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index e32aed8..5866cdb 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -742,10 +742,32 @@ class for :class:`~syncano.models.base.Object` model. 'eq', 'neq', 'exists', 'in', ] + def __init__(self): + super(ObjectManager, self).__init__() + self.query = { + 'include_count': True, + } + def serialize(self, data, model=None): model = model or self.model.get_subclass_model(**self.properties) return super(ObjectManager, self).serialize(data, model) + @clone + def count(self): + """ + Return the queryset count; + + Usage:: + 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; + """ + self.method = 'GET' + self.query.update({'page_size': 0}) + response = self.request() + return response['objects_count'] + @clone def filter(self, **kwargs): """ From 6b5f6628808e79be278941af3e6ab5d929d9a953 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 24 Nov 2015 11:38:32 +0100 Subject: [PATCH 189/558] [LIB-278] move include_count paramter to manager count method --- syncano/models/manager.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 5866cdb..6ab27b5 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -742,12 +742,6 @@ class for :class:`~syncano.models.base.Object` model. 'eq', 'neq', 'exists', 'in', ] - def __init__(self): - super(ObjectManager, self).__init__() - self.query = { - 'include_count': True, - } - def serialize(self, data, model=None): model = model or self.model.get_subclass_model(**self.properties) return super(ObjectManager, self).serialize(data, model) @@ -764,7 +758,10 @@ def count(self): :return: The integer with estimated objects count; """ self.method = 'GET' - self.query.update({'page_size': 0}) + self.query.update({ + 'include_count': True, + 'page_size': 0, + }) response = self.request() return response['objects_count'] From 9daaa142995db7a0ca671e6ac17bf839c1059def Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 30 Nov 2015 15:00:17 +0100 Subject: [PATCH 190/558] [release-update] version and credits update --- syncano/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index a1e5c20..708d5b5 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,10 +2,13 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.6' -__author__ = 'Daniel Kopka' -__license__ = 'MIT' +__version__ = '4.0.7' +__author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" +__credits__ = ["Daniel Kopka", + "Michal Kobus", + "Sebastian Opalczynski"] __copyright__ = 'Copyright 2015 Syncano' +__license__ = 'MIT' env_loglevel = os.getenv('SYNCANO_LOGLEVEL', 'INFO') loglevel = getattr(logging, env_loglevel.upper(), None) From 8f232fe951d1856f465c77d0619292d24e646e6b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 1 Dec 2015 10:54:00 +0100 Subject: [PATCH 191/558] [LIB-315] add GCMDevice model to the python lib, supprots read, create and delete; --- syncano/models/base.py | 1 + syncano/models/push_notification.py | 68 +++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 syncano/models/push_notification.py diff --git a/syncano/models/base.py b/syncano/models/base.py index 334539e..7bc3d35 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -8,3 +8,4 @@ from .data_views import * # NOQA from .incentives import * # NOQA from .traces import * # NOQA +from .push_notification import * # NOQA \ No newline at end of file diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py new file mode 100644 index 0000000..d73b1bd --- /dev/null +++ b/syncano/models/push_notification.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +from . import fields +from .base import Instance, Model + + +class GCMDevice(Model): + """ + Model which handles the Google Cloud Message Device. + CORE supports only Create, Delete and Read; + + Usage:: + + Create a new Device: + + gcm_device = GCMDevice( + label='dummy label', + registration_id=86152312314401555, + user_id=u.id, + device_id='32132132132131', + ) + + gcm_device.save() + + Note:: + + another save on the same object will always fail (altering the Device data is currently not possible); + + Delete a Device: + + gcm_device.delete() + + Read a Device data: + + gcm_device = GCMDevice.please.get(registration_id=86152312314401554) + + """ + + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + registration_id = fields.StringField(max_length=512, unique=True, primary_key=True) + device_id = fields.StringField(required=False) + is_active = fields.BooleanField(default=True) + label = fields.StringField(max_length=80) + user_id = fields.IntegerField(required=False) + + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'post', 'get'], + 'path': '/push_notifications/gcm/devices/{registration_id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/push_notifications/gcm/devices/', + } + } + + def is_new(self): + if self.created_at is None: + return True + return False From f55edddea4a5c1be88e7d65dc129a348c21e0e29 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 1 Dec 2015 12:15:43 +0100 Subject: [PATCH 192/558] [LIB-315] add possibility for creating gcm messages; --- syncano/__init__.py | 1 + syncano/models/fields.py | 14 +++++- syncano/models/push_notification.py | 69 +++++++++++++++++++++++++++-- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index a1e5c20..e431f02 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -29,6 +29,7 @@ PASSWORD = os.getenv('SYNCANO_PASSWORD') APIKEY = os.getenv('SYNCANO_APIKEY') INSTANCE = os.getenv('SYNCANO_INSTANCE') +PUSH_ENV = os.getenv('SYNCANO_PUSH_ENV', 'production') def connect(*args, **kwargs): diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 6c9f2f6..09be6dd 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -4,7 +4,7 @@ import six import validictory -from syncano import logger +from syncano import PUSH_ENV, logger from syncano.exceptions import SyncanoFieldError, SyncanoValueError from syncano.utils import force_text @@ -579,6 +579,18 @@ def to_native(self, value): return super(SchemaField, self).to_native(value) +class PushJSONField(JSONField): + def to_native(self, value): + if value is None: + return + + if not isinstance(value, six.string_types): + value.update({ + 'environment': PUSH_ENV, + }) + value = json.dumps(value) + return value + MAPPING = { 'string': StringField, 'text': StringField, diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index d73b1bd..276e53b 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -14,10 +14,10 @@ class GCMDevice(Model): Create a new Device: gcm_device = GCMDevice( - label='dummy label', + label='example label', registration_id=86152312314401555, user_id=u.id, - device_id='32132132132131', + device_id='10000000001', ) gcm_device.save() @@ -53,7 +53,7 @@ class Meta: parent = Instance endpoints = { 'detail': { - 'methods': ['delete', 'post', 'get'], + 'methods': ['delete', 'get'], 'path': '/push_notifications/gcm/devices/{registration_id}/', }, 'list': { @@ -66,3 +66,66 @@ def is_new(self): if self.created_at is None: return True return False + + +class GCMMessage(Model): + """ + Model which handles the Google Cloud Messaging Message. + Only creating and reading is allowed. + + Usage:: + + Create + The content parameter is passed as-it-is to the GCM server; Base checking is made on syncano CORE; + + message = GCMMessage( + content={ + 'registration_ids': [gcm_device.registration_id], # maximum 1000 elements; + 'data': { + 'example_data_one': 1, + 'example_data_two': 2, + } + } + ) + message.save() + + Note:: + Every save after initial one will raise an error; + + Read + gcm_message = GCMMessage.please.get(id=1) + + Debugging: + gcm_message.status - on of the (scheduled, error, partially_delivered, delivered) + gcm_message.result - a result from GCM server; + + Note:: + The altering of existing Message is not possible. It also not possible to delete message. + + """ + STATUS_CHOICES = ( + {'display_name': 'scheduled', 'value': 0}, + {'display_name': 'error', 'value': 1}, + {'display_name': 'partially_delivered', 'value': 2}, + {'display_name': 'delivered', 'value': 3}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True) + content = fields.PushJSONField(default={}) + result = fields.JSONField(default={}, read_only=True) + + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'get'], + 'path': '/push_notifications/gcm/messages/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/push_notifications/gcm/messages/', + } + } From 012c112110235d9d6bd2482d3da5d2d8be0b65e7 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 1 Dec 2015 12:40:29 +0100 Subject: [PATCH 193/558] [LIB-315] Small fixes after qa: add new line and change is_new method --- syncano/models/fields.py | 1 + syncano/models/push_notification.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 09be6dd..cf3029a 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -591,6 +591,7 @@ def to_native(self, value): value = json.dumps(value) return value + MAPPING = { 'string': StringField, 'text': StringField, diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index 276e53b..5ae9362 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -63,9 +63,7 @@ class Meta: } def is_new(self): - if self.created_at is None: - return True - return False + return self.created_at is None class GCMMessage(Model): From 853d7f8d356fa14e2fc7d1f38b546ab126d4a3f7 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 1 Dec 2015 14:06:01 +0100 Subject: [PATCH 194/558] [LIB-318] Add handling of apns device and apns push message; --- syncano/models/archetypes.py | 6 + syncano/models/push_notification.py | 170 +++++++++++++++++++++++----- 2 files changed, 146 insertions(+), 30 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 95be2db..70b31e4 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -18,6 +18,7 @@ def __new__(cls, name, bases, attrs): super_new = super(ModelMetaclass, cls).__new__ parents = [b for b in bases if isinstance(b, ModelMetaclass)] + abstracts = [b for b in bases if hasattr(b, 'Meta') and hasattr(b.Meta, 'abstract') and b.Meta.abstract] if not parents: return super_new(cls, name, bases, attrs) @@ -37,6 +38,11 @@ def __new__(cls, name, bases, attrs): for n, v in six.iteritems(attrs): new_class.add_to_class(n, v) + for abstract in abstracts: + for n, v in abstract.__dict__.iteritems(): + if isinstance(v, fields.Field) or n in ['LINKS']: # extend this condition if required; + new_class.add_to_class(n, v) + if not meta.pk: pk_field = fields.IntegerField(primary_key=True, read_only=True, required=False) diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index 5ae9362..685cd31 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -4,7 +4,31 @@ from .base import Instance, Model -class GCMDevice(Model): +class DeviceBase(object): + """ + Base abstract class for GCM and APNS Devices; + """ + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + registration_id = fields.StringField(max_length=512, unique=True, primary_key=True) + device_id = fields.StringField(required=False) + is_active = fields.BooleanField(default=True) + label = fields.StringField(max_length=80) + user_id = fields.IntegerField(required=False) + + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + abstract = True + + def is_new(self): + return self.created_at is None + + +class GCMDevice(DeviceBase, Model): """ Model which handles the Google Cloud Message Device. CORE supports only Create, Delete and Read; @@ -36,37 +60,88 @@ class GCMDevice(Model): """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'get'], + 'path': '/push_notifications/gcm/devices/{registration_id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/push_notifications/gcm/devices/', + } + } - registration_id = fields.StringField(max_length=512, unique=True, primary_key=True) - device_id = fields.StringField(required=False) - is_active = fields.BooleanField(default=True) - label = fields.StringField(max_length=80) - user_id = fields.IntegerField(required=False) - created_at = fields.DateTimeField(read_only=True, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) +class APNSDevice(DeviceBase, Model): + """ + Model which handles the Apple Push Notification Server Device. + CORE supports only Create, Delete and Read; + + Usage:: + + Create + apns_device = APNSDevice( + label='example label', + registration_id='4719084371920471208947120984731208947910827409128470912847120894', + user_id=u.id, + device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a', + ) + apns_device.save() + + Note:: + + another save on the same object will always fail (altering the Device data is currently not possible); + + Also note the different format (from GCM) of registration_id required by APNS; the device_id have different + format too. + Read + apns_device = + APNSDevice.please.get(registration_id='4719084371920471208947120984731208947910827409128470912847120894') + + Delete + apns_device.delete() + + """ class Meta: parent = Instance endpoints = { 'detail': { 'methods': ['delete', 'get'], - 'path': '/push_notifications/gcm/devices/{registration_id}/', + 'path': '/push_notifications/apns/devices/{registration_id}/', }, 'list': { 'methods': ['get'], - 'path': '/push_notifications/gcm/devices/', + 'path': '/push_notifications/apns/devices/', } } - def is_new(self): - return self.created_at is None + +class MessageBase(object): + """ + Base abstract class for GCM and APNS Messages; + """ + STATUS_CHOICES = ( + {'display_name': 'scheduled', 'value': 0}, + {'display_name': 'error', 'value': 1}, + {'display_name': 'partially_delivered', 'value': 2}, + {'display_name': 'delivered', 'value': 3}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True) + content = fields.PushJSONField(default={}) + result = fields.JSONField(default={}, read_only=True) + + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + abstract = True -class GCMMessage(Model): +class GCMMessage(MessageBase, Model): """ Model which handles the Google Cloud Messaging Message. Only creating and reading is allowed. @@ -74,7 +149,7 @@ class GCMMessage(Model): Usage:: Create - The content parameter is passed as-it-is to the GCM server; Base checking is made on syncano CORE; + message = GCMMessage( content={ @@ -87,6 +162,9 @@ class GCMMessage(Model): ) message.save() + The data parameter is passed as-it-is to the GCM server; Base checking is made on syncano CORE; + For more details read the GCM documentation; + Note:: Every save after initial one will raise an error; @@ -101,19 +179,6 @@ class GCMMessage(Model): The altering of existing Message is not possible. It also not possible to delete message. """ - STATUS_CHOICES = ( - {'display_name': 'scheduled', 'value': 0}, - {'display_name': 'error', 'value': 1}, - {'display_name': 'partially_delivered', 'value': 2}, - {'display_name': 'delivered', 'value': 3}, - ) - - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True) - content = fields.PushJSONField(default={}) - result = fields.JSONField(default={}, read_only=True) - - created_at = fields.DateTimeField(read_only=True, required=False) - updated_at = fields.DateTimeField(read_only=True, required=False) class Meta: parent = Instance @@ -127,3 +192,48 @@ class Meta: 'path': '/push_notifications/gcm/messages/', } } + + +class APNSMessage(MessageBase, Model): + """ + Model which handles the Apple Push Notification Server Message. + Only creating and reading is allowed. + + Usage:: + + Create + apns_message = APNSMessage( + content={ + 'registration_ids': [gcm_device.registration_id], + 'aps': {'alert': 'test alert'}, + } + ) + + apns_message.save() + + The 'aps' data is send 'as-it-is' to APNS, some validation is made on syncano CORE; + For more details read the APNS documentation; + + Note:: + Every save after initial one will raise an error; + + Read + apns_message = APNSMessage.please.get(id=1) + + Debugging + apns_message.status - one of the following: scheduled, error, partially_delivered, delivered; + apns_message.result - a result from APNS server; + + """ + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'get'], + 'path': '/push_notifications/apns/messages/{id}/', + }, + 'list': { + 'methods': ['get'], + 'path': '/push_notifications/apns/messages/', + } + } From 3abeebeb25e5b710c75f33664583459f9ab39f24 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 1 Dec 2015 15:09:38 +0100 Subject: [PATCH 195/558] [LIB-318] add test for gcm and apns devices and messages --- tests/integration_test_push.py | 75 +++++++++++++++++++++ tests/test_push.py | 120 +++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 tests/integration_test_push.py create mode 100644 tests/test_push.py diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py new file mode 100644 index 0000000..a6d67ff --- /dev/null +++ b/tests/integration_test_push.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +from syncano.exceptions import SyncanoRequestError +from syncano.models import APNSDevice, APNSMessage, GCMDevice, GCMMessage +from tests.integration_test import InstanceMixin, IntegrationTest + + +class PushNotificationTest(InstanceMixin, IntegrationTest): + def test_gcm_device(self): + device = GCMDevice( + instance_name=self.instance.name, + label='example label', + registration_id=86152312314401555, + device_id='10000000001', + ) + self._test_device(device, GCMDevice.please) + + def test_apns_device(self): + device = APNSDevice( + instance_name=self.instance.name, + label='example label', + registration_id='4719084371920471208947120984731208947910827409128470912847120894', + device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a', + ) + + self._test_device(device, APNSDevice.please) + + def test_gcm_message(self): + message = GCMMessage( + instance_name=self.instance.name, + content={ + 'registration_ids': ['TESTIDREGISRATION', ], + 'data': { + 'param1': 'test' + } + } + ) + + self._test_message(message, GCMMessage.please) + + def test_apns_message(self): + message = APNSMessage( + instance_name=self.instance.name, + content={ + 'registration_ids': ['TESTIDREGISRATION', ], + 'aps': {'alert': 'semo example label'} + } + ) + + self._test_message(message, APNSMessage.please) + + def _test_device(self, device, manager): + + self.assertFalse(manager.all(instance_name=self.instance.name)) + + device.save() + + self.assertEqual(len(list(manager.all(instance_name=self.instance.name,))), 1) + + # test get: + device_ = manager.get(instance_name=self.instance.name, registration_id=device.registration_id) + + self.assertEqual(device_.label, device.label) + self.assertEqual(device_.registration_id, device.registration_id) + self.assertEqual(device_.device_id, device.device_id) + + device.delete() + + self.assertFalse(manager.all(instance_name=self.instance.name)) + + def _test_message(self, message, manager): + self.assertFalse(manager.all(instance_name=self.instance.name)) + + with self.assertRaises(SyncanoRequestError): + # unable to save because of lack of API key; + message.save() diff --git a/tests/test_push.py b/tests/test_push.py new file mode 100644 index 0000000..a17316f --- /dev/null +++ b/tests/test_push.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- + +import unittest +from datetime import datetime + +from mock import mock +from syncano.models import APNSDevice, APNSMessage, GCMDevice, GCMMessage + + +class CodeBoxTestCase(unittest.TestCase): + + @mock.patch('syncano.models.GCMDevice._get_connection') + def test_gcm_device(self, connection_mock): + model = GCMDevice( + instance_name='test', + label='example label', + registration_id=86152312314401555, + device_id='10000000001', + ) + + connection_mock.return_value = connection_mock + connection_mock.request.return_value = {'registration_id': 86152312314401555} + + self.assertFalse(connection_mock.called) + self.assertFalse(connection_mock.request.called) + + model.save() + + self.assertTrue(connection_mock.called) + self.assertTrue(connection_mock.request.called) + + connection_mock.assert_called_once_with() + connection_mock.request.assert_called_once_with( + u'POST', u'/v1/instances/test/push_notifications/gcm/devices/', + data={"registration_id": u'86152312314401555', "device_id": "10000000001", "is_active": True, + "label": "example label"} + ) + model.created_at = datetime.now() # to Falsify is_new() + model.delete() + connection_mock.request.assert_called_with( + u'DELETE', u'/v1/instances/test/push_notifications/gcm/devices/86152312314401555/' + ) + + @mock.patch('syncano.models.APNSDevice._get_connection') + def test_apns_device(self, connection_mock): + # just mock test - values here should be different; + model = APNSDevice( + instance_name='test', + label='example label', + registration_id=86152312314401555, + device_id='10000000001', + ) + + connection_mock.return_value = connection_mock + connection_mock.request.return_value = {'registration_id': 86152312314401555} + + self.assertFalse(connection_mock.called) + self.assertFalse(connection_mock.request.called) + + model.save() + + self.assertTrue(connection_mock.called) + self.assertTrue(connection_mock.request.called) + + connection_mock.assert_called_once_with() + connection_mock.request.assert_called_once_with( + u'POST', u'/v1/instances/test/push_notifications/apns/devices/', + data={"registration_id": u'86152312314401555', "device_id": "10000000001", "is_active": True, + "label": "example label"} + ) + + model.created_at = datetime.now() # to Falsify is_new() + model.delete() + connection_mock.request.assert_called_with( + u'DELETE', u'/v1/instances/test/push_notifications/apns/devices/86152312314401555/' + ) + + @mock.patch('syncano.models.GCMMessage._get_connection') + def test_gcm_message(self, connection_mock): + model = GCMMessage( + instance_name='test', + content={'data': 'some data'} + ) + connection_mock.return_value = connection_mock + + self.assertFalse(connection_mock.called) + self.assertFalse(connection_mock.request.called) + + model.save() + + self.assertTrue(connection_mock.called) + self.assertTrue(connection_mock.request.called) + + connection_mock.assert_called_once_with() + connection_mock.request.assert_called_once_with( + u'POST', u'/v1/instances/test/push_notifications/gcm/messages/', + data={'content': '{"environment": "production", "data": "some data"}'} + ) + + @mock.patch('syncano.models.APNSMessage._get_connection') + def test_apns_message(self, connection_mock): + model = APNSMessage( + instance_name='test', + content={'data': 'some data'} + ) + connection_mock.return_value = connection_mock + + self.assertFalse(connection_mock.called) + self.assertFalse(connection_mock.request.called) + + model.save() + + self.assertTrue(connection_mock.called) + self.assertTrue(connection_mock.request.called) + + connection_mock.assert_called_once_with() + connection_mock.request.assert_called_once_with( + u'POST', u'/v1/instances/test/push_notifications/apns/messages/', + data={'content': '{"environment": "production", "data": "some data"}'} + ) From 7635e29fc5c100dd6bb56afd06d672fb9afce238 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Dec 2015 08:45:30 +0100 Subject: [PATCH 196/558] [LIB-318] fix after QA: remove choice field from status, correct getting abstract models in archetypes; --- syncano/models/archetypes.py | 2 +- syncano/models/push_notification.py | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 70b31e4..88277a5 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -18,7 +18,7 @@ def __new__(cls, name, bases, attrs): super_new = super(ModelMetaclass, cls).__new__ parents = [b for b in bases if isinstance(b, ModelMetaclass)] - abstracts = [b for b in bases if hasattr(b, 'Meta') and hasattr(b.Meta, 'abstract') and b.Meta.abstract] + abstracts = [b for b in bases if hasattr(b, 'Meta') and getattr(b.Meta, 'abstract', None)] if not parents: return super_new(cls, name, bases, attrs) diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index 685cd31..0693f9d 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -123,14 +123,8 @@ class MessageBase(object): """ Base abstract class for GCM and APNS Messages; """ - STATUS_CHOICES = ( - {'display_name': 'scheduled', 'value': 0}, - {'display_name': 'error', 'value': 1}, - {'display_name': 'partially_delivered', 'value': 2}, - {'display_name': 'delivered', 'value': 3}, - ) - status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True) + status = fields.StringField(read_only=True) content = fields.PushJSONField(default={}) result = fields.JSONField(default={}, read_only=True) From f8fb0f9c08a7655ce24a68d993d4277221d89031 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 13:58:16 +0100 Subject: [PATCH 197/558] [LIB-274] Start to implement custom response handling; --- syncano/models/custom_response.py | 62 +++++++++++++++++++++++++++++++ syncano/models/traces.py | 9 +++-- 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 syncano/models/custom_response.py diff --git a/syncano/models/custom_response.py b/syncano/models/custom_response.py new file mode 100644 index 0000000..5f7c3f8 --- /dev/null +++ b/syncano/models/custom_response.py @@ -0,0 +1,62 @@ +import json + + +class CustomResponseHandler(object): + + def __init__(self): + # a content_type -> handler method dict + self.handlers = {} + + def register_handler(self, content_type, handler): + self.handlers[content_type] = handler + + def process_response(self, response): + content_type = self._find_content_type(response) + try: + return self.handlers[content_type](response) + except KeyError: + return self._default_handler(response) + + @staticmethod + def _find_content_type(response): + return response.get('response', {}).get('content_type') + + @staticmethod + def _default_handler(response): + if 'response' in response: + return response['response'] + if 'stdout' in response: + return response['stdout'] + + return response + + @staticmethod + def json_handler(response): + return json.loads(response['response']['content']) + + @staticmethod + def plain_handler(response): + return response['response']['content'] + +custom_response_handler = CustomResponseHandler() +custom_response_handler.register_handler('application/json', CustomResponseHandler.json_handler) +custom_response_handler.register_handler('text/plain', CustomResponseHandler.plain_handler) + + +class CustomResponseMixin(object): + + @property + def content(self): + return custom_response_handler.process_response(self.result) + + @property + def response_status(self): + return self.result.get('response', {}).get('status') + + @property + def error(self): + return self.result.get('stderr') + + @property + def content_type(self): + return self.result.get('response', {}).get('content_type') diff --git a/syncano/models/traces.py b/syncano/models/traces.py index 78a3212..4bcc61a 100644 --- a/syncano/models/traces.py +++ b/syncano/models/traces.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals +from .custom_response import CustomResponseMixin from . import fields from .base import Model from .incentives import CodeBox, Schedule, Trigger, Webhook -class CodeBoxTrace(Model): +class CodeBoxTrace(CustomResponseMixin, Model): """ :ivar status: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` @@ -26,7 +27,7 @@ class CodeBoxTrace(Model): status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) links = fields.HyperlinkedField(links=LINKS) executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) + result = fields.JSONField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) class Meta: @@ -119,7 +120,7 @@ class Meta: } -class WebhookTrace(Model): +class WebhookTrace(CustomResponseMixin, Model): """ :ivar status: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` @@ -140,7 +141,7 @@ class WebhookTrace(Model): status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) links = fields.HyperlinkedField(links=LINKS) executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.StringField(read_only=True, required=False) + result = fields.JSONField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) class Meta: From 1e878fdf4fd91cc5b1f4cbf1132bc7bd78ce0d71 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 13:59:50 +0100 Subject: [PATCH 198/558] [LIB-274] Isort...; --- syncano/models/traces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/traces.py b/syncano/models/traces.py index 4bcc61a..531c15f 100644 --- a/syncano/models/traces.py +++ b/syncano/models/traces.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals -from .custom_response import CustomResponseMixin from . import fields from .base import Model +from .custom_response import CustomResponseMixin from .incentives import CodeBox, Schedule, Trigger, Webhook From 1911ac1ac62562d14c1176812d20e099639da305 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 14:03:28 +0100 Subject: [PATCH 199/558] [LIB-274] Correct Webhook test; --- tests/test_incentives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_incentives.py b/tests/test_incentives.py index 6d013ed..ad38ed0 100644 --- a/tests/test_incentives.py +++ b/tests/test_incentives.py @@ -49,7 +49,7 @@ def test_run(self, connection_mock): connection_mock.request.return_value = { 'status': 'success', 'duration': 937, - 'result': '1', + 'result': 1, 'executed_at': '2015-03-16T11:52:14.172830Z' } From 8954975b7097f6ae916db07d71f0a4dca07515fd Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 14:05:47 +0100 Subject: [PATCH 200/558] [LIB-274] Correct Webhook test; --- tests/test_incentives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_incentives.py b/tests/test_incentives.py index ad38ed0..521e833 100644 --- a/tests/test_incentives.py +++ b/tests/test_incentives.py @@ -61,7 +61,7 @@ def test_run(self, connection_mock): self.assertIsInstance(result, WebhookTrace) self.assertEqual(result.status, 'success') self.assertEqual(result.duration, 937) - self.assertEqual(result.result, '1') + self.assertEqual(result.result, 1) self.assertIsInstance(result.executed_at, datetime) connection_mock.assert_called_once_with(x=1, y=2) From 99a7ebb47062183a6b8175862b91eba6a28dbf1b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 14:08:08 +0100 Subject: [PATCH 201/558] [LIB-274] Correct Webhook manager test; --- tests/test_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index 3392f67..724ce55 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -490,7 +490,7 @@ def test_run(self, clone_mock, filter_mock, request_mock): request_mock.return_value = { 'status': 'success', 'duration': 937, - 'result': '1', + 'result': 1, 'executed_at': '2015-03-16T11:52:14.172830Z' } @@ -501,7 +501,7 @@ def test_run(self, clone_mock, filter_mock, request_mock): self.assertIsInstance(result, WebhookTrace) self.assertEqual(result.status, 'success') self.assertEqual(result.duration, 937) - self.assertEqual(result.result, '1') + self.assertEqual(result.result, 1) self.assertIsInstance(result.executed_at, datetime) self.assertTrue(filter_mock.called) From 6d92431f66e7f9d13ddd1e874b0539d75a5af49d Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 14:13:28 +0100 Subject: [PATCH 202/558] [LIB-274] Correct codebox and webhooks integrations tests; --- tests/integration_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 999d3e1..1af88c9 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -337,7 +337,7 @@ def test_source_run(self): trace.reload() self.assertEquals(trace.status, 'success') - self.assertEquals(trace.result, u'{u\'stderr\': u\'\', u\'stdout\': u\'IntegrationTest\'}') + self.assertDictEqual(trace.result, {u'stderr': u'', u'stdout': u'IntegrationTest'}) codebox.delete() @@ -388,5 +388,5 @@ def test_codebox_run(self): trace = webhook.run() self.assertEquals(trace.status, 'success') - self.assertEquals(trace.result, u'{u\'stderr\': u\'\', u\'stdout\': u\'IntegrationTest\'}') + self.assertDictEqual(trace.result, {u'stderr': u'', u'stdout': u'IntegrationTest'}) webhook.delete() From 4e7156779f90dac97cafd7bf0dedc42689d111dc Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 17:09:24 +0100 Subject: [PATCH 203/558] [LIB-274] add tests, finalize the code structure; --- syncano/models/custom_response.py | 71 ++++++++++++++++++++++++++++--- tests/integration_test.py | 41 ++++++++++++++++++ tests/test_custom_response.py | 33 ++++++++++++++ 3 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 tests/test_custom_response.py diff --git a/syncano/models/custom_response.py b/syncano/models/custom_response.py index 5f7c3f8..adcbebb 100644 --- a/syncano/models/custom_response.py +++ b/syncano/models/custom_response.py @@ -1,13 +1,63 @@ import json +from syncano.exceptions import SyncanoException + class CustomResponseHandler(object): + """ + A helper class which allows to define and maintain custom response handlers. + + Consider an example: + CodeBox code: + + >> set_response(HttpResponse(status_code=200, content='{"one": 1}', content_type='application/json')) + + When suitable CodeBoxTrace is used: + + >> trace = CodeBoxTrace.please.get(id=, codebox_id=) + + Then trace object will have a content attribute, which will be a dict created from json (simple: json.loads under + the hood); + + So this is possible: + + >> trace.content['one'] + + And the trace.content is equal to: + >> {'one': 1} + + The handler can be easily overwrite: + + def custom_handler(response): + return json.loads(response['response']['content'])['one'] + trace.response_handler.overwrite_handler('application/json', custom_handler) + + or globally: + + CodeBoxTrace.response_handler.overwrite_handler('application/json', custom_handler) + + Then trace.content is equal to: + >> 1 + + Currently supported content_types (but any handler can be defined): + * application/json + * text/plain + + """ def __init__(self): - # a content_type -> handler method dict self.handlers = {} + self.register_handler('application/json', self.json_handler) + self.register_handler('plain/text', self.plain_handler) def register_handler(self, content_type, handler): + if content_type in self.handlers: + raise SyncanoException('Handler "{}" already defined. User overwrite_handler instead.'.format(content_type)) + self.handlers[content_type] = handler + + def overwrite_handler(self, content_type, handler): + if content_type not in self.handlers: + raise SyncanoException('Handler "{}" not defined. User register_handler instead.'.format(content_type)) self.handlers[content_type] = handler def process_response(self, response): @@ -38,19 +88,26 @@ def json_handler(response): def plain_handler(response): return response['response']['content'] -custom_response_handler = CustomResponseHandler() -custom_response_handler.register_handler('application/json', CustomResponseHandler.json_handler) -custom_response_handler.register_handler('text/plain', CustomResponseHandler.plain_handler) - class CustomResponseMixin(object): + """ + A mixin which extends the CodeBox and Webhook traces (and any other Model - if used) with following fields: + * content - This is the response data if set_response is used in CodeBox code, otherwise it is the 'stdout' field; + * content_type - The content_type specified by the user in CodeBox code; + * status_code - The status_code specified by the user in CodeBox code; + * error - An error which can occur when code is executed: the stderr response field; + + To process the content based on content_type this Mixin uses the CustomResponseHandler - see the docs there. + """ + + response_handler = CustomResponseHandler() @property def content(self): - return custom_response_handler.process_response(self.result) + return self.response_handler.process_response(self.result) @property - def response_status(self): + def status_code(self): return self.result.get('response', {}).get('status') @property diff --git a/tests/integration_test.py b/tests/integration_test.py index 1af88c9..b9ab323 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -341,6 +341,27 @@ def test_source_run(self): codebox.delete() + def test_custom_response_run(self): + codebox = self.model.please.create( + instance_name=self.instance.name, + label='cb%s' % self.generate_hash()[:10], + runtime_name='python', + source=""" +set_response(HttpResponse(status_code=200, content='{"one": 1}', content_type='application/json'))""" + ) + + trace = codebox.run() + while trace.status == 'pending': + sleep(1) + trace.reload() + + self.assertEquals(trace.status, 'success') + self.assertDictEqual(trace.content, {'one', 1}) + self.assertEqual(trace.contet_type, 'application/json') + self.assertEqual(trace.status_code, 200) + + codebox.delete() + class WebhookIntegrationTest(InstanceMixin, IntegrationTest): model = Webhook @@ -356,6 +377,14 @@ def setUpClass(cls): source='print "IntegrationTest"' ) + cls.custom_codebox = CodeBox.please.create( + instance_name=cls.instance.name, + label='cb%s' % cls.generate_hash()[:10], + runtime_name='python', + source=""" +set_response(HttpResponse(status_code=200, content='{"one": 1}', content_type='application/json'))""" + ) + @classmethod def tearDownClass(cls): cls.codebox.delete() @@ -390,3 +419,15 @@ def test_codebox_run(self): self.assertEquals(trace.status, 'success') self.assertDictEqual(trace.result, {u'stderr': u'', u'stdout': u'IntegrationTest'}) webhook.delete() + + def test_custom_codebox_run(self): + webhook = self.model.please.create( + instance_name=self.instance.name, + codebox=self.custom_codebox.id, + name='wh%s' % self.generate_hash()[:10], + ) + + trace = webhook.run() + self.assertEquals(trace.status, 'success') + self.assertDictEqual(trace.content, {'one': 1}) + webhook.delete() diff --git a/tests/test_custom_response.py b/tests/test_custom_response.py new file mode 100644 index 0000000..cba6fab --- /dev/null +++ b/tests/test_custom_response.py @@ -0,0 +1,33 @@ +import json +import unittest + +from syncano.models import CustomResponseHandler + + +class ObjectTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.json_data = json.dumps({'one': 1, 'two': 2}) + + def _wrap_data(self): + return {'response': {'content': self.json_data, 'content_type': 'application/json'}} + + def test_default_json_handler(self): + custom_handler = CustomResponseHandler() + processed_data = custom_handler.process_response(self._wrap_data()) + + self.assertDictEqual(processed_data, json.loads(self.json_data)) + + def test_custom_json_handler(self): + + def json_custom_handler(response): + # return only two + return json.loads(response['response']['content'])['two'] + + custom_handler = CustomResponseHandler() + custom_handler.overwrite_handler('application/json', json_custom_handler) + + processed_data = custom_handler.process_response(self._wrap_data()) + + self.assertEqual(processed_data, json.loads(self.json_data)['two']) From a3b30cc3d96872b522bb59f25d47da5bf28de949 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 17:10:06 +0100 Subject: [PATCH 204/558] [LIB-274] add tests, finalize the code structure; --- tests/test_custom_response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_custom_response.py b/tests/test_custom_response.py index cba6fab..eba7713 100644 --- a/tests/test_custom_response.py +++ b/tests/test_custom_response.py @@ -1,7 +1,7 @@ import json import unittest -from syncano.models import CustomResponseHandler +from syncano.models.custom_response import CustomResponseHandler class ObjectTestCase(unittest.TestCase): From 280f80be8a7dc016924315ba6bb4185d312e242e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 17:35:02 +0100 Subject: [PATCH 205/558] [LIB-274] add custom handling fro webhook.run, correct webhook tests; --- syncano/models/custom_response.py | 11 +++++++++++ syncano/models/incentives.py | 10 ++++++---- tests/integration_test.py | 5 ++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/syncano/models/custom_response.py b/syncano/models/custom_response.py index adcbebb..bc712b8 100644 --- a/syncano/models/custom_response.py +++ b/syncano/models/custom_response.py @@ -69,10 +69,15 @@ def process_response(self, response): @staticmethod def _find_content_type(response): + if not response: + return None return response.get('response', {}).get('content_type') @staticmethod def _default_handler(response): + if not response: + return None + if 'response' in response: return response['response'] if 'stdout' in response: @@ -108,12 +113,18 @@ def content(self): @property def status_code(self): + if not self.result: + return None return self.result.get('response', {}).get('status') @property def error(self): + if not self.result: + return None return self.result.get('stderr') @property def content_type(self): + if not self.result: + return None return self.result.get('response', {}).get('content_type') diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index a012ab3..e159e19 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -275,10 +275,12 @@ def run(self, **payload): connection = self._get_connection(**payload) response = connection.request('POST', endpoint, **{'data': payload}) - - response.update({'instance_name': self.instance_name, - 'webhook_name': self.name}) - return WebhookTrace(**response) + if 'result' in response and 'stdout' in response['result']: + response.update({'instance_name': self.instance_name, + 'webhook_name': self.name}) + return WebhookTrace(**response) + # if codebox is a custom one, return result 'as-it-is'; + return response def reset_link(self): """ diff --git a/tests/integration_test.py b/tests/integration_test.py index b9ab323..881ea1a 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -356,7 +356,7 @@ def test_custom_response_run(self): trace.reload() self.assertEquals(trace.status, 'success') - self.assertDictEqual(trace.content, {'one', 1}) + self.assertDictEqual(trace.content, {'one': 1}) self.assertEqual(trace.contet_type, 'application/json') self.assertEqual(trace.status_code, 200) @@ -428,6 +428,5 @@ def test_custom_codebox_run(self): ) trace = webhook.run() - self.assertEquals(trace.status, 'success') - self.assertDictEqual(trace.content, {'one': 1}) + self.assertDictEqual(trace, {'one': 1}) webhook.delete() From 8e1f0935db89a6dd76366b02b3578edabf25ecb8 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 17:42:02 +0100 Subject: [PATCH 206/558] [LIB-274] Correct Webhook tests after 'as-it-is' custom response change; --- tests/test_incentives.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_incentives.py b/tests/test_incentives.py index 521e833..8967b4b 100644 --- a/tests/test_incentives.py +++ b/tests/test_incentives.py @@ -49,7 +49,7 @@ def test_run(self, connection_mock): connection_mock.request.return_value = { 'status': 'success', 'duration': 937, - 'result': 1, + 'result': {u'stdout': 1, u'stderr': u''}, 'executed_at': '2015-03-16T11:52:14.172830Z' } @@ -61,7 +61,7 @@ def test_run(self, connection_mock): self.assertIsInstance(result, WebhookTrace) self.assertEqual(result.status, 'success') self.assertEqual(result.duration, 937) - self.assertEqual(result.result, 1) + self.assertEqual(result.result, {u'stdout': 1, u'stderr': u''}) self.assertIsInstance(result.executed_at, datetime) connection_mock.assert_called_once_with(x=1, y=2) From 1e922890dd56355cfde07786cc43eaba21533699 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 8 Dec 2015 17:51:51 +0100 Subject: [PATCH 207/558] [LIB-274] correct typo --- tests/integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 881ea1a..1acca7e 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -357,7 +357,7 @@ def test_custom_response_run(self): self.assertEquals(trace.status, 'success') self.assertDictEqual(trace.content, {'one': 1}) - self.assertEqual(trace.contet_type, 'application/json') + self.assertEqual(trace.content_type, 'application/json') self.assertEqual(trace.status_code, 200) codebox.delete() From 51e9a29156168444b6817510628334cbeab8cfc2 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 9 Dec 2015 10:47:19 +0100 Subject: [PATCH 208/558] [LIB-341] Hotfix for creating the object with 'data' kwarg; --- syncano/models/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index cb06fbc..71c6c05 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -268,6 +268,7 @@ def update(self, *args, **kwargs): self.endpoint = 'detail' self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') self.data = kwargs.pop('data', kwargs) + self.data.update(kwargs) model = self.serialize(self.data, self.model) serialized = model.to_native() From f060a039e35fbce8da7ee714db5b7c2f035bbe06 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 9 Dec 2015 10:59:13 +0100 Subject: [PATCH 209/558] [LIB-341] Correct tests with test_update after hotfix --- tests/test_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index d3a7a3c..1ce3b82 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -175,7 +175,7 @@ def test_update(self, clone_mock, filter_mock, request_mock): self.assertEqual(self.manager.method, 'PATCH') self.assertEqual(self.manager.endpoint, 'detail') - self.assertEqual(self.manager.data, {'x': 1, 'y': 2}) + self.assertEqual(self.manager.data, {'x': 1, 'y': 2, 'a': 1, 'b': 2}) result = self.manager.update(1, 2, a=1, b=2, x=3, y=2) self.assertEqual(request_mock, result) From 30fbabb1f88bfd6bb70ea24928a20bcc89cdc00b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 9 Dec 2015 11:05:24 +0100 Subject: [PATCH 210/558] [LIB-341] reverse update order in update data creation; --- syncano/models/manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 71c6c05..8ab74e2 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -267,8 +267,9 @@ def update(self, *args, **kwargs): """ self.endpoint = 'detail' self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') - self.data = kwargs.pop('data', kwargs) - self.data.update(kwargs) + data = kwargs.pop('data', {}) + self.data = kwargs.copy() + self.data.update(data) model = self.serialize(self.data, self.model) serialized = model.to_native() From 96916ed1e43b487beed59af353355a731f8f1646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 9 Dec 2015 11:38:10 +0100 Subject: [PATCH 211/558] Version bump Change version number from 4.0.6 to 4.0.7 --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index f70e6ba..7f3376f 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.6' +__version__ = '4.0.7' __author__ = 'Daniel Kopka' __license__ = 'MIT' __copyright__ = 'Copyright 2015 Syncano' From a7fc2de7a42f10283dc2cf450964a99e749d27e0 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 10 Dec 2015 10:58:31 +0100 Subject: [PATCH 212/558] [LIB-343] Add a test which checks if serializer on update object get all needed parameters --- tests/test_manager.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_manager.py b/tests/test_manager.py index e435896..d55997c 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -584,6 +584,28 @@ def test_order_by(self, clone_mock): with self.assertRaises(SyncanoValueError): self.manager.order_by(10) + @mock.patch('syncano.models.manager.Manager.request') + @mock.patch('syncano.models.manager.ObjectManager.serialize') + def test_update(self, serialize_mock, request_mock): + serialize_mock.return_value = serialize_mock + self.assertFalse(serialize_mock.called) + + self.model.please.update( + id=20, + class_name='test', + instance_name='test', + fieldb='23', + data={ + 'fielda': 1, + 'fieldb': None + }) + + self.assertTrue(serialize_mock.called) + serialize_mock.assert_called_once_with( + {'class_name': 'test', 'instance_name': 'test', 'fielda': 1, 'id': 20, 'fieldb': None}, + self.model + ) + # TODO class SchemaManagerTestCase(unittest.TestCase): From ad61c1ee61a5815ea286844b99c9171a26bfffc0 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 10 Dec 2015 15:20:50 +0100 Subject: [PATCH 213/558] [LIB-157] add in bulk get --- syncano/models/manager.py | 53 +++++++++++++++++++++++++++++++++ tests/integration_test_batch.py | 30 ++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index acc63f4..950457a 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -296,6 +296,59 @@ def get(self, *args, **kwargs): self._filter(*args, **kwargs) return self.request() + @clone + def in_bulk(self, object_ids_list, **kwargs): + """ + A method which allows to bulk get objects; + + Use:: + + response = Classes.please.in_bulk(['test_class', ...]) + + response is: + + > {'test_class': } + + For objects: + + res = Object.please.in_bulk([1, 2], class_name='test_class') + + or + + res = klass.objects.in_bulk([1, 2]) + + response is: + + {1: , 2: {u'content': {u'detail': u'Not found.'}, u'code': 404}} + + + :param object_ids_list: This list expects the primary keys - id in api, a names, ids can be used here; + :return: a dict in which keys are the object_ids_list elements, and values are a populated objects; + """ + self.properties.update(kwargs) + path, defaults = self._get_endpoint_properties() + requests = [ + {'method': 'GET', 'path': '{path}{id}/'.format(path=path, id=object_id)} for object_id in object_ids_list + ] + + response = self.connection.request( + 'POST', + self.BATCH_URI.format(name=registry.last_used_instance), + **{'data': {'requests': requests}} + ) + + bulk_response = {} + + for object_id, object in zip(object_ids_list, response): + if object['code'] == 200: + data = object['content'].copy() + data.update(self.properties) + bulk_response[object_id] = self.model(**data) + else: + bulk_response[object_id] = object + + return bulk_response + def detail(self, *args, **kwargs): """ Wrapper around ``get`` method. diff --git a/tests/integration_test_batch.py b/tests/integration_test_batch.py index 341ff30..c71da3d 100644 --- a/tests/integration_test_batch.py +++ b/tests/integration_test_batch.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from syncano.models import Object, User +from syncano.models import Class, Object, User from tests.integration_test import InstanceMixin, IntegrationTest @@ -101,3 +101,31 @@ def test_batch_mix(self): # assert delete; self.assertEqual(mix_batches[2]['code'], 204) + + def test_in_bulk_get(self): + + self.update1.reload() + self.update2.reload() + self.update3.reload() + + # test object bulk; + bulk_res = self.klass.objects.in_bulk([self.update1.id, self.update2.id, self.update3.id]) + + for res_id, res in bulk_res.iteritems(): + self.assertEqual(res_id, res.id) + + self.assertEqual(bulk_res[self.update1.id].title, self.update1.title) + self.assertEqual(bulk_res[self.update2.id].title, self.update2.title) + self.assertEqual(bulk_res[self.update3.id].title, self.update3.title) + + # test class bulk + + c_bulk_res = Class.please.in_bulk(['class_a']) + + self.assertEqual(c_bulk_res['class_a'].name, 'class_a') + + # test 404 + + c_bulk_res = Class.please.in_bulk(['class_b']) + + self.assertEqual(c_bulk_res['class_b']['code'], 404) From bfb34415cd511eedcf0384505b886fa678f0efa1 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 11 Dec 2015 10:29:15 +0100 Subject: [PATCH 214/558] [LIB-274] correct CustomResponseMixin properties --- syncano/models/custom_response.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/syncano/models/custom_response.py b/syncano/models/custom_response.py index bc712b8..7e07327 100644 --- a/syncano/models/custom_response.py +++ b/syncano/models/custom_response.py @@ -113,18 +113,12 @@ def content(self): @property def status_code(self): - if not self.result: - return None - return self.result.get('response', {}).get('status') + return self.result.get('response', {}).get('status') if self.result else None @property def error(self): - if not self.result: - return None - return self.result.get('stderr') + return self.result.get('stderr') if self.result else None @property def content_type(self): - if not self.result: - return None - return self.result.get('response', {}).get('content_type') + return self.result.get('response', {}).get('content_type') if self.result else None From f2dd6976c7813f10bed883663c1f4d12a807dad1 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 14 Dec 2015 15:08:40 +0100 Subject: [PATCH 215/558] [LIB-352] quickfix --- syncano/connection.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 09831e9..f47fdbe 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -234,7 +234,12 @@ def make_request(self, method_name, path, **kwargs): :raises SyncanoValueError: if invalid request method was chosen :raises SyncanoRequestError: if something went wrong during the request """ - files = kwargs.get('data', {}).pop('files', None) + data = kwargs.get('data', {}) + + files = {k: v for k, v in data.iteritems() + if hasattr(v, 'read')} + map(data.pop, files.keys()) + params = self.build_params(kwargs) method = getattr(self.session, method_name.lower(), None) From 82f1b59afbc7fb90cc07c9d16a23f530b86d8854 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Mon, 14 Dec 2015 15:46:38 +0100 Subject: [PATCH 216/558] [LIB-352] version bump --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 1d3a739..3f63156 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.8' +__version__ = '4.0.9' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From bd12e847cb18bcc8ca94f4222d4bfe43104177ad Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 15 Dec 2015 10:48:45 +0100 Subject: [PATCH 217/558] [LIB-352] correct for create --- syncano/connection.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index f47fdbe..0e01341 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -236,9 +236,12 @@ def make_request(self, method_name, path, **kwargs): """ data = kwargs.get('data', {}) - files = {k: v for k, v in data.iteritems() - if hasattr(v, 'read')} - map(data.pop, files.keys()) + if method_name == 'POST': + files = data.pop('files', {}) + else: + files = {k: v for k, v in data.iteritems() + if hasattr(v, 'read')} + map(data.pop, files.keys()) params = self.build_params(kwargs) method = getattr(self.session, method_name.lower(), None) @@ -260,6 +263,7 @@ def make_request(self, method_name, path, **kwargs): # Encode request payload if 'data' in params and not isinstance(params['data'], six.string_types): params['data'] = json.dumps(params['data']) + url = self.build_url(path) response = method(url, **params) content = self.get_response_content(url, response) From 04b03cae5e73b6bcb8c23fef4224d2cf12b0758a Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 15 Dec 2015 11:29:21 +0100 Subject: [PATCH 218/558] [LIB-352] add tests for fix --- tests/test_connection.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_connection.py b/tests/test_connection.py index 0a591a9..efb8380 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -213,6 +213,31 @@ def test_invalid_method_name(self): with self.assertRaises(SyncanoValueError): self.connection.make_request('INVALID', 'test') + @staticmethod + def _create_test_file(): + with open('test_data_object_file', 'w+') as test_file: + test_file.write('bazinga') + + @mock.patch('syncano.connection.Connection.get_response_content') + def test_make_request_for_creating_object_with_file(self, get_response_mock): + self._create_test_file() + kwargs = { + 'data': { + 'files': {'filename': open('test_data_object_file')} + } + } + # if FAIL will raise TypeError for json dump + self.connection.make_request('POST', 'test', **kwargs) + + @mock.patch('syncano.connection.Connection.get_response_content') + def test_make_request_for_updating_object_with_file(self, get_reponse_mock): + self._create_test_file() + kwargs = { + 'data': {'filename': open('test_data_object_file')} + } + # if FAIL will raise TypeError for json dump + self.connection.make_request('PATCH', 'test', **kwargs) + @mock.patch('requests.Session.post') def test_request_error(self, post_mock): post_mock.return_value = mock.MagicMock(status_code=404, text='Invalid request') From 8c24efd82bcc3a0c63857cb311eee25d7a9c7ddf Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 15 Dec 2015 11:48:41 +0100 Subject: [PATCH 219/558] [LIB-352] version bump --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 3f63156..6a501d6 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.9' +__version__ = '4.0.10' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From e4fe41e167ea7d89f618664192d6e3cd366f2035 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 15 Dec 2015 12:07:49 +0100 Subject: [PATCH 220/558] [LIB-352] check for write and read --- syncano/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 0e01341..5a9cc47 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -240,7 +240,7 @@ def make_request(self, method_name, path, **kwargs): files = data.pop('files', {}) else: files = {k: v for k, v in data.iteritems() - if hasattr(v, 'read')} + if hasattr(v, 'read') and hasattr(v, 'write')} map(data.pop, files.keys()) params = self.build_params(kwargs) From 23805afe403829f7ae7e93bf9f65ef56a1ef183b Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 15 Dec 2015 12:12:54 +0100 Subject: [PATCH 221/558] [LIB-352] use tempfile --- tests/test_connection.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index efb8380..ae3b43c 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,3 +1,4 @@ +import tempfile import unittest from urlparse import urljoin @@ -213,27 +214,22 @@ def test_invalid_method_name(self): with self.assertRaises(SyncanoValueError): self.connection.make_request('INVALID', 'test') - @staticmethod - def _create_test_file(): - with open('test_data_object_file', 'w+') as test_file: - test_file.write('bazinga') - @mock.patch('syncano.connection.Connection.get_response_content') - def test_make_request_for_creating_object_with_file(self, get_response_mock): - self._create_test_file() + @mock.patch('requests.Session.patch') + def test_make_request_for_creating_object_with_file(self, patch_mock, get_response_mock): kwargs = { 'data': { - 'files': {'filename': open('test_data_object_file')} + 'files': {'filename': tempfile.TemporaryFile(mode='w')} } } # if FAIL will raise TypeError for json dump self.connection.make_request('POST', 'test', **kwargs) @mock.patch('syncano.connection.Connection.get_response_content') - def test_make_request_for_updating_object_with_file(self, get_reponse_mock): - self._create_test_file() + @mock.patch('requests.Session.patch') + def test_make_request_for_updating_object_with_file(self, patch_mock, get_reponse_mock): kwargs = { - 'data': {'filename': open('test_data_object_file')} + 'data': {'filename': tempfile.TemporaryFile(mode='w')} } # if FAIL will raise TypeError for json dump self.connection.make_request('PATCH', 'test', **kwargs) From 4f31339fb51764e2bfe9037bb6a8395fa3f999c1 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Tue, 15 Dec 2015 21:29:51 +0100 Subject: [PATCH 222/558] [certifi] Certifi version fix Newest version removed 1024bit CA certificates and 2048 bit ones have some issues with openssl < 1.0.2 (which is pretty new and only a part of newest ubuntu and debian sid). --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a918009..2551e61 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ def readme(): ], install_requires=[ 'requests==2.7.0', - 'certifi', + 'certifi==2015.09.06.2', 'ndg-httpsclient==0.4.0', 'pyasn1==0.1.8', 'pyOpenSSL==0.15.1', From 8a6f9661e59bdd970a9973ecb2ebebbd731eb5ab Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 22 Dec 2015 15:31:11 +0100 Subject: [PATCH 223/558] [LIB-353] nicer file detection --- syncano/connection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 5a9cc47..151b398 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -1,4 +1,5 @@ import json +import os from copy import deepcopy import requests @@ -235,12 +236,11 @@ def make_request(self, method_name, path, **kwargs): :raises SyncanoRequestError: if something went wrong during the request """ data = kwargs.get('data', {}) + files = data.pop('files', None) - if method_name == 'POST': - files = data.pop('files', {}) - else: + if files is None: files = {k: v for k, v in data.iteritems() - if hasattr(v, 'read') and hasattr(v, 'write')} + if os.path.isfile(v.name)} map(data.pop, files.keys()) params = self.build_params(kwargs) From fd80eac255f1201c20d4cdc391a90cf0083b28eb Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Tue, 22 Dec 2015 18:59:07 +0100 Subject: [PATCH 224/558] [LIB-353] checking if type is file --- syncano/connection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 151b398..535cc0a 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -1,5 +1,4 @@ import json -import os from copy import deepcopy import requests @@ -240,7 +239,7 @@ def make_request(self, method_name, path, **kwargs): if files is None: files = {k: v for k, v in data.iteritems() - if os.path.isfile(v.name)} + if isinstance(v, file)} map(data.pop, files.keys()) params = self.build_params(kwargs) From 45c6b5362aabd76ab4c2f95a09d70e6a0747f19c Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 29 Dec 2015 19:24:11 +0100 Subject: [PATCH 225/558] [LIB-342] Initial try for cleaning up the update method; --- syncano/models/manager.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 950457a..f758d4e 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -453,6 +453,33 @@ def update(self, *args, **kwargs): return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) + @clone + def filter_by_attributes(self, *args, **kwargs): + self._filter(*args, **kwargs) + return self + + @clone + def do_update(self, *args, **kwargs): + self.endpoint = 'detail' + self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') + self.data = kwargs + + model = self.serialize(self.data, self.model) + + serialized = model.to_native() + + serialized = {k: v for k, v in serialized.iteritems() + if k in self.data} + + self.data.update(serialized) + + if not self.is_lazy: + return self.request() + + path, defaults = self._get_endpoint_properties() + + return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) + def update_or_create(self, defaults=None, **kwargs): """ A convenience method for updating an object with the given parameters, creating a new one if necessary. From 8c01c0e117b584ecf62e29931cbe350eed35e382 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Wed, 30 Dec 2015 11:53:21 +0100 Subject: [PATCH 226/558] [LIB-367] Replace map usage with side effects with for loops/dict comprehension. --- syncano/connection.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 535cc0a..c7976c2 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -104,18 +104,14 @@ def __init__(self, host=None, **kwargs): self.session = requests.Session() def _init_login_params(self, login_kwargs): - - def _set_value_or_default(param): - param_lib_default_name = ''.join(param.split('_')).upper() - value = login_kwargs.get(param, getattr(syncano, param_lib_default_name, None)) + for param in self.LOGIN_PARAMS.union(self.ALT_LOGIN_PARAMS, + self.USER_LOGIN_PARAMS, + self.USER_ALT_LOGIN_PARAMS, + self.SOCIAL_LOGIN_PARAMS): + def_name = param.replace('_', '').upper() + value = login_kwargs.get(param, getattr(syncano, def_name, None)) setattr(self, param, value) - map(_set_value_or_default, - self.LOGIN_PARAMS.union(self.ALT_LOGIN_PARAMS, - self.USER_LOGIN_PARAMS, - self.USER_ALT_LOGIN_PARAMS, - self.SOCIAL_LOGIN_PARAMS)) - def _are_params_ok(self, params): return all(getattr(self, p) for p in params) @@ -240,7 +236,7 @@ def make_request(self, method_name, path, **kwargs): if files is None: files = {k: v for k, v in data.iteritems() if isinstance(v, file)} - map(data.pop, files.keys()) + data = {k: v for k, v in data.iteritems() if k not in files} params = self.build_params(kwargs) method = getattr(self.session, method_name.lower(), None) From d6d6e7b520db5f96913fb0873250b61d3fc4921c Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 30 Dec 2015 13:46:42 +0100 Subject: [PATCH 227/558] [LIB-342] add custom update method for DataObjects --- syncano/models/manager.py | 83 +++++++++++++++++++++++++-------------- tests/test_manager.py | 16 +++----- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index f758d4e..c11adeb 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -218,9 +218,16 @@ def batch(self, *args): found resource to delete); """ # firstly turn off lazy mode: - meta = [arg['meta'] for arg in args] - self.is_lazy = False + + meta = [] + for arg in args: + if isinstance(arg, list): # update now can return a list; + for nested_arg in arg: + meta.append(nested_arg['meta']) + else: + meta.append(arg['meta']) + response = self.connection.request( 'POST', self.BATCH_URI.format(name=registry.last_used_instance), @@ -453,33 +460,6 @@ def update(self, *args, **kwargs): return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) - @clone - def filter_by_attributes(self, *args, **kwargs): - self._filter(*args, **kwargs) - return self - - @clone - def do_update(self, *args, **kwargs): - self.endpoint = 'detail' - self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') - self.data = kwargs - - model = self.serialize(self.data, self.model) - - serialized = model.to_native() - - serialized = {k: v for k, v in serialized.iteritems() - if k in self.data} - - self.data.update(serialized) - - if not self.is_lazy: - return self.request() - - path, defaults = self._get_endpoint_properties() - - return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) - def update_or_create(self, defaults=None, **kwargs): """ A convenience method for updating an object with the given parameters, creating a new one if necessary. @@ -885,6 +865,51 @@ def filter(self, **kwargs): self.endpoint = 'list' return self + @clone + def update(self, *args, **kwargs): + """ + Updates multiple instances based on provided arguments. There to ways to do so: + + 1. Django-style update. + 2. By specifying arguments. + + Usage:: + + objects = Object.please.list(instance_name=INSTANCE_NAME, + class_name='someclass').filter(id=1).update(arg='103') + objects = Object.please.list(instance_name=INSTANCE_NAME, + class_name='someclass').filter(id=1).update(arg='103') + + The return value is a list of objects; + + """ + instances = [] + for obj in self: + self.endpoint = 'detail' + self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') + self.data = kwargs.copy() + self._filter(*args, **kwargs) + self.properties.update({'id': obj.id}) + + model = self.serialize(self.data, self.model) + + serialized = model.to_native() + + serialized = {k: v for k, v in serialized.iteritems() + if k in self.data} + + self.data.update(serialized) + + if not self.is_lazy: + updated_instance = self.request() + else: + path, defaults = self._get_endpoint_properties() + updated_instance = self.model.batch_object(method=self.method, path=path, body=self.data, + properties=defaults) + + instances.append(updated_instance) + return instances + def bulk_create(self, *objects): """ Creates many new objects. diff --git a/tests/test_manager.py b/tests/test_manager.py index e6eabef..70e3187 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -586,23 +586,17 @@ def test_order_by(self, clone_mock): @mock.patch('syncano.models.manager.Manager.request') @mock.patch('syncano.models.manager.ObjectManager.serialize') - def test_update(self, serialize_mock, request_mock): + @mock.patch('syncano.models.manager.Manager.iterator') + def test_update(self, iterator_mock, serialize_mock, request_mock): + iterator_mock.return_value = [Object(class_name='test', instance_name='test')] serialize_mock.return_value = serialize_mock self.assertFalse(serialize_mock.called) - self.model.please.update( - id=20, - class_name='test', - instance_name='test', - fieldb='23', - data={ - 'fielda': 1, - 'fieldb': None - }) + self.model.please.list(class_name='test', instance_name='test').filter(id=20).update(fielda=1, fieldb=None) self.assertTrue(serialize_mock.called) serialize_mock.assert_called_once_with( - {'class_name': 'test', 'instance_name': 'test', 'fielda': 1, 'id': 20, 'fieldb': None}, + {'fielda': 1, 'fieldb': None}, self.model ) From 14f433c6ec8e9ff0fdec8d0a7316c952c406b69a Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 30 Dec 2015 13:57:28 +0100 Subject: [PATCH 228/558] [LIB-342] correct batch method --- syncano/models/manager.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index c11adeb..ca527e2 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -221,17 +221,20 @@ def batch(self, *args): self.is_lazy = False meta = [] + requests = [] for arg in args: if isinstance(arg, list): # update now can return a list; for nested_arg in arg: meta.append(nested_arg['meta']) + requests.append(nested_arg['body']) else: meta.append(arg['meta']) + requests.append(arg['body']) response = self.connection.request( 'POST', self.BATCH_URI.format(name=registry.last_used_instance), - **{'data': {'requests': [arg['body'] for arg in args]}} + **{'data': {'requests': requests}} ) populated_response = [] From 3c6c505579d8ae759f5dcb0e22a73ea906e63f5c Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 30 Dec 2015 20:16:44 +0100 Subject: [PATCH 229/558] [LIB-342] correct batch test --- tests/integration_test_batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_batch.py b/tests/integration_test_batch.py index c71da3d..f289d42 100644 --- a/tests/integration_test_batch.py +++ b/tests/integration_test_batch.py @@ -88,7 +88,7 @@ def test_batch_delete(self): def test_batch_mix(self): mix_batches = Object.please.batch( self.klass.objects.as_batch().create(title='four'), - self.klass.objects.as_batch().update(title='TerminatorArrival', id=self.update3.id), + self.klass.objects.as_batch().filter(id=self.update3.id).update(title='TerminatorArrival'), self.klass.objects.as_batch().delete(id=self.delete3.id) ) From 9a844244ff391499fab82a87f57ba82cc94a6e5a Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Mon, 4 Jan 2016 08:30:01 +0100 Subject: [PATCH 230/558] [LIB-367] Added get_account_info method to Connection. --- syncano/connection.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/syncano/connection.py b/syncano/connection.py index c7976c2..28387b6 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -65,6 +65,7 @@ class Connection(object): CONTENT_TYPE = 'application/json' AUTH_SUFFIX = 'v1/account/auth' + ACCOUNT_SUFFIX = 'v1/account/' SOCIAL_AUTH_SUFFIX = AUTH_SUFFIX + '/{social_backend}/' USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' @@ -376,6 +377,11 @@ def authenticate_user(self, **kwargs): self.user_key = response.get('user_key') return self.user_key + def get_account_info(self, api_key=None): + if api_key: + self.api_key = api_key + return self.make_request('GET', self.ACCOUNT_SUFFIX, headers={'X-API-KEY': self.api_key}) + class ConnectionMixin(object): """Injects connection attribute with support of basic validation.""" From 0c07dd3ca88ccbe32090d37a2d4d12bf1f9345a4 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Mon, 4 Jan 2016 10:03:16 +0100 Subject: [PATCH 231/558] [LIB-367] Set 'data' key in kwargs. Because it's later used in build_params. --- syncano/connection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 28387b6..0b0b63b 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -235,9 +235,9 @@ def make_request(self, method_name, path, **kwargs): files = data.pop('files', None) if files is None: - files = {k: v for k, v in data.iteritems() - if isinstance(v, file)} - data = {k: v for k, v in data.iteritems() if k not in files} + files = {k: v for k, v in data.iteritems() if isinstance(v, file)} + if data: + kwargs['data'] = data = {k: v for k, v in data.iteritems() if k not in files} params = self.build_params(kwargs) method = getattr(self.session, method_name.lower(), None) From 8cd806be870352e5e4bc9240c920962867fef1c8 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Mon, 4 Jan 2016 10:04:22 +0100 Subject: [PATCH 232/558] [LIB-367] If there is no api_key given raise SyncanoValueError. --- syncano/connection.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/syncano/connection.py b/syncano/connection.py index 0b0b63b..427c3cd 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -380,6 +380,8 @@ def authenticate_user(self, **kwargs): def get_account_info(self, api_key=None): if api_key: self.api_key = api_key + if not self.api_key: + raise SyncanoValueError('api_key is required.') return self.make_request('GET', self.ACCOUNT_SUFFIX, headers={'X-API-KEY': self.api_key}) From 519e250f5b7a3e98484015299d107cc1da0648b9 Mon Sep 17 00:00:00 2001 From: Marcin Swiderski Date: Mon, 4 Jan 2016 10:05:23 +0100 Subject: [PATCH 233/558] [LIB-367] Addedd tests for connection.get_account_info method. --- tests/test_connection.py | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_connection.py b/tests/test_connection.py index ae3b43c..b51f9fb 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -312,6 +312,56 @@ def test_successful_authentication(self, make_request): self.assertIsNotNone(self.connection.api_key) self.assertEqual(self.connection.api_key, api_key) + @mock.patch('syncano.connection.Connection.make_request') + def test_get_account_info(self, make_request): + info = {u'first_name': u'', u'last_name': u'', u'is_active': True, + u'id': 1, u'has_password': True, u'email': u'dummy'} + self.test_successful_authentication() + make_request.return_value = info + self.assertFalse(make_request.called) + self.assertIsNotNone(self.connection.api_key) + ret = self.connection.get_account_info() + self.assertTrue(make_request.called) + self.assertEqual(info, ret) + + @mock.patch('syncano.connection.Connection.make_request') + def test_get_account_info_with_api_key(self, make_request): + info = {u'first_name': u'', u'last_name': u'', u'is_active': True, + u'id': 1, u'has_password': True, u'email': u'dummy'} + make_request.return_value = info + self.assertFalse(make_request.called) + self.assertIsNone(self.connection.api_key) + ret = self.connection.get_account_info(api_key='test') + self.assertIsNotNone(self.connection.api_key) + self.assertTrue(make_request.called) + self.assertEqual(info, ret) + + @mock.patch('syncano.connection.Connection.make_request') + def test_get_account_info_invalid_key(self, make_request): + err = SyncanoRequestError(403, 'No such API Key.') + make_request.side_effect = err + self.assertFalse(make_request.called) + self.assertIsNone(self.connection.api_key) + try: + self.connection.get_account_info(api_key='invalid') + self.assertTrue(False) + except SyncanoRequestError, e: + self.assertIsNotNone(self.connection.api_key) + self.assertTrue(make_request.called) + self.assertEqual(e, err) + + @mock.patch('syncano.connection.Connection.make_request') + def test_get_account_info_missing_key(self, make_request): + self.assertFalse(make_request.called) + self.assertIsNone(self.connection.api_key) + try: + self.connection.get_account_info() + self.assertTrue(False) + except SyncanoValueError, e: + self.assertIsNone(self.connection.api_key) + self.assertFalse(make_request.called) + self.assertIn('api_key', e.message) + class DefaultConnectionTestCase(unittest.TestCase): From bdb7fb29fc166e404ad8cc730e108bb4118ce568 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 8 Jan 2016 12:31:13 +0100 Subject: [PATCH 234/558] [LIB-421] add deprecation decorator in LIB; --- syncano/models/manager.py | 2 ++ syncano/release_utils.py | 21 +++++++++++++++++++++ tests/test_deprecation_decorator.py | 26 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 syncano/release_utils.py create mode 100644 tests/test_deprecation_decorator.py diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 950457a..25c70e6 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -6,6 +6,7 @@ from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValidationError, SyncanoValueError from syncano.models.bulk import ModelBulkCreate, ObjectBulkCreate +from syncano.release_utils import Deprecated from .registry import registry @@ -413,6 +414,7 @@ def delete(self, *args, **kwargs): return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) @clone + @Deprecated(lineno=2) def update(self, *args, **kwargs): """ Updates single instance based on provided arguments. There to ways to do so: diff --git a/syncano/release_utils.py b/syncano/release_utils.py new file mode 100644 index 0000000..0275452 --- /dev/null +++ b/syncano/release_utils.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- + +import warnings + +warnings.simplefilter('default') + + +class Deprecated(object): + + def __init__(self, lineno): + self.lineno = lineno # how many decorators decorates the depracated func; + + def __call__(self, original_func): + def new_func(*args, **kwargs): + warnings.showwarning( + message="Call to deprecated function '{}'.".format(original_func.__name__), + category=DeprecationWarning, + filename=original_func.func_code.co_filename, + lineno=original_func.func_code.co_firstlineno + self.lineno) + original_func(*args, **kwargs) + return new_func diff --git a/tests/test_deprecation_decorator.py b/tests/test_deprecation_decorator.py new file mode 100644 index 0000000..efdf709 --- /dev/null +++ b/tests/test_deprecation_decorator.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +import unittest +import warnings + +from syncano.release_utils import Deprecated + + +class DeprecationDecoratorTestCase(unittest.TestCase): + + def test_deprecation_decorator(self): + + class SomeClass(object): + + @Deprecated(lineno=0) + def some_deprecated_method(self): + pass + + with warnings.catch_warnings(record=True) as warning: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + # Trigger a warning. + SomeClass().some_deprecated_method() + # Verify some things + self.assertEqual(len(warning), 1) + self.assertEqual(warning[-1].category, DeprecationWarning) + self.assertIn("deprecated", str(warning[-1].message)) From 131179e05a0d722e6b28ee0e6c022013101ffd10 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 8 Jan 2016 15:13:27 +0100 Subject: [PATCH 235/558] [LIB-421] Correct decorator - no return :) --- syncano/release_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/release_utils.py b/syncano/release_utils.py index 0275452..599e0e2 100644 --- a/syncano/release_utils.py +++ b/syncano/release_utils.py @@ -17,5 +17,5 @@ def new_func(*args, **kwargs): category=DeprecationWarning, filename=original_func.func_code.co_filename, lineno=original_func.func_code.co_firstlineno + self.lineno) - original_func(*args, **kwargs) + return original_func(*args, **kwargs) return new_func From 12ec3e1584aa5a0dd553294aeb3cd2dab9be5d29 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 8 Jan 2016 15:26:23 +0100 Subject: [PATCH 236/558] [LIB-421] remove Deprecated decorator from update - it's not neede yet; --- syncano/models/manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 25c70e6..950457a 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -6,7 +6,6 @@ from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValidationError, SyncanoValueError from syncano.models.bulk import ModelBulkCreate, ObjectBulkCreate -from syncano.release_utils import Deprecated from .registry import registry @@ -414,7 +413,6 @@ def delete(self, *args, **kwargs): return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) @clone - @Deprecated(lineno=2) def update(self, *args, **kwargs): """ Updates single instance based on provided arguments. There to ways to do so: From e703396ee4c1b6f96529954c4e8e04cb03dfa10d Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 8 Jan 2016 16:15:21 +0100 Subject: [PATCH 237/558] [LIB-421] add removed version to deprecation warning --- syncano/release_utils.py | 8 ++++++-- tests/test_deprecation_decorator.py | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/syncano/release_utils.py b/syncano/release_utils.py index 599e0e2..291386e 100644 --- a/syncano/release_utils.py +++ b/syncano/release_utils.py @@ -7,13 +7,17 @@ class Deprecated(object): - def __init__(self, lineno): + def __init__(self, lineno, removed_in_version): self.lineno = lineno # how many decorators decorates the depracated func; + self.removed_in_version = removed_in_version def __call__(self, original_func): def new_func(*args, **kwargs): warnings.showwarning( - message="Call to deprecated function '{}'.".format(original_func.__name__), + message="Call to deprecated function '{}'. Will be removed in version: {}.".format( + original_func.__name__, + self.removed_in_version + ), category=DeprecationWarning, filename=original_func.func_code.co_filename, lineno=original_func.func_code.co_firstlineno + self.lineno) diff --git a/tests/test_deprecation_decorator.py b/tests/test_deprecation_decorator.py index efdf709..a751713 100644 --- a/tests/test_deprecation_decorator.py +++ b/tests/test_deprecation_decorator.py @@ -11,16 +11,17 @@ def test_deprecation_decorator(self): class SomeClass(object): - @Deprecated(lineno=0) + @Deprecated(lineno=0, removed_in_version='5.0.10') def some_deprecated_method(self): pass with warnings.catch_warnings(record=True) as warning: # Cause all warnings to always be triggered. - warnings.simplefilter("always") + warnings.simplefilter('always') # Trigger a warning. SomeClass().some_deprecated_method() # Verify some things self.assertEqual(len(warning), 1) self.assertEqual(warning[-1].category, DeprecationWarning) - self.assertIn("deprecated", str(warning[-1].message)) + self.assertIn('deprecated', str(warning[-1].message)) + self.assertIn('5.0.10', str(warning[-1].message)) From d8afb2652eeb69d995938e062e9c6157775fd292 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 8 Jan 2016 16:59:29 +0100 Subject: [PATCH 238/558] [LIB-421] change requirements libs order --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fbaf632..ab01918 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ Unidecode==0.4.18 coverage==3.7.1 +pep8==1.5.7 flake8==2.4.1 funcsigs==0.4 isort==4.0.0 @@ -7,7 +8,6 @@ mccabe==0.3.1 mock==1.3.0 nose==1.3.7 pbr==1.6.0 -pep8==1.5.7 pyflakes==0.8.1 python-slugify==0.1.0 requests==2.7.0 From d7ad9159c6d99ef72c3722374a1c8266db1a3da5 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 11 Jan 2016 12:50:55 +0100 Subject: [PATCH 239/558] [LIB-421] Add wraps to new function; correct to get only one waring; --- syncano/release_utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/syncano/release_utils.py b/syncano/release_utils.py index 291386e..2358091 100644 --- a/syncano/release_utils.py +++ b/syncano/release_utils.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- import warnings +from functools import wraps -warnings.simplefilter('default') +warnings.simplefilter('once') class Deprecated(object): @@ -12,8 +13,9 @@ def __init__(self, lineno, removed_in_version): self.removed_in_version = removed_in_version def __call__(self, original_func): + @wraps(original_func) def new_func(*args, **kwargs): - warnings.showwarning( + warnings.warn_explicit( message="Call to deprecated function '{}'. Will be removed in version: {}.".format( original_func.__name__, self.removed_in_version From 16cf42f326e54851795e2656ccaca3e1419ae0cc Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 11 Jan 2016 14:44:34 +0100 Subject: [PATCH 240/558] [LIB-342] working-in-progress: update redesign; --- syncano/models/manager.py | 125 ++++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 46 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index ca527e2..516be29 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -83,6 +83,7 @@ def __init__(self): self.query = {} self.data = {} self.is_lazy = False + self._filter_kwargs = {} self._limit = None self._serialize = True @@ -422,8 +423,85 @@ def delete(self, *args, **kwargs): return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) + @clone + def filter(self, **kwargs): + self._filter_kwargs = kwargs + return self + @clone def update(self, *args, **kwargs): + if self._filter_kwargs or self.query: + return self.new_update(*args, **kwargs) + return self.old_update(*args, **kwargs) + + @clone + def new_update(self, *args, **kwargs): + """ + Updates multiple instances based on provided arguments. There to ways to do so: + + 1. Django-style update. + 2. By specifying arguments. + + Usage:: + + objects = Object.please.list(instance_name=INSTANCE_NAME, + class_name='someclass').filter(id=1).update(arg='103') + objects = Object.please.list(instance_name=INSTANCE_NAME, + class_name='someclass').filter(id=1).update(arg='103') + + The return value is a list of objects; + + """ + + if self._filter_kwargs: + # do a single object update: Class, Instance for example; + self.data = kwargs.copy() + self.data.update(self._filter_kwargs) + + model = self.serialize(self.data, self.model) + + serialized = model.to_native() + + serialized = {k: v for k, v in serialized.iteritems() + if k in self.data} + + self.data.update(serialized) + self._filter(*args, **kwargs) + + if not self.is_lazy: + return [self.request()] + path, defaults = self._get_endpoint_properties() + return [self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults)] + + instances = [] + for obj in self: + self.endpoint = 'detail' + self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') + self.data = kwargs.copy() + self._filter(*args, **kwargs) + model = self.serialize(self.data, self.model) + + serialized = model.to_native() + + serialized = {k: v for k, v in serialized.iteritems() + if k in self.data} + + self.data.update(serialized) + self.properties.update({'id': obj.id}) + path, defaults = self._get_endpoint_properties() + updated_instance = self.model.batch_object(method=self.method, path=path, body=self.data, + properties=defaults) + + instances.append(updated_instance) # always a batch structure here; + + if not self.is_lazy: + # do batch query here! + instances = self.batch(instances) + + return instances + + @clone + def old_update(self, *args, **kwargs): """ Updates single instance based on provided arguments. There to ways to do so: @@ -460,7 +538,6 @@ def update(self, *args, **kwargs): return self.request() path, defaults = self._get_endpoint_properties() - return self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults) def update_or_create(self, defaults=None, **kwargs): @@ -651,6 +728,7 @@ def _clone(self): manager._limit = self._limit manager.method = self.method manager.query = deepcopy(self.query) + manager._filter_kwargs = deepcopy(self._filter_kwargs) manager.data = deepcopy(self.data) manager._serialize = self._serialize manager.is_lazy = self.is_lazy @@ -868,51 +946,6 @@ def filter(self, **kwargs): self.endpoint = 'list' return self - @clone - def update(self, *args, **kwargs): - """ - Updates multiple instances based on provided arguments. There to ways to do so: - - 1. Django-style update. - 2. By specifying arguments. - - Usage:: - - objects = Object.please.list(instance_name=INSTANCE_NAME, - class_name='someclass').filter(id=1).update(arg='103') - objects = Object.please.list(instance_name=INSTANCE_NAME, - class_name='someclass').filter(id=1).update(arg='103') - - The return value is a list of objects; - - """ - instances = [] - for obj in self: - self.endpoint = 'detail' - self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') - self.data = kwargs.copy() - self._filter(*args, **kwargs) - self.properties.update({'id': obj.id}) - - model = self.serialize(self.data, self.model) - - serialized = model.to_native() - - serialized = {k: v for k, v in serialized.iteritems() - if k in self.data} - - self.data.update(serialized) - - if not self.is_lazy: - updated_instance = self.request() - else: - path, defaults = self._get_endpoint_properties() - updated_instance = self.model.batch_object(method=self.method, path=path, body=self.data, - properties=defaults) - - instances.append(updated_instance) - return instances - def bulk_create(self, *objects): """ Creates many new objects. From e6988f08bf8c73e1aa0804a1ae730e1898bc989e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 12 Jan 2016 11:38:55 +0100 Subject: [PATCH 241/558] [LIB-342] update redesign, support old style update and new style update now; --- syncano/models/manager.py | 22 ++++++++++++---------- tests/integration_test_batch.py | 2 +- tests/test_manager.py | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 516be29..260b62f 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -425,12 +425,16 @@ def delete(self, *args, **kwargs): @clone def filter(self, **kwargs): + endpoint_fields = [field.name for field in self.model._meta.fields if field.has_endpoint_data] + for kwarg_name in kwargs: + if kwarg_name not in endpoint_fields: + raise SyncanoValueError('Only endpoint properties can be used in filter: {}'.format(endpoint_fields)) self._filter_kwargs = kwargs return self @clone def update(self, *args, **kwargs): - if self._filter_kwargs or self.query: + if self._filter_kwargs or self.query: # means that .filter() was run; return self.new_update(*args, **kwargs) return self.old_update(*args, **kwargs) @@ -453,36 +457,35 @@ def new_update(self, *args, **kwargs): """ - if self._filter_kwargs: + if self._filter_kwargs: # Manager context; # do a single object update: Class, Instance for example; + self.endpoint = 'detail' + self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') self.data = kwargs.copy() self.data.update(self._filter_kwargs) model = self.serialize(self.data, self.model) - serialized = model.to_native() - serialized = {k: v for k, v in serialized.iteritems() if k in self.data} self.data.update(serialized) - self._filter(*args, **kwargs) + self._filter(*args, **self.data) # sets the proper self.properties here if not self.is_lazy: - return [self.request()] + return [self.serialize(self.request(), self.model)] path, defaults = self._get_endpoint_properties() return [self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults)] - instances = [] + instances = [] # ObjectManager context; for obj in self: self.endpoint = 'detail' self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') self.data = kwargs.copy() self._filter(*args, **kwargs) - model = self.serialize(self.data, self.model) + model = self.serialize(self.data, self.model) serialized = model.to_native() - serialized = {k: v for k, v in serialized.iteritems() if k in self.data} @@ -495,7 +498,6 @@ def new_update(self, *args, **kwargs): instances.append(updated_instance) # always a batch structure here; if not self.is_lazy: - # do batch query here! instances = self.batch(instances) return instances diff --git a/tests/integration_test_batch.py b/tests/integration_test_batch.py index f289d42..7975468 100644 --- a/tests/integration_test_batch.py +++ b/tests/integration_test_batch.py @@ -88,7 +88,7 @@ def test_batch_delete(self): def test_batch_mix(self): mix_batches = Object.please.batch( self.klass.objects.as_batch().create(title='four'), - self.klass.objects.as_batch().filter(id=self.update3.id).update(title='TerminatorArrival'), + self.klass.objects.as_batch().update(id=self.update3.id, title='TerminatorArrival'), self.klass.objects.as_batch().delete(id=self.delete3.id) ) diff --git a/tests/test_manager.py b/tests/test_manager.py index 70e3187..b136fa6 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -592,7 +592,7 @@ def test_update(self, iterator_mock, serialize_mock, request_mock): serialize_mock.return_value = serialize_mock self.assertFalse(serialize_mock.called) - self.model.please.list(class_name='test', instance_name='test').filter(id=20).update(fielda=1, fieldb=None) + self.model.please.list(class_name='test', instance_name='test').update(id=20, fielda=1, fieldb=None) self.assertTrue(serialize_mock.called) serialize_mock.assert_called_once_with( From 2070eb9b9574b5fc1f49f27efe7bedb4fd471b36 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 12 Jan 2016 11:41:55 +0100 Subject: [PATCH 242/558] [LIB-342] restore old assert in test_manager --- tests/test_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index b136fa6..70edc7b 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -596,7 +596,7 @@ def test_update(self, iterator_mock, serialize_mock, request_mock): self.assertTrue(serialize_mock.called) serialize_mock.assert_called_once_with( - {'fielda': 1, 'fieldb': None}, + {'id': 20, 'fielda': 1, 'fieldb': None}, self.model ) From efccc50b28095b22d455060ca8ee2a1817973eda Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 12 Jan 2016 12:55:16 +0100 Subject: [PATCH 243/558] [LIB-342] little refactor, add test for new update syntax; --- syncano/models/manager.py | 52 +++++++++++++++++++------------------- tests/test_manager.py | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 260b62f..82a4c63 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -435,11 +435,11 @@ def filter(self, **kwargs): @clone def update(self, *args, **kwargs): if self._filter_kwargs or self.query: # means that .filter() was run; - return self.new_update(*args, **kwargs) + return self.new_update(**kwargs) return self.old_update(*args, **kwargs) @clone - def new_update(self, *args, **kwargs): + def new_update(self, **kwargs): """ Updates multiple instances based on provided arguments. There to ways to do so: @@ -457,42 +457,34 @@ def new_update(self, *args, **kwargs): """ + model_fields = [field.name for field in self.model._meta.fields if not field.has_endpoint_data] + for field_name in kwargs: + if field_name not in model_fields: + raise SyncanoValueError('This model has not field {}'.format(field_name)) + + self.endpoint = 'detail' + self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') + self.data = kwargs.copy() + if self._filter_kwargs: # Manager context; # do a single object update: Class, Instance for example; - self.endpoint = 'detail' - self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') - self.data = kwargs.copy() self.data.update(self._filter_kwargs) - - model = self.serialize(self.data, self.model) - serialized = model.to_native() - serialized = {k: v for k, v in serialized.iteritems() - if k in self.data} - - self.data.update(serialized) - self._filter(*args, **self.data) # sets the proper self.properties here + serialized = self._get_serialized_data() + self._filter(*(), **self.data) # sets the proper self.properties here if not self.is_lazy: return [self.serialize(self.request(), self.model)] + path, defaults = self._get_endpoint_properties() - return [self.model.batch_object(method=self.method, path=path, body=self.data, properties=defaults)] + return [self.model.batch_object(method=self.method, path=path, body=serialized, properties=defaults)] instances = [] # ObjectManager context; for obj in self: - self.endpoint = 'detail' - self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') - self.data = kwargs.copy() - self._filter(*args, **kwargs) - - model = self.serialize(self.data, self.model) - serialized = model.to_native() - serialized = {k: v for k, v in serialized.iteritems() - if k in self.data} - - self.data.update(serialized) + self._filter(*(), **kwargs) + serialized = self._get_serialized_data() self.properties.update({'id': obj.id}) path, defaults = self._get_endpoint_properties() - updated_instance = self.model.batch_object(method=self.method, path=path, body=self.data, + updated_instance = self.model.batch_object(method=self.method, path=path, body=serialized, properties=defaults) instances.append(updated_instance) # always a batch structure here; @@ -702,6 +694,14 @@ def contribute_to_class(self, model, name): # pragma: no cover if not self.name: self.name = name + def _get_serialized_data(self): + model = self.serialize(self.data, self.model) + serialized = model.to_native() + serialized = {k: v for k, v in serialized.iteritems() + if k in self.data} + self.data.update(serialized) + return serialized + def _filter(self, *args, **kwargs): properties = self.model._meta.get_endpoint_properties(self.endpoint) diff --git a/tests/test_manager.py b/tests/test_manager.py index 70edc7b..d5e3d50 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -225,6 +225,42 @@ def test_update(self, clone_mock, filter_mock, request_mock): self.assertEqual(self.manager.endpoint, 'detail') self.assertEqual(self.manager.data, {'x': 3, 'y': 2, 'a': 1, 'b': 2}) + @mock.patch('syncano.models.manager.Manager.request') + @mock.patch('syncano.models.manager.Manager._filter') + @mock.patch('syncano.models.manager.Manager._clone') + @mock.patch('syncano.models.manager.Manager.serialize') + def test_update_with_filter(self, serializer_mock, clone_mock, filter_mock, request_mock, ): + serializer_mock.returnValue = Instance(name='test') + clone_mock.return_value = self.manager + request_mock.return_value = request_mock + + self.assertFalse(filter_mock.called) + self.assertFalse(request_mock.called) + + result = self.manager.filter(name=2).update(created_at=1, updated_at=2, links=1) + # self.assertEqual(request_mock, result) + + self.assertTrue(filter_mock.called) + self.assertTrue(request_mock.called) + + filter_mock.assert_called_once_with(created_at=1, updated_at=2, links=1, name=2) + request_mock.assert_called_once_with() + + self.assertEqual(self.manager.data, {'created_at': 1, 'updated_at': 2, 'links': 1, 'name': 2}) + + @mock.patch('syncano.models.manager.Manager.request') + @mock.patch('syncano.models.manager.Manager._filter') + @mock.patch('syncano.models.manager.Manager._clone') + def test_update_with_filter_wrong_arg(self, clone_mock, filter_mock, request_mock): + clone_mock.return_value = self.manager + request_mock.return_value = request_mock + + self.assertFalse(filter_mock.called) + self.assertFalse(request_mock.called) + + with self.assertRaises(SyncanoValueError): + self.manager.filter(name='1', bad_arg='something').update(a=1, b=2, data={'x': 1, 'y': 2}) + @mock.patch('syncano.models.manager.Manager.update') @mock.patch('syncano.models.manager.Manager.create') def test_update_or_create(self, create_mock, update_mock): @@ -600,6 +636,23 @@ def test_update(self, iterator_mock, serialize_mock, request_mock): self.model ) + @mock.patch('syncano.models.manager.Manager.request') + @mock.patch('syncano.models.manager.ObjectManager.serialize') + @mock.patch('syncano.models.manager.Manager.iterator') + def test_update_with_filter(self, iterator_mock, serialize_mock, request_mock): + iterator_mock.return_value = [Object(class_name='test', instance_name='test')] + serialize_mock.return_value = serialize_mock + self.assertFalse(serialize_mock.called) + + self.model.please.list(class_name='test', instance_name='test').filter(id=20).update(channel=1, revision=None) + + self.assertTrue(serialize_mock.called) + serialize_mock.assert_called_once_with( + {'channel': 1, 'revision': None}, + self.model + ) + + # TODO class SchemaManagerTestCase(unittest.TestCase): From 808d76f4f71976cadcdb1cf443643c5643a4bbdd Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 12 Jan 2016 12:57:32 +0100 Subject: [PATCH 244/558] [LIB-342] correct flake issues --- tests/test_manager.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index d5e3d50..7c234ce 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -237,8 +237,7 @@ def test_update_with_filter(self, serializer_mock, clone_mock, filter_mock, requ self.assertFalse(filter_mock.called) self.assertFalse(request_mock.called) - result = self.manager.filter(name=2).update(created_at=1, updated_at=2, links=1) - # self.assertEqual(request_mock, result) + self.manager.filter(name=2).update(created_at=1, updated_at=2, links=1) self.assertTrue(filter_mock.called) self.assertTrue(request_mock.called) @@ -653,7 +652,6 @@ def test_update_with_filter(self, iterator_mock, serialize_mock, request_mock): ) - # TODO class SchemaManagerTestCase(unittest.TestCase): pass From c33a766ec4ed80a748d18293756ec59d69cc2013 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 12 Jan 2016 14:39:51 +0100 Subject: [PATCH 245/558] [LIB-178] Add support for expected_revision in save; --- syncano/models/archetypes.py | 2 ++ tests/test_models.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 88277a5..baec78c 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -134,6 +134,8 @@ def save(self, **kwargs): method = 'PUT' endpoint = self._meta.resolve_endpoint(endpoint_name, properties) + if 'expected_revision' in kwargs: + data.update({'expected_revision': kwargs['expected_revision']}) request = {'data': data} if not self.is_lazy: diff --git a/tests/test_models.py b/tests/test_models.py index d63f900..0a2f14a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -179,3 +179,21 @@ def test_to_native(self): self.model.description = 'desc' self.model.dummy = 'test' self.assertEqual(self.model.to_native(), {'name': 'test', 'description': 'desc'}) + + @mock.patch('syncano.models.Instance._get_connection') + def test_save_with_revision(self, connection_mock): + connection_mock.return_value = connection_mock + connection_mock.request.return_value = {} + + self.assertFalse(connection_mock.called) + self.assertFalse(connection_mock.request.called) + + Instance(name='test').save(expected_revision=12) + + self.assertTrue(connection_mock.called) + self.assertTrue(connection_mock.request.called) + connection_mock.request.assert_called_with( + 'POST', + '/v1/instances/', + data={'name': 'test', 'expected_revision': 12} + ) From e32e6146e722a248e34499483996a00201805eaa Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 12 Jan 2016 15:11:11 +0100 Subject: [PATCH 246/558] [LIB-178] add RevisionMismatchException; raised when revisions do not match; --- syncano/connection.py | 4 +++- syncano/exceptions.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 427c3cd..e09a2ec 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -4,7 +4,7 @@ import requests import six import syncano -from syncano.exceptions import SyncanoRequestError, SyncanoValueError +from syncano.exceptions import RevisionMismatchException, SyncanoRequestError, SyncanoValueError if six.PY3: from urllib.parse import urljoin @@ -290,6 +290,8 @@ def get_response_content(self, url, response): # Validation error if is_client_error(response.status_code): + if response.status_code == 400 and 'Revision mismatch.' in content.get('non_field_errors', [''])[0]: + raise RevisionMismatchException(response.status_code, content) raise SyncanoRequestError(response.status_code, content) # Other errors diff --git a/syncano/exceptions.py b/syncano/exceptions.py index 7fdfbc8..bcd3f2a 100644 --- a/syncano/exceptions.py +++ b/syncano/exceptions.py @@ -71,3 +71,7 @@ def __str__(self): class SyncanoDoesNotExist(SyncanoException): """Syncano object doesn't exist error occurred.""" + + +class RevisionMismatchException(SyncanoRequestError): + """Revision do not match with expected one""" From c247d0195adc6f31e749ed870eb076c17098a492 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 15 Jan 2016 12:54:44 +0100 Subject: [PATCH 247/558] [LIB-178] correct mismatch revision checking --- syncano/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index e09a2ec..9f365e4 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -290,7 +290,7 @@ def get_response_content(self, url, response): # Validation error if is_client_error(response.status_code): - if response.status_code == 400 and 'Revision mismatch.' in content.get('non_field_errors', [''])[0]: + if response.status_code == 400 and 'expected_revision' in content: raise RevisionMismatchException(response.status_code, content) raise SyncanoRequestError(response.status_code, content) From ec05c76e1391c812370b3ccca4f946bc15d417e1 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 13:12:07 +0100 Subject: [PATCH 248/558] [LIB-447] create response templates models in lib; --- syncano/models/incentives.py | 52 +++++++++++++++++++++ syncano/models/instances.py | 1 + tests/integration_test_reponse_templates.py | 51 ++++++++++++++++++++ tests/test_incentives.py | 29 +++++++++++- 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 tests/integration_test_reponse_templates.py diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index e159e19..0902ba7 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -295,3 +295,55 @@ def reset_link(self): response = connection.request('POST', endpoint) self.public_link = response['public_link'] + + +class ResponseTemplate(Model): + """ + OO wrapper around templates. + + :ivar name: :class:`~syncano.models.fields.StringField` + :ivar content: :class:`~syncano.models.fields.IntegerField` + :ivar content_type: :class:`~syncano.models.fields.StringField` + :ivar context: :class:`~syncano.models.fields.JSONField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + + LINKS = ( + {'type': 'detail', 'name': 'self'}, + ) + + name = fields.StringField(max_length=64) + content = fields.StringField(label='content') + content_type = fields.StringField(label='content type') + context = fields.JSONField(label='context') + links = fields.HyperlinkedField(links=LINKS) + + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/snippets/templates/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/snippets/templates/', + }, + 'render': { + 'methods': ['post'], + 'path': '/snippets/templates/{name}/render/', + }, + } + + def render(self, context=None): + context = context if context else {} + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('render', properties) + + connection = self._get_connection() + return connection.request('POST', endpoint, data={'context': context}) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index b5e59f6..f972f2a 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -30,6 +30,7 @@ class Instance(Model): {'type': 'list', 'name': 'users'}, {'type': 'list', 'name': 'webhooks'}, {'type': 'list', 'name': 'schedules'}, + {'type': 'list', 'name': 'templates'} ) name = fields.StringField(max_length=64, primary_key=True) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py new file mode 100644 index 0000000..41d0629 --- /dev/null +++ b/tests/integration_test_reponse_templates.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from syncano.exceptions import SyncanoRequestError +from syncano.models import ResponseTemplate +from tests.integration_test import InstanceMixin, IntegrationTest + + +class ManagerBatchTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(ManagerBatchTest, cls).setUpClass() + cls.to_delete = cls.instance.templates.create(name='to_delete', content="
", + content_type='text/html', context={'one': 1}) + cls.for_update = cls.instance.templates.create(name='to_update', content="
", + content_type='text/html', context={'one': 1}) + + def test_retrieve_api(self): + template = ResponseTemplate.please.get('to_update') + self.assertTrue(isinstance(template, ResponseTemplate)) + self.assertTrue(ResponseTemplate.name) + self.assertTrue(ResponseTemplate.content) + self.assertTrue(ResponseTemplate.content_type) + self.assertTrue(ResponseTemplate.context) + + def test_create_api(self): + template = ResponseTemplate.please.create(name='just_created', content='
', content_type='text/html', + context={'two': 2}) + self.assertTrue(isinstance(template, ResponseTemplate)) + + def test_delete_api(self): + ResponseTemplate.please.get('to_delete').delete() + with self.assertRaises(SyncanoRequestError): + ResponseTemplate.please.get('to_delete') + + def test_update_api(self): + self.for_update.content = "
Hello!
" + self.for_update.save() + + template = ResponseTemplate.please.get(name='to_udpate') + self.assertEqual(template.content, "
Hello!
") + + def test_render_api(self): + render_template = self.instance.templates.create(name='to_update', + content="{% for o in objects}
  • o
  • {% endfor %}", + content_type='text/html', context={'objects': [1, 2]}) + + rendered = render_template.render() + self.assertEqual(rendered, '
  • 1
  • 2
  • ') + + rendered = render_template.render(context={'objects': [3]}) + self.assertEqual(rendered, '
  • 3
  • ') diff --git a/tests/test_incentives.py b/tests/test_incentives.py index 8967b4b..435bfd1 100644 --- a/tests/test_incentives.py +++ b/tests/test_incentives.py @@ -1,8 +1,10 @@ +# -*- coding: utf-8 -*- + import unittest from datetime import datetime from syncano.exceptions import SyncanoValidationError -from syncano.models import CodeBox, CodeBoxTrace, Webhook, WebhookTrace +from syncano.models import CodeBox, CodeBoxTrace, ResponseTemplate, Webhook, WebhookTrace try: from unittest import mock @@ -74,3 +76,28 @@ def test_run(self, connection_mock): model = Webhook() with self.assertRaises(SyncanoValidationError): model.run() + + +class ResponseTemplateTestCase(unittest.TestCase): + def setUp(self): + self.model = ResponseTemplate + + @mock.patch('syncano.models.ResponseTemplate._get_connection') + def test_render(self, connection_mock): + model = self.model(instance_name='test', name='name', + links={'run': '/v1/instances/test/snippets/templates/name/render/'}) + connection_mock.return_value = connection_mock + connection_mock.request.return_value = '
    12345
    ' + + self.assertFalse(connection_mock.called) + self.assertFalse(connection_mock.request.called) + response = model.render() + self.assertTrue(connection_mock.called) + self.assertTrue(connection_mock.request.called) + self.assertEqual(response, '
    12345
    ') + + connection_mock.request.assert_called_once_with( + 'POST', + '/v1/instances/test/snippets/templates/name/render/', + data={'context': {}} + ) From 8a8094399f5cfd37e4a40d4ad4255d6626919db1 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 13:22:42 +0100 Subject: [PATCH 249/558] [LIB-447] correct response templates tests --- tests/integration_test_reponse_templates.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index 41d0629..e9e55a8 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -4,18 +4,18 @@ from tests.integration_test import InstanceMixin, IntegrationTest -class ManagerBatchTest(InstanceMixin, IntegrationTest): +class ResponseTemplateApiTest(InstanceMixin, IntegrationTest): @classmethod def setUpClass(cls): - super(ManagerBatchTest, cls).setUpClass() + super(ResponseTemplateApiTest, cls).setUpClass() cls.to_delete = cls.instance.templates.create(name='to_delete', content="
    ", content_type='text/html', context={'one': 1}) cls.for_update = cls.instance.templates.create(name='to_update', content="
    ", content_type='text/html', context={'one': 1}) def test_retrieve_api(self): - template = ResponseTemplate.please.get('to_update') + template = ResponseTemplate.please.get(name='to_update') self.assertTrue(isinstance(template, ResponseTemplate)) self.assertTrue(ResponseTemplate.name) self.assertTrue(ResponseTemplate.content) @@ -28,7 +28,7 @@ def test_create_api(self): self.assertTrue(isinstance(template, ResponseTemplate)) def test_delete_api(self): - ResponseTemplate.please.get('to_delete').delete() + ResponseTemplate.please.delete('to_delete') with self.assertRaises(SyncanoRequestError): ResponseTemplate.please.get('to_delete') @@ -36,11 +36,11 @@ def test_update_api(self): self.for_update.content = "
    Hello!
    " self.for_update.save() - template = ResponseTemplate.please.get(name='to_udpate') + template = ResponseTemplate.please.get(name='to_update') self.assertEqual(template.content, "
    Hello!
    ") def test_render_api(self): - render_template = self.instance.templates.create(name='to_update', + render_template = self.instance.templates.create(name='to_render', content="{% for o in objects}
  • o
  • {% endfor %}", content_type='text/html', context={'objects': [1, 2]}) From 40b22cd2a79b226ce7e511de8783aec76c2c81cb Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 13:29:06 +0100 Subject: [PATCH 250/558] [LIB-447] correct response templates tests --- tests/integration_test_reponse_templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index e9e55a8..aa1f340 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -28,7 +28,7 @@ def test_create_api(self): self.assertTrue(isinstance(template, ResponseTemplate)) def test_delete_api(self): - ResponseTemplate.please.delete('to_delete') + ResponseTemplate.please.delete(name='to_delete') with self.assertRaises(SyncanoRequestError): ResponseTemplate.please.get('to_delete') @@ -41,7 +41,7 @@ def test_update_api(self): def test_render_api(self): render_template = self.instance.templates.create(name='to_render', - content="{% for o in objects}
  • o
  • {% endfor %}", + content="{% for o in objects %}
  • o
  • {% endfor %}", content_type='text/html', context={'objects': [1, 2]}) rendered = render_template.render() From a424bba9d3e342388286f3e2b07c764be936ff3d Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 13:33:35 +0100 Subject: [PATCH 251/558] [LIB-447] correct response templates tests --- tests/integration_test_reponse_templates.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index aa1f340..730e0f1 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -17,10 +17,10 @@ def setUpClass(cls): def test_retrieve_api(self): template = ResponseTemplate.please.get(name='to_update') self.assertTrue(isinstance(template, ResponseTemplate)) - self.assertTrue(ResponseTemplate.name) - self.assertTrue(ResponseTemplate.content) - self.assertTrue(ResponseTemplate.content_type) - self.assertTrue(ResponseTemplate.context) + self.assertTrue(template.name) + self.assertTrue(template.content) + self.assertTrue(template.content_type) + self.assertTrue(template.context) def test_create_api(self): template = ResponseTemplate.please.create(name='just_created', content='
    ', content_type='text/html', @@ -30,7 +30,7 @@ def test_create_api(self): def test_delete_api(self): ResponseTemplate.please.delete(name='to_delete') with self.assertRaises(SyncanoRequestError): - ResponseTemplate.please.get('to_delete') + ResponseTemplate.please.get(name='to_delete') def test_update_api(self): self.for_update.content = "
    Hello!
    " @@ -41,7 +41,7 @@ def test_update_api(self): def test_render_api(self): render_template = self.instance.templates.create(name='to_render', - content="{% for o in objects %}
  • o
  • {% endfor %}", + content="{% for o in objects %}
  • {{ o }}
  • {% endfor %}", content_type='text/html', context={'objects': [1, 2]}) rendered = render_template.render() From 2cf64a25a6d71bc0dfe5f86a5bdb4290bca25d56 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 13:38:03 +0100 Subject: [PATCH 252/558] [LIB-447] correct response templates tests --- tests/integration_test_reponse_templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index 730e0f1..3f98af1 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from syncano.exceptions import SyncanoRequestError + from syncano.models import ResponseTemplate from tests.integration_test import InstanceMixin, IntegrationTest @@ -29,7 +29,7 @@ def test_create_api(self): def test_delete_api(self): ResponseTemplate.please.delete(name='to_delete') - with self.assertRaises(SyncanoRequestError): + with self.assertRaises(self.model.DoesNotExist): ResponseTemplate.please.get(name='to_delete') def test_update_api(self): From c059a537aa044ac31a0f394c7dce0bc5b2edf2bd Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 13:46:24 +0100 Subject: [PATCH 253/558] [LIB-447] correct response templates tests --- tests/integration_test_reponse_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index 3f98af1..54b2562 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -29,7 +29,7 @@ def test_create_api(self): def test_delete_api(self): ResponseTemplate.please.delete(name='to_delete') - with self.assertRaises(self.model.DoesNotExist): + with self.assertRaises(ResponseTemplate.DoesNotExist): ResponseTemplate.please.get(name='to_delete') def test_update_api(self): From bf316fee018efaef08251abf333dc96e00e9daa5 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 14:12:07 +0100 Subject: [PATCH 254/558] [LIB-177] removed connect_instance method totally; --- syncano/__init__.py | 46 ---------------------------------------- tests/test_connection.py | 42 +----------------------------------- 2 files changed, 1 insertion(+), 87 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 6a501d6..5426e46 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -87,49 +87,3 @@ def connect(*args, **kwargs): if instance is not None: registry.set_default_instance(instance) return registry - - -def connect_instance(name=None, *args, **kwargs): - """ - Connects with Syncano API and tries to load instance with provided name. - - :type name: string - :param name: Chosen instance name - - :type email: string - :param email: Your Syncano account email address - - :type password: string - :param password: Your Syncano password - - :type api_key: string - :param api_key: Your Syncano account key or instance api_key - - :type username: string - :param username: Your Syncano username - - :type instance_name: string - :param instance_name: Your Syncano instance_name - - :type verify_ssl: boolean - :param verify_ssl: Verify SSL certificate - - :rtype: :class:`syncano.models.base.Instance` - :return: Instance object - - Usage:: - - # For Admin - my_instance = syncano.connect_instance('my_instance_name', email='', password='') - # OR - my_instance = syncano.connect_instance('my_instance_name', api_key='') - - # For User - my_instance = syncano.connect_instance(username='', password='', api_key='', instance_name='') - # OR - my_instance = syncano.connect_instance(user_key='', api_key='', instance_name='') - """ - name = name or kwargs.get('instance_name', INSTANCE) - kwargs['instance_name'] = name - connection = connect(*args, **kwargs) - return connection.Instance.please.get(name) diff --git a/tests/test_connection.py b/tests/test_connection.py index b51f9fb..95e56b6 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -2,7 +2,7 @@ import unittest from urlparse import urljoin -from syncano import connect, connect_instance +from syncano import connect from syncano.connection import Connection, ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValueError from syncano.models.registry import registry @@ -37,46 +37,6 @@ def test_env_instance(self, instance_mock, registry_mock, *args): registry_mock.set_default_instance.assert_called_once_with(instance_mock) -class ConnectInstanceTestCase(unittest.TestCase): - - @mock.patch('syncano.connect') - def test_connect_instance(self, connect_mock): - connect_mock.return_value = connect_mock - get_mock = connect_mock.Instance.please.get - get_mock.return_value = get_mock - - self.assertFalse(connect_mock.called) - self.assertFalse(get_mock.called) - - instance = connect_instance('test-name', a=1, b=2) - - self.assertTrue(connect_mock.called) - self.assertTrue(get_mock.called) - - connect_mock.assert_called_once_with(a=1, b=2, instance_name='test-name') - get_mock.assert_called_once_with('test-name') - self.assertEqual(instance, get_mock) - - @mock.patch('syncano.connect') - @mock.patch('syncano.INSTANCE') - def test_env_connect_instance(self, instance_mock, connect_mock): - connect_mock.return_value = connect_mock - get_mock = connect_mock.Instance.please.get - get_mock.return_value = get_mock - - self.assertFalse(connect_mock.called) - self.assertFalse(get_mock.called) - - instance = connect_instance(a=1, b=2) - - self.assertTrue(connect_mock.called) - self.assertTrue(get_mock.called) - - connect_mock.assert_called_once_with(a=1, b=2, instance_name=instance_mock) - get_mock.assert_called_once_with(instance_mock) - self.assertEqual(instance, get_mock) - - class ConnectionTestCase(unittest.TestCase): def _get_response_mock(self, **kwargs): From 2fb3d8b067fa0cab1c12999184447681a2028b03 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 14:35:07 +0100 Subject: [PATCH 255/558] [LIB-311] rework count DataObject manager method; --- syncano/models/manager.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 82a4c63..6d9f74a 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -900,15 +900,20 @@ 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: Two elements tuple with objects list and count: objects, count = DataObjects.please.list(...).count(); """ self.method = 'GET' self.query.update({ 'include_count': True, - 'page_size': 0, + 'page_size': 20, }) response = self.request() - return response['objects_count'] + raw_objects = response['objects'] + objects = [] + for raw_data in raw_objects: + raw_data.update(self.properties) + objects.append(self.model(**raw_data)) + return objects, response['objects_count'] @clone def filter(self, **kwargs): From 28f646da63dff41d45139ed91e8dcb985295748d Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 15:23:48 +0100 Subject: [PATCH 256/558] [LIB-151] extend get_account_info method with user info --- syncano/connection.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 9f365e4..061b25b 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -69,6 +69,7 @@ class Connection(object): SOCIAL_AUTH_SUFFIX = AUTH_SUFFIX + '/{social_backend}/' USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' + USER_INFO_SUFFIX = 'v1/instances/{name}/user/' LOGIN_PARAMS = {'email', 'password'} @@ -379,12 +380,21 @@ def authenticate_user(self, **kwargs): self.user_key = response.get('user_key') return self.user_key - def get_account_info(self, api_key=None): - if api_key: - self.api_key = api_key + def get_account_info(self, api_key=None, user_key=None): + self.api_key = api_key if api_key else self.api_key + self.user_key = user_key if user_key else self.user_key + if not self.api_key: raise SyncanoValueError('api_key is required.') - return self.make_request('GET', self.ACCOUNT_SUFFIX, headers={'X-API-KEY': self.api_key}) + + if self.api_key and not self.user_key: + return self.make_request('GET', self.ACCOUNT_SUFFIX, headers={'X-API-KEY': self.api_key}) + # user profile info case + if not self.instance_name: + raise SyncanoValueError('Instance name not provided.') + + return self.make_request('GET', self.USER_INFO_SUFFIX.format(name=self.instance_name), headers={ + 'X-API-KEY': self.api_key, 'X-USER-KEY': self.user_key}) class ConnectionMixin(object): From f72a676649f667b586aeb4929f953245e2d9bc8b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Jan 2016 15:35:07 +0100 Subject: [PATCH 257/558] [LIB-151] Re-work get_user_info --- syncano/connection.py | 18 +++++++++++++----- tests/test_connection.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 061b25b..94c8890 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -380,18 +380,26 @@ def authenticate_user(self, **kwargs): self.user_key = response.get('user_key') return self.user_key - def get_account_info(self, api_key=None, user_key=None): + def get_account_info(self, api_key=None): + self.api_key = api_key if api_key else self.api_key + + if not self.api_key: + raise SyncanoValueError('api_key is required.') + + return self.make_request('GET', self.ACCOUNT_SUFFIX, headers={'X-API-KEY': self.api_key}) + + def get_user_info(self, api_key=None, user_key=None): self.api_key = api_key if api_key else self.api_key self.user_key = user_key if user_key else self.user_key if not self.api_key: raise SyncanoValueError('api_key is required.') - if self.api_key and not self.user_key: - return self.make_request('GET', self.ACCOUNT_SUFFIX, headers={'X-API-KEY': self.api_key}) - # user profile info case + if not self.user_key: + raise SyncanoValueError('user_key is required.') + if not self.instance_name: - raise SyncanoValueError('Instance name not provided.') + raise SyncanoValueError('instance_name is required.') return self.make_request('GET', self.USER_INFO_SUFFIX.format(name=self.instance_name), headers={ 'X-API-KEY': self.api_key, 'X-USER-KEY': self.user_key}) diff --git a/tests/test_connection.py b/tests/test_connection.py index b51f9fb..99d4ce4 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -362,6 +362,44 @@ def test_get_account_info_missing_key(self, make_request): self.assertFalse(make_request.called) self.assertIn('api_key', e.message) + @mock.patch('syncano.connection.Connection.make_request') + def test_get_user_info(self, make_request_mock): + info = {u'profile': {}} + make_request_mock.return_value = info + self.assertFalse(make_request_mock.called) + self.connection.api_key = 'Ala has a cat' + self.connection.user_key = 'Tom has a cat also' + self.connection.instance_name = 'tom_ala' + ret = self.connection.get_user_info() + self.assertTrue(make_request_mock.called) + self.assertEqual(info, ret) + + @mock.patch('syncano.connection.Connection.make_request') + def test_get_user_info_without_instance(self, make_request_mock): + info = {u'profile': {}} + make_request_mock.return_value = info + self.assertFalse(make_request_mock.called) + self.connection.api_key = 'Ala has a cat' + self.connection.user_key = 'Tom has a cat also' + self.connection.instance_name = None + with self.assertRaises(SyncanoValueError): + self.connection.get_user_info() + + @mock.patch('syncano.connection.Connection.make_request') + def test_get_user_info_without_auth_keys(self, make_request_mock): + info = {u'profile': {}} + make_request_mock.return_value = info + self.assertFalse(make_request_mock.called) + + self.connection.api_key = None + with self.assertRaises(SyncanoValueError): + self.connection.get_user_info() + + self.connection.api_key = 'Ala has a cat' + self.connection.user_key = None + with self.assertRaises(SyncanoValueError): + self.connection.get_user_info() + class DefaultConnectionTestCase(unittest.TestCase): From 899e8287197fe2ba8b277af26de2c97f9e4f108e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 20 Jan 2016 11:30:06 +0100 Subject: [PATCH 258/558] [LIB-447] fixes after QA --- syncano/models/incentives.py | 4 ++-- tests/integration_test_reponse_templates.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 0902ba7..ad72662 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -302,7 +302,7 @@ class ResponseTemplate(Model): OO wrapper around templates. :ivar name: :class:`~syncano.models.fields.StringField` - :ivar content: :class:`~syncano.models.fields.IntegerField` + :ivar content: :class:`~syncano.models.fields.StringField` :ivar content_type: :class:`~syncano.models.fields.StringField` :ivar context: :class:`~syncano.models.fields.JSONField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` @@ -341,7 +341,7 @@ class Meta: } def render(self, context=None): - context = context if context else {} + context = context or {} properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('render', properties) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index 54b2562..a145c95 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -17,10 +17,10 @@ def setUpClass(cls): def test_retrieve_api(self): template = ResponseTemplate.please.get(name='to_update') self.assertTrue(isinstance(template, ResponseTemplate)) - self.assertTrue(template.name) - self.assertTrue(template.content) - self.assertTrue(template.content_type) - self.assertTrue(template.context) + self.assertEqual(template.name, 'to_update') + self.assertEqual(template.content, '
    ') + self.assertEqual(template.content_type, 'text/html') + self.assertEqual(template.context, {'one': 1}) def test_create_api(self): template = ResponseTemplate.please.create(name='just_created', content='
    ', content_type='text/html', From dbb9cbda48d74df7f7640fcff569d14d0a181966 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 20 Jan 2016 11:38:48 +0100 Subject: [PATCH 259/558] [LIB-151] fixes after qa --- syncano/connection.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 94c8890..20fe546 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -381,7 +381,7 @@ def authenticate_user(self, **kwargs): return self.user_key def get_account_info(self, api_key=None): - self.api_key = api_key if api_key else self.api_key + self.api_key = api_key or self.api_key if not self.api_key: raise SyncanoValueError('api_key is required.') @@ -389,17 +389,12 @@ def get_account_info(self, api_key=None): return self.make_request('GET', self.ACCOUNT_SUFFIX, headers={'X-API-KEY': self.api_key}) def get_user_info(self, api_key=None, user_key=None): - self.api_key = api_key if api_key else self.api_key - self.user_key = user_key if user_key else self.user_key + self.api_key = api_key or self.api_key + self.user_key = user_key or self.user_key - if not self.api_key: - raise SyncanoValueError('api_key is required.') - - if not self.user_key: - raise SyncanoValueError('user_key is required.') - - if not self.instance_name: - raise SyncanoValueError('instance_name is required.') + for attribute_name in ['api_key', 'user_key', 'instance_name']: + if not getattr(self, attribute_name, None): + SyncanoValueError('{attribute_name} is required.'.format(attribute_name=attribute_name)) return self.make_request('GET', self.USER_INFO_SUFFIX.format(name=self.instance_name), headers={ 'X-API-KEY': self.api_key, 'X-USER-KEY': self.user_key}) From 0f9faf17514aa7bc7d76cf040044a1fed97e2fd4 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 20 Jan 2016 11:42:05 +0100 Subject: [PATCH 260/558] [LIB-151] Add raise :) forget about it --- syncano/connection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 20fe546..81c62d1 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -392,9 +392,9 @@ def get_user_info(self, api_key=None, user_key=None): self.api_key = api_key or self.api_key self.user_key = user_key or self.user_key - for attribute_name in ['api_key', 'user_key', 'instance_name']: + for attribute_name in ('api_key', 'user_key', 'instance_name'): if not getattr(self, attribute_name, None): - SyncanoValueError('{attribute_name} is required.'.format(attribute_name=attribute_name)) + raise SyncanoValueError('{attribute_name} is required.'.format(attribute_name=attribute_name)) return self.make_request('GET', self.USER_INFO_SUFFIX.format(name=self.instance_name), headers={ 'X-API-KEY': self.api_key, 'X-USER-KEY': self.user_key}) From e6635c51537e8db7ad0e1f1f081d94460b449a2e Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 20 Jan 2016 13:18:06 +0100 Subject: [PATCH 261/558] [release-4.1.0] version bump --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 5426e46..5a490d8 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.0.10' +__version__ = '4.1.0' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From e4fb8cc00c82eb5cf313e1bcffe9e45244c191d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 22 Jan 2016 14:25:11 +0100 Subject: [PATCH 262/558] [LIB-311] rework count with objects method on data objects manager --- syncano/models/manager.py | 24 ++++++++++++++++++++++-- tests/integration_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 6d9f74a..1158620 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -900,12 +900,32 @@ 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: Two elements tuple with objects list and count: objects, count = DataObjects.please.list(...).count(); + :return: The count of the returned objects: count = DataObjects.please.list(...).count(); """ self.method = 'GET' self.query.update({ 'include_count': True, - 'page_size': 20, + 'page_size': 0, + }) + response = self.request() + return response['objects_count'] + + @clone + def with_count(self, pagination_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(pagination_size=30) + Object.please.all(instance_name='raptor', class_name='some_class').with_count() + :param pagination_size: The size of the pagination; Default to 20; + :return: The tuple with objects and the count: objects, count = DataObjects.please.list(...).with_count(); + """ + self.method = 'GET' + self.query.update({ + 'include_count': True, + 'page_size': pagination_size, }) response = self.request() raw_objects = response['objects'] diff --git a/tests/integration_test.py b/tests/integration_test.py index 1acca7e..7f767a0 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -278,6 +278,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.class_name).count() + self.assertEqual(count, 2) + + objects, count = Object.please.list(instance_name=self.instance.name, + class_name=self.author.class_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 From 495f3f3791de0d08efc2b2d6c910cc8c5d4b116c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 22 Jan 2016 14:30:03 +0100 Subject: [PATCH 263/558] [LIB-311] rework count with objects method on data objects manager --- tests/integration_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 7f767a0..dd506f1 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -289,11 +289,11 @@ def test_count_and_with_count(self): # just created two authors - count = Object.please.list(instance_name=self.instance.name, class_name=self.author.class_name).count() + 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.class_name).with_count() + class_name=self.author.name).with_count() self.assertEqual(count, 2) for o in objects: From 0df4791e011c3fad87f44ef298d623ab658efa60 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 25 Jan 2016 14:38:31 +0100 Subject: [PATCH 264/558] [LIB-311] worked out the python lib objects with count; --- syncano/models/manager.py | 41 ++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 1158620..01c470b 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -801,7 +801,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') @@ -819,6 +819,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) @@ -887,6 +890,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) @@ -911,29 +918,27 @@ def count(self): return response['objects_count'] @clone - def with_count(self, pagination_size=20): + 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(pagination_size=30) + 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 pagination_size: The size of the pagination; Default to 20; + :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(); """ - self.method = 'GET' - self.query.update({ + query_data = { 'include_count': True, - 'page_size': pagination_size, - }) + 'page_size': page_size, + } + + self.method = 'GET' + self.query.update(query_data) response = self.request() - raw_objects = response['objects'] - objects = [] - for raw_data in raw_objects: - raw_data.update(self.properties) - objects.append(self.model(**raw_data)) - return objects, response['objects_count'] + self._initial_response = response + return self, self.__initial_response['objects_count'] @clone def filter(self, **kwargs): @@ -987,6 +992,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) @@ -1059,6 +1067,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): From 58082934e5996d7fe28bcd03ffc60e09232336fd Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 25 Jan 2016 14:40:28 +0100 Subject: [PATCH 265/558] [LIB-311] worked out the python lib objects with count; --- syncano/models/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 01c470b..0c1bc13 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -1067,7 +1067,7 @@ 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 From 92d4de5007608b3acddfd9244ec10d5aa4ecfdc0 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 25 Jan 2016 14:43:02 +0100 Subject: [PATCH 266/558] [LIB-311] worked out the python lib objects with count; correct typo; --- syncano/models/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 0c1bc13..e485b62 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -938,7 +938,7 @@ def with_count(self, page_size=20): self.query.update(query_data) response = self.request() self._initial_response = response - return self, self.__initial_response['objects_count'] + return self, self._initial_response['objects_count'] @clone def filter(self, **kwargs): From 9836030829fa71686e3e6455d19068a80efac084 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 3 Feb 2016 13:46:10 +0100 Subject: [PATCH 267/558] [LIB-487] correct to_native in DateTime Field; use strftime insread of isoformat --- syncano/models/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index cf3029a..cdf8704 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -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.strfitme(self.FORMAT) if ret.endswith('+00:00'): ret = ret[:-6] + 'Z' From b0e4957ba24f23a406d6ae293204d70868ab0d3b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 3 Feb 2016 13:48:23 +0100 Subject: [PATCH 268/558] [LIB-487] correct typo --- syncano/models/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index cdf8704..258cd21 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -375,7 +375,7 @@ def parse_from_date(self, value): def to_native(self, value): if value is None: return - ret = value.strfitme(self.FORMAT) + ret = value.strftime(self.FORMAT) if ret.endswith('+00:00'): ret = ret[:-6] + 'Z' From 44aa32e7058a0ae0e18ec9d1340a1c356da93827 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 3 Feb 2016 14:38:16 +0100 Subject: [PATCH 269/558] [LIB-488] correct to_native model method --- syncano/models/archetypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index baec78c..080bd2a 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -242,7 +242,7 @@ 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: From 98485f869f6944dd00709839d614bd1e23a3ef8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 19 Feb 2016 10:52:17 +0100 Subject: [PATCH 270/558] [LIB-361] add allow_anonymous_read to ApiKey in library; --- syncano/models/instances.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index f972f2a..1d9cac7 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -72,6 +72,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: From ca3c59cc54a768eef40011b1a5136b43a309082d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 19 Feb 2016 11:19:24 +0100 Subject: [PATCH 271/558] [LIB-361] add integration test for api key; --- tests/integration_test.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index dd506f1..49a5a78 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -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): @@ -454,3 +454,38 @@ 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, + ) + self._assert_api_key_flags(api_key_id=api_key.id) + + def test_api_key_flags_update(self): + api_key = self.model.please.create( + allow_user_create=True, + ignore_acl=True, + allow_anonymous_read=True, + ) + + self._assert_api_key_flags(api_key_id=api_key.id) + + api_key.allow_user_create = False + api_key.ignore_acl = False + api_key.allow_anonymous_read = False + api_key.save() + + self._assert_api_key_flags(api_key_id=api_key.id, checked_value=False) + + def _assert_api_key_flags(self, api_key_id, checked_value=True): + reloaded_api_key = self.model.please.get(id=api_key_id) + + self.assertTrue(reloaded_api_key.allow_user_create, checked_value) + self.assertTrue(reloaded_api_key.ignore_acl, checked_value) + self.assertTrue(reloaded_api_key.allow_anonymous_read, checked_value) From 246bdee368457a6851e5ed2018fc3615fd826e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 19 Feb 2016 11:29:33 +0100 Subject: [PATCH 272/558] [LIB-361] add integration test for api key; --- tests/integration_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration_test.py b/tests/integration_test.py index 49a5a78..4beba6f 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -464,6 +464,7 @@ def test_api_key_flags(self): allow_user_create=True, ignore_acl=True, allow_anonymous_read=True, + instance_name=self.instance.name, ) self._assert_api_key_flags(api_key_id=api_key.id) @@ -472,6 +473,7 @@ def test_api_key_flags_update(self): allow_user_create=True, ignore_acl=True, allow_anonymous_read=True, + instance_name=self.instance.name, ) self._assert_api_key_flags(api_key_id=api_key.id) From 0db1140f7e473fd1f4ca99a2a4b7d81e351b902e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 19 Feb 2016 11:32:46 +0100 Subject: [PATCH 273/558] [LIB-361] add integration test for api key; --- tests/integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 4beba6f..b4351ec 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -486,7 +486,7 @@ def test_api_key_flags_update(self): self._assert_api_key_flags(api_key_id=api_key.id, checked_value=False) def _assert_api_key_flags(self, api_key_id, checked_value=True): - reloaded_api_key = self.model.please.get(id=api_key_id) + reloaded_api_key = self.model.please.get(id=api_key_id, instance_name=self.instance.name) self.assertTrue(reloaded_api_key.allow_user_create, checked_value) self.assertTrue(reloaded_api_key.ignore_acl, checked_value) From a3d160b7816de0c0c803b28b8c34bd3c42ce7ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 19 Feb 2016 11:36:58 +0100 Subject: [PATCH 274/558] [LIB-361] allow to edit ApiKey - change allowed methods in endpoints; --- syncano/models/instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 1d9cac7..e61bb13 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -79,7 +79,7 @@ class Meta: parent = Instance endpoints = { 'detail': { - 'methods': ['get', 'delete'], + 'methods': ['get', 'put', 'patch', 'delete'], 'path': '/api_keys/{id}/', }, 'list': { From 3e58fa8e5ecfda2a2e48d9cec8585d6bc8e2c786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 19 Feb 2016 11:48:06 +0100 Subject: [PATCH 275/558] [LIB-361] well - edit an ApiKey is not possible - remove changes; --- syncano/models/instances.py | 2 +- tests/integration_test.py | 27 ++++----------------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index e61bb13..1d9cac7 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -79,7 +79,7 @@ class Meta: parent = Instance endpoints = { 'detail': { - 'methods': ['get', 'put', 'patch', 'delete'], + 'methods': ['get', 'delete'], 'path': '/api_keys/{id}/', }, 'list': { diff --git a/tests/integration_test.py b/tests/integration_test.py index b4351ec..62c56d7 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -466,28 +466,9 @@ def test_api_key_flags(self): allow_anonymous_read=True, instance_name=self.instance.name, ) - self._assert_api_key_flags(api_key_id=api_key.id) - def test_api_key_flags_update(self): - api_key = self.model.please.create( - allow_user_create=True, - ignore_acl=True, - allow_anonymous_read=True, - instance_name=self.instance.name, - ) - - self._assert_api_key_flags(api_key_id=api_key.id) - - api_key.allow_user_create = False - api_key.ignore_acl = False - api_key.allow_anonymous_read = False - api_key.save() - - self._assert_api_key_flags(api_key_id=api_key.id, checked_value=False) - - def _assert_api_key_flags(self, api_key_id, checked_value=True): - reloaded_api_key = self.model.please.get(id=api_key_id, 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, checked_value) - self.assertTrue(reloaded_api_key.ignore_acl, checked_value) - self.assertTrue(reloaded_api_key.allow_anonymous_read, checked_value) + 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) From 493adc9daaa1971cbaacb53436673e4efcf309e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 19 Feb 2016 12:15:10 +0100 Subject: [PATCH 276/558] [LIB-453] add rename method on Instance Model; --- syncano/models/instances.py | 13 +++++++++++++ tests/integration_test.py | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index f972f2a..c4e7333 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -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): """ diff --git a/tests/integration_test.py b/tests/integration_test.py index dd506f1..9e69403 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -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 From 277f37e24be1d220fde35af32c54c7fe20c36b33 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 4 Mar 2016 11:50:30 +0100 Subject: [PATCH 277/558] [LIB-516] rename models and field to comply with sockets functionality --- syncano/models/billing.py | 8 +- syncano/models/custom_response.py | 16 ++-- syncano/models/data_views.py | 13 ++-- syncano/models/incentives.py | 113 +++++++++++++++-------------- syncano/models/instances.py | 8 +- syncano/models/manager.py | 14 ++-- syncano/models/traces.py | 11 +-- tests/integration_test.py | 82 ++++++++++----------- tests/integration_test_accounts.py | 2 +- tests/test_channels.py | 4 +- tests/test_fields.py | 4 +- tests/test_incentives.py | 35 ++++----- tests/test_manager.py | 32 ++++---- tests/test_models.py | 16 ++-- tests/test_options.py | 8 +- tests/test_push.py | 14 ++-- 16 files changed, 191 insertions(+), 189 deletions(-) diff --git a/syncano/models/billing.py b/syncano/models/billing.py index 5fd9fd2..91ba221 100644 --- a/syncano/models/billing.py +++ b/syncano/models/billing.py @@ -37,11 +37,11 @@ class Meta: endpoints = { 'detail': { 'methods': ['get', 'delete'], - 'path': '/v1/billing/coupons/{name}/', + 'path': '/v1.1/billing/coupons/{name}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/v1/billing/coupons/', + 'path': '/v1.1/billing/coupons/', } } @@ -71,10 +71,10 @@ class Meta: endpoints = { 'detail': { 'methods': ['get'], - 'path': '/v1/billing/discounts/{id}/', + 'path': '/v1.1/billing/discounts/{id}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/v1/billing/discounts/', + 'path': '/v1.1/billing/discounts/', } } diff --git a/syncano/models/custom_response.py b/syncano/models/custom_response.py index 7e07327..e9fa71d 100644 --- a/syncano/models/custom_response.py +++ b/syncano/models/custom_response.py @@ -8,13 +8,13 @@ class CustomResponseHandler(object): A helper class which allows to define and maintain custom response handlers. Consider an example: - CodeBox code: + Script code: >> set_response(HttpResponse(status_code=200, content='{"one": 1}', content_type='application/json')) - When suitable CodeBoxTrace is used: + When suitable ScriptTrace is used: - >> trace = CodeBoxTrace.please.get(id=, codebox_id=) + >> trace = ScriptTrace.please.get(id=, script=) Then trace object will have a content attribute, which will be a dict created from json (simple: json.loads under the hood); @@ -35,7 +35,7 @@ def custom_handler(response): or globally: - CodeBoxTrace.response_handler.overwrite_handler('application/json', custom_handler) + ScriptTrace.response_handler.overwrite_handler('application/json', custom_handler) Then trace.content is equal to: >> 1 @@ -96,10 +96,10 @@ def plain_handler(response): class CustomResponseMixin(object): """ - A mixin which extends the CodeBox and Webhook traces (and any other Model - if used) with following fields: - * content - This is the response data if set_response is used in CodeBox code, otherwise it is the 'stdout' field; - * content_type - The content_type specified by the user in CodeBox code; - * status_code - The status_code specified by the user in CodeBox code; + A mixin which extends the Script and ScriptEndpoint traces (and any other Model - if used) with following fields: + * content - This is the response data if set_response is used in Script code, otherwise it is the 'stdout' field; + * content_type - The content_type specified by the user in Script code; + * status_code - The status_code specified by the user in Script code; * error - An error which can occur when code is executed: the stderr response field; To process the content based on content_type this Mixin uses the CustomResponseHandler - see the docs there. diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 22bd543..2bd0d5c 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -5,7 +5,7 @@ from .instances import Instance -class DataView(Model): +class EndpointData(Model): """ :ivar name: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` @@ -46,27 +46,26 @@ class DataView(Model): class Meta: parent = Instance - plural_name = 'DataViews' endpoints = { 'detail': { 'methods': ['get', 'put', 'patch', 'delete'], - 'path': '/api/objects/{name}/', + 'path': '/endpoints/data/{name}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/api/objects/', + 'path': '/endpoints/data/', }, 'get': { 'methods': ['get'], - 'path': '/api/objects/{name}/get/', + 'path': '/endpoints/data/{name}/get/', }, 'rename': { 'methods': ['post'], - 'path': '/api/objects/{name}/rename/', + 'path': '/endpoints/data/{name}/rename/', }, 'clear_cache': { 'methods': ['post'], - 'path': '/api/objects/{name}/clear_cache/', + 'path': '/endpoints/data/{name}/clear_cache/', } } diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index ad72662..200629c 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -7,12 +7,12 @@ from . import fields from .base import Model from .instances import Instance -from .manager import CodeBoxManager, WebhookManager +from .manager import ScriptManager, ScriptEndpointManager -class CodeBox(Model): +class Script(Model): """ - OO wrapper around codeboxes `endpoint `_. + OO wrapper around scripts `endpoint `_. :ivar label: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` @@ -24,17 +24,17 @@ class CodeBox(Model): :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` .. note:: - **CodeBox** has special method called ``run`` which will execute attached source code:: + **Script** has special method called ``run`` which will execute attached source code:: - >>> CodeBox.please.run('instance-name', 1234) - >>> CodeBox.please.run('instance-name', 1234, payload={'variable_one': 1, 'variable_two': 2}) - >>> CodeBox.please.run('instance-name', 1234, payload="{\"variable_one\": 1, \"variable_two\": 2}") + >>> Script.please.run('instance-name', 1234) + >>> Script.please.run('instance-name', 1234, payload={'variable_one': 1, 'variable_two': 2}) + >>> Script.please.run('instance-name', 1234, payload="{\"variable_one\": 1, \"variable_two\": 2}") or via instance:: - >>> cb = CodeBox.please.get('instance-name', 1234) - >>> cb.run() - >>> cb.run(variable_one=1, variable_two=2) + >>> s = Script.please.get('instance-name', 1234) + >>> s.run() + >>> s.run(variable_one=1, variable_two=2) """ LINKS = ( {'type': 'detail', 'name': 'self'}, @@ -60,24 +60,24 @@ class CodeBox(Model): created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) - please = CodeBoxManager() + please = ScriptManager() class Meta: parent = Instance - name = 'Codebox' - plural_name = 'Codeboxes' + name = 'Script' + plural_name = 'Scripts' endpoints = { 'detail': { 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/codeboxes/{id}/', + 'path': '/snippets/scripts/{id}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/codeboxes/', + 'path': '/snippets/scripts/', }, 'run': { 'methods': ['post'], - 'path': '/codeboxes/{id}/run/', + 'path': '/snippets/scripts/{id}/run/', }, } @@ -85,11 +85,11 @@ def run(self, **payload): """ Usage:: - >>> cb = CodeBox.please.get('instance-name', 1234) - >>> cb.run() - >>> cb.run(variable_one=1, variable_two=2) + >>> s = Script.please.get('instance-name', 1234) + >>> s.run() + >>> s.run(variable_one=1, variable_two=2) """ - from .traces import CodeBoxTrace + from .traces import ScriptTrace if self.is_new(): raise SyncanoValidationError('Method allowed only on existing model.') @@ -103,16 +103,16 @@ def run(self, **payload): } } response = connection.request('POST', endpoint, **request) - response.update({'instance_name': self.instance_name, 'codebox_id': self.id}) - return CodeBoxTrace(**response) + response.update({'instance_name': self.instance_name, 'script_id': self.id}) + return ScriptTrace(**response) class Schedule(Model): """ - OO wrapper around codebox schedules `endpoint `_. + OO wrapper around script schedules `endpoint `_. :ivar label: :class:`~syncano.models.fields.StringField` - :ivar codebox: :class:`~syncano.models.fields.IntegerField` + :ivar script: :class:`~syncano.models.fields.IntegerField` :ivar interval_sec: :class:`~syncano.models.fields.IntegerField` :ivar crontab: :class:`~syncano.models.fields.StringField` :ivar payload: :class:`~syncano.models.fields.HyperliStringFieldnkedField` @@ -122,12 +122,12 @@ class Schedule(Model): """ LINKS = [ {'type': 'detail', 'name': 'self'}, - {'type': 'detail', 'name': 'codebox'}, + {'type': 'detail', 'name': 'script'}, {'type': 'list', 'name': 'traces'}, ] label = fields.StringField(max_length=80) - codebox = fields.IntegerField(label='codebox id') + script = fields.IntegerField(label='script id') interval_sec = fields.IntegerField(read_only=False, required=False) crontab = fields.StringField(max_length=40, required=False) payload = fields.StringField(required=False) @@ -154,7 +154,7 @@ class Trigger(Model): OO wrapper around triggers `endpoint `_. :ivar label: :class:`~syncano.models.fields.StringField` - :ivar codebox: :class:`~syncano.models.fields.IntegerField` + :ivar script: :class:`~syncano.models.fields.IntegerField` :ivar class_name: :class:`~syncano.models.fields.StringField` :ivar signal: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` @@ -163,7 +163,7 @@ class Trigger(Model): """ LINKS = ( {'type': 'detail', 'name': 'self'}, - {'type': 'detail', 'name': 'codebox'}, + {'type': 'detail', 'name': 'script'}, {'type': 'detail', 'name': 'class_name'}, {'type': 'list', 'name': 'traces'}, ) @@ -174,7 +174,7 @@ class Trigger(Model): ) label = fields.StringField(max_length=80) - codebox = fields.IntegerField(label='codebox id') + script = fields.IntegerField(label='script id') class_name = fields.StringField(label='class name', mapping='class') signal = fields.ChoiceField(choices=SIGNAL_CHOICES) links = fields.HyperlinkedField(links=LINKS) @@ -195,65 +195,66 @@ class Meta: } -class Webhook(Model): +class ScriptEndpoint(Model): + # TODO: update docs when ready; """ - OO wrapper around webhooks `endpoint `_. + OO wrapper around script endpoints `endpoint `_. :ivar name: :class:`~syncano.models.fields.SlugField` - :ivar codebox: :class:`~syncano.models.fields.IntegerField` + :ivar script: :class:`~syncano.models.fields.IntegerField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` .. note:: - **WebHook** has special method called ``run`` which will execute related codebox:: + **ScriptEndpoint** has special method called ``run`` which will execute related script:: - >>> Webhook.please.run('instance-name', 'webhook-name') - >>> Webhook.please.run('instance-name', 'webhook-name', payload={'variable_one': 1, 'variable_two': 2}) - >>> Webhook.please.run('instance-name', 'webhook-name', + >>> ScriptEndpoint.please.run('instance-name', 'script-name') + >>> ScriptEndpoint.please.run('instance-name', 'script-name', payload={'variable_one': 1, 'variable_two': 2}) + >>> ScriptEndpoint.please.run('instance-name', 'script-name', payload="{\"variable_one\": 1, \"variable_two\": 2}") or via instance:: - >>> wh = Webhook.please.get('instance-name', 'webhook-name') - >>> wh.run() - >>> wh.run(variable_one=1, variable_two=2) + >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') + >>> se.run() + >>> se.run(variable_one=1, variable_two=2) """ LINKS = ( {'type': 'detail', 'name': 'self'}, - {'type': 'detail', 'name': 'codebox'}, + {'type': 'detail', 'name': 'script'}, {'type': 'list', 'name': 'traces'}, ) name = fields.SlugField(max_length=50, primary_key=True) - codebox = fields.IntegerField(label='codebox id') + script = fields.IntegerField(label='script id') public = fields.BooleanField(required=False, default=False) public_link = fields.ChoiceField(required=False, read_only=True) links = fields.HyperlinkedField(links=LINKS) - please = WebhookManager() + please = ScriptEndpointManager() class Meta: parent = Instance endpoints = { 'detail': { 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/webhooks/{name}/', + 'path': '/endpoints/scripts/{name}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/webhooks/', + 'path': '/endpoints/scripts/', }, 'run': { 'methods': ['post'], - 'path': '/webhooks/{name}/run/', + 'path': '/endpoints/scripts/{name}/run/', }, 'reset': { 'methods': ['post'], - 'path': '/webhooks/{name}/reset_link/', + 'path': '/endpoints/scripts/{name}/reset_link/', }, 'public': { 'methods': ['get'], - 'path': '/webhooks/p/{public_link}/{name}/', + 'path': '/endpoints/scripts/p/{public_link}/{name}/', } } @@ -261,11 +262,11 @@ def run(self, **payload): """ Usage:: - >>> wh = Webhook.please.get('instance-name', 'webhook-name') - >>> wh.run() - >>> wh.run(variable_one=1, variable_two=2) + >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') + >>> se.run() + >>> se.run(variable_one=1, variable_two=2) """ - from .traces import WebhookTrace + from .traces import ScriptEndpointTrace if self.is_new(): raise SyncanoValidationError('Method allowed only on existing model.') @@ -277,17 +278,17 @@ def run(self, **payload): response = connection.request('POST', endpoint, **{'data': payload}) if 'result' in response and 'stdout' in response['result']: response.update({'instance_name': self.instance_name, - 'webhook_name': self.name}) - return WebhookTrace(**response) - # if codebox is a custom one, return result 'as-it-is'; + 'script_name': self.name}) + return ScriptEndpointTrace(**response) + # if script is a custom one, return result 'as-it-is'; return response def reset_link(self): """ Usage:: - >>> wh = Webhook.please.get('instance-name', 'webhook-name') - >>> wh.reset_link() + >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') + >>> se.reset_link() """ properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('reset', properties) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 63e9f5e..7d1ec09 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -22,13 +22,13 @@ class Instance(Model): {'type': 'detail', 'name': 'self'}, {'type': 'list', 'name': 'admins'}, {'type': 'list', 'name': 'classes'}, - {'type': 'list', 'name': 'codeboxes'}, + {'type': 'list', 'name': 'scripts'}, {'type': 'list', 'name': 'invitations'}, {'type': 'list', 'name': 'runtimes'}, {'type': 'list', 'name': 'api_keys'}, {'type': 'list', 'name': 'triggers'}, {'type': 'list', 'name': 'users'}, - {'type': 'list', 'name': 'webhooks'}, + {'type': 'list', 'name': 'script_endpoints'}, {'type': 'list', 'name': 'schedules'}, {'type': 'list', 'name': 'templates'} ) @@ -46,11 +46,11 @@ class Meta: endpoints = { 'detail': { 'methods': ['delete', 'patch', 'put', 'get'], - 'path': '/v1/instances/{name}/', + 'path': '/v1.1/instances/{name}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/v1/instances/', + 'path': '/v1.1/instances/', } } diff --git a/syncano/models/manager.py b/syncano/models/manager.py index e485b62..d745f06 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -70,7 +70,7 @@ def __get__(self, instance, owner=None): class Manager(ConnectionMixin): """Base class responsible for all ORM (``please``) actions.""" - BATCH_URI = '/v1/instances/{name}/batch/' + BATCH_URI = '/v1.1/instances/{name}/batch/' def __init__(self): self.name = None @@ -833,10 +833,10 @@ def _get_endpoint_properties(self): return self.model._meta.resolve_endpoint(self.endpoint, defaults), defaults -class CodeBoxManager(Manager): +class ScriptManager(Manager): """ Custom :class:`~syncano.models.manager.Manager` - class for :class:`~syncano.models.base.CodeBox` model. + class for :class:`~syncano.models.base.Script` model. """ @clone @@ -852,13 +852,13 @@ def run(self, *args, **kwargs): self._filter(*args, **kwargs) self._serialize = False response = self.request() - return registry.CodeBoxTrace(**response) + return registry.ScriptTrace(**response) -class WebhookManager(Manager): +class ScriptEndpointManager(Manager): """ Custom :class:`~syncano.models.manager.Manager` - class for :class:`~syncano.models.base.Webhook` model. + class for :class:`~syncano.models.base.ScriptEndpoint` model. """ @clone @@ -876,7 +876,7 @@ def run(self, *args, **kwargs): response = self.request() # Workaround for circular import - return registry.WebhookTrace(**response) + return registry.ScriptEndpointTrace(**response) class ObjectManager(Manager): diff --git a/syncano/models/traces.py b/syncano/models/traces.py index 531c15f..933e686 100644 --- a/syncano/models/traces.py +++ b/syncano/models/traces.py @@ -3,10 +3,10 @@ from . import fields from .base import Model from .custom_response import CustomResponseMixin -from .incentives import CodeBox, Schedule, Trigger, Webhook +from .incentives import Script, Schedule, Trigger, ScriptEndpoint -class CodeBoxTrace(CustomResponseMixin, Model): +class ScriptTrace(CustomResponseMixin, Model): """ :ivar status: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` @@ -18,6 +18,7 @@ class CodeBoxTrace(CustomResponseMixin, Model): {'display_name': 'Success', 'value': 'success'}, {'display_name': 'Failure', 'value': 'failure'}, {'display_name': 'Timeout', 'value': 'timeout'}, + {'display_name': 'Processing', 'value': 'processing'}, {'display_name': 'Pending', 'value': 'pending'}, ) LINKS = ( @@ -31,7 +32,7 @@ class CodeBoxTrace(CustomResponseMixin, Model): duration = fields.IntegerField(read_only=True, required=False) class Meta: - parent = CodeBox + parent = Script endpoints = { 'detail': { 'methods': ['get'], @@ -120,7 +121,7 @@ class Meta: } -class WebhookTrace(CustomResponseMixin, Model): +class ScriptEndpointTrace(CustomResponseMixin, Model): """ :ivar status: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` @@ -145,7 +146,7 @@ class WebhookTrace(CustomResponseMixin, Model): duration = fields.IntegerField(read_only=True, required=False) class Meta: - parent = Webhook + parent = ScriptEndpoint endpoints = { 'detail': { 'methods': ['get'], diff --git a/tests/integration_test.py b/tests/integration_test.py index ab1d36b..d6a7e5c 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -7,7 +7,7 @@ import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError -from syncano.models import ApiKey, Class, CodeBox, Instance, Object, Webhook, registry +from syncano.models import ApiKey, Class, Script, Instance, Object, ScriptEndpoint, registry class IntegrationTest(unittest.TestCase): @@ -313,11 +313,11 @@ def test_count_and_with_count(self): class CodeboxIntegrationTest(InstanceMixin, IntegrationTest): - model = CodeBox + model = Script @classmethod def tearDownClass(cls): - for cb in cls.instance.codeboxes.all(): + for cb in cls.instance.scripts.all(): cb.delete() super(CodeboxIntegrationTest, cls).tearDownClass() @@ -327,44 +327,44 @@ def test_required_fields(self): list(self.model.please.all()) def test_list(self): - codeboxes = self.model.please.all(self.instance.name) - self.assertTrue(len(list(codeboxes)) >= 0) + scripts = self.model.please.all(self.instance.name) + self.assertTrue(len(list(scripts)) >= 0) def test_create(self): - codebox = self.model.please.create( + script = self.model.please.create( instance_name=self.instance.name, label='cb%s' % self.generate_hash()[:10], runtime_name='python', source='print "IntegrationTest"' ) - codebox.delete() + script.delete() def test_update(self): - codebox = self.model.please.create( + script = self.model.please.create( instance_name=self.instance.name, label='cb%s' % self.generate_hash()[:10], runtime_name='python', source='print "IntegrationTest"' ) - codebox.source = 'print "NotIntegrationTest"' - codebox.save() + script.source = 'print "NotIntegrationTest"' + script.save() - codebox2 = self.model.please.get(self.instance.name, codebox.pk) - self.assertEqual(codebox.source, codebox2.source) + script2 = self.model.please.get(self.instance.name, script.pk) + self.assertEqual(script.source, script2.source) - codebox.delete() + script.delete() def test_source_run(self): - codebox = self.model.please.create( + script = self.model.please.create( instance_name=self.instance.name, label='cb%s' % self.generate_hash()[:10], runtime_name='python', source='print "IntegrationTest"' ) - trace = codebox.run() + trace = script.run() while trace.status == 'pending': sleep(1) trace.reload() @@ -372,10 +372,10 @@ def test_source_run(self): self.assertEquals(trace.status, 'success') self.assertDictEqual(trace.result, {u'stderr': u'', u'stdout': u'IntegrationTest'}) - codebox.delete() + script.delete() def test_custom_response_run(self): - codebox = self.model.please.create( + script = self.model.please.create( instance_name=self.instance.name, label='cb%s' % self.generate_hash()[:10], runtime_name='python', @@ -383,7 +383,7 @@ def test_custom_response_run(self): set_response(HttpResponse(status_code=200, content='{"one": 1}', content_type='application/json'))""" ) - trace = codebox.run() + trace = script.run() while trace.status == 'pending': sleep(1) trace.reload() @@ -393,24 +393,24 @@ def test_custom_response_run(self): self.assertEqual(trace.content_type, 'application/json') self.assertEqual(trace.status_code, 200) - codebox.delete() + script.delete() -class WebhookIntegrationTest(InstanceMixin, IntegrationTest): - model = Webhook +class ScriptEndpointIntegrationTest(InstanceMixin, IntegrationTest): + model = ScriptEndpoint @classmethod def setUpClass(cls): - super(WebhookIntegrationTest, cls).setUpClass() + super(ScriptEndpointIntegrationTest, cls).setUpClass() - cls.codebox = CodeBox.please.create( + cls.script = Script.please.create( instance_name=cls.instance.name, label='cb%s' % cls.generate_hash()[:10], runtime_name='python', source='print "IntegrationTest"' ) - cls.custom_codebox = CodeBox.please.create( + cls.custom_script = Script.please.create( instance_name=cls.instance.name, label='cb%s' % cls.generate_hash()[:10], runtime_name='python', @@ -420,8 +420,8 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): - cls.codebox.delete() - super(WebhookIntegrationTest, cls).tearDownClass() + cls.script.delete() + super(ScriptEndpointIntegrationTest, cls).tearDownClass() def test_required_fields(self): with self.assertRaises(SyncanoValueError): @@ -429,40 +429,40 @@ def test_required_fields(self): list(self.model.please.all()) def test_list(self): - webhooks = self.model.please.all(self.instance.name) - self.assertTrue(len(list(webhooks)) >= 0) + script_endpoints = self.model.please.all(self.instance.name) + self.assertTrue(len(list(script_endpoints)) >= 0) def test_create(self): - webhook = self.model.please.create( + script_endpoint = self.model.please.create( instance_name=self.instance.name, - codebox=self.codebox.id, + script=self.script.id, name='wh%s' % self.generate_hash()[:10], ) - webhook.delete() + script_endpoint.delete() - def test_codebox_run(self): - webhook = self.model.please.create( + def test_script_run(self): + script_endpoint = self.model.please.create( instance_name=self.instance.name, - codebox=self.codebox.id, + script=self.script.id, name='wh%s' % self.generate_hash()[:10], ) - trace = webhook.run() + trace = script_endpoint.run() self.assertEquals(trace.status, 'success') self.assertDictEqual(trace.result, {u'stderr': u'', u'stdout': u'IntegrationTest'}) - webhook.delete() + script_endpoint.delete() - def test_custom_codebox_run(self): - webhook = self.model.please.create( + def test_custom_script_run(self): + script_endpoint = self.model.please.create( instance_name=self.instance.name, - codebox=self.custom_codebox.id, + script=self.custom_script.id, name='wh%s' % self.generate_hash()[:10], ) - trace = webhook.run() + trace = script_endpoint.run() self.assertDictEqual(trace, {'one': 1}) - webhook.delete() + script_endpoint.delete() class ApiKeyIntegrationTest(InstanceMixin, IntegrationTest): diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py index cd0ad7d..e6c02d2 100644 --- a/tests/integration_test_accounts.py +++ b/tests/integration_test_accounts.py @@ -34,7 +34,7 @@ def tearDownClass(cls): cls.connection = None def check_connection(self, con): - response = con.request('GET', '/v1/instances/test_login/classes/') + response = con.request('GET', '/v1.1/instances/test_login/classes/') obj_list = response['objects'] diff --git a/tests/test_channels.py b/tests/test_channels.py index 69489ba..326b7af 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -92,7 +92,7 @@ def test_poll(self, poll_thread_mock, connection_mock): self.assertTrue(connection_mock.called) poll_thread_mock.assert_called_once_with( connection_mock, - '/v1/instances/None/channels/None/poll/', + '/v1.1/instances/None/channels/None/poll/', 'c', 'd', last_id='b', @@ -113,6 +113,6 @@ def test_publish(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.request.assert_called_once_with( 'POST', - '/v1/instances/None/channels/None/publish/', + '/v1.1/instances/None/channels/None/publish/', data={'room': u'1', 'payload': '{"a": 1}'} ) diff --git a/tests/test_fields.py b/tests/test_fields.py index 6f6e1ea..6e9cf8f 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -64,11 +64,11 @@ class Meta: endpoints = { 'detail': { 'methods': ['delete', 'post', 'patch', 'get'], - 'path': '/v1/dummy/{dynamic_field}/', + 'path': '/v1.1/dummy/{dynamic_field}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/v1/dummy/', + 'path': '/v1.1/dummy/', } } diff --git a/tests/test_incentives.py b/tests/test_incentives.py index 435bfd1..69e3119 100644 --- a/tests/test_incentives.py +++ b/tests/test_incentives.py @@ -4,7 +4,7 @@ from datetime import datetime from syncano.exceptions import SyncanoValidationError -from syncano.models import CodeBox, CodeBoxTrace, ResponseTemplate, Webhook, WebhookTrace +from syncano.models import Script, ScriptTrace, ResponseTemplate, ScriptEndpoint, ScriptEndpointTrace try: from unittest import mock @@ -12,14 +12,14 @@ import mock -class CodeBoxTestCase(unittest.TestCase): +class ScriptTestCase(unittest.TestCase): def setUp(self): - self.model = CodeBox() + self.model = Script() - @mock.patch('syncano.models.CodeBox._get_connection') + @mock.patch('syncano.models.Script._get_connection') def test_run(self, connection_mock): - model = CodeBox(instance_name='test', id=10, links={'run': '/v1/instances/test/codeboxes/10/run/'}) + model = Script(instance_name='test', id=10, links={'run': '/v1.1/instances/test/snippets/scripts/10/run/'}) connection_mock.return_value = connection_mock connection_mock.request.return_value = {'id': 10} @@ -28,25 +28,26 @@ def test_run(self, connection_mock): result = model.run(a=1, b=2) self.assertTrue(connection_mock.called) self.assertTrue(connection_mock.request.called) - self.assertIsInstance(result, CodeBoxTrace) + self.assertIsInstance(result, ScriptTrace) connection_mock.assert_called_once_with(a=1, b=2) connection_mock.request.assert_called_once_with( - 'POST', '/v1/instances/test/codeboxes/10/run/', data={'payload': '{"a": 1, "b": 2}'} + 'POST', '/v1.1/instances/test/snippets/scripts/10/run/', data={'payload': '{"a": 1, "b": 2}'} ) - model = CodeBox() + model = Script() with self.assertRaises(SyncanoValidationError): model.run() -class WebhookTestCase(unittest.TestCase): +class ScriptEndpointTestCase(unittest.TestCase): def setUp(self): - self.model = Webhook() + self.model = ScriptEndpoint() - @mock.patch('syncano.models.Webhook._get_connection') + @mock.patch('syncano.models.ScriptEndpoint._get_connection') def test_run(self, connection_mock): - model = Webhook(instance_name='test', name='name', links={'run': '/v1/instances/test/webhooks/name/run/'}) + model = ScriptEndpoint(instance_name='test', name='name', + links={'run': '/v1.1/instances/test/endpoints/scripts/name/run/'}) connection_mock.return_value = connection_mock connection_mock.request.return_value = { 'status': 'success', @@ -60,7 +61,7 @@ def test_run(self, connection_mock): result = model.run(x=1, y=2) self.assertTrue(connection_mock.called) self.assertTrue(connection_mock.request.called) - self.assertIsInstance(result, WebhookTrace) + self.assertIsInstance(result, ScriptEndpointTrace) self.assertEqual(result.status, 'success') self.assertEqual(result.duration, 937) self.assertEqual(result.result, {u'stdout': 1, u'stderr': u''}) @@ -69,11 +70,11 @@ def test_run(self, connection_mock): connection_mock.assert_called_once_with(x=1, y=2) connection_mock.request.assert_called_once_with( 'POST', - '/v1/instances/test/webhooks/name/run/', + '/v1.1/instances/test/endpoints/scripts/name/run/', data={"y": 2, "x": 1} ) - model = Webhook() + model = ScriptEndpoint() with self.assertRaises(SyncanoValidationError): model.run() @@ -85,7 +86,7 @@ def setUp(self): @mock.patch('syncano.models.ResponseTemplate._get_connection') def test_render(self, connection_mock): model = self.model(instance_name='test', name='name', - links={'run': '/v1/instances/test/snippets/templates/name/render/'}) + links={'run': '/v1.1/instances/test/snippets/templates/name/render/'}) connection_mock.return_value = connection_mock connection_mock.request.return_value = '
    12345
    ' @@ -98,6 +99,6 @@ def test_render(self, connection_mock): connection_mock.request.assert_called_once_with( 'POST', - '/v1/instances/test/snippets/templates/name/render/', + '/v1.1/instances/test/snippets/templates/name/render/', data={'context': {}} ) diff --git a/tests/test_manager.py b/tests/test_manager.py index 7c234ce..dde9965 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -2,7 +2,7 @@ from datetime import datetime from syncano.exceptions import SyncanoDoesNotExist, SyncanoRequestError, SyncanoValueError -from syncano.models import CodeBox, CodeBoxTrace, Instance, Object, User, Webhook, WebhookTrace +from syncano.models import Script, ScriptTrace, Instance, Object, User, ScriptEndpoint, ScriptEndpointTrace try: from unittest import mock @@ -419,7 +419,7 @@ def test_request(self, connection_mock): request_mock.assert_called_once_with( 'GET', - u'/v1/instances/', + u'/v1.1/instances/', data={'b': 2}, params={'a': 1} ) @@ -481,15 +481,15 @@ def test_get_allowed_method(self): self.manager.get_allowed_method('dummy') -class CodeBoxManagerTestCase(unittest.TestCase): +class ScriptManagerTestCase(unittest.TestCase): def setUp(self): - self.model = CodeBox - self.manager = CodeBox.please + self.model = Script + self.manager = Script.please - @mock.patch('syncano.models.manager.CodeBoxManager.request') - @mock.patch('syncano.models.manager.CodeBoxManager._filter') - @mock.patch('syncano.models.manager.CodeBoxManager._clone') + @mock.patch('syncano.models.manager.ScriptManager.request') + @mock.patch('syncano.models.manager.ScriptManager._filter') + @mock.patch('syncano.models.manager.ScriptManager._clone') def test_run(self, clone_mock, filter_mock, request_mock): clone_mock.return_value = self.manager request_mock.return_value = {'id': 10} @@ -498,7 +498,7 @@ def test_run(self, clone_mock, filter_mock, request_mock): self.assertFalse(request_mock.called) result = self.manager.run(1, 2, a=1, b=2, payload={'x': 1, 'y': 2}) - self.assertIsInstance(result, CodeBoxTrace) + self.assertIsInstance(result, ScriptTrace) self.assertTrue(filter_mock.called) self.assertTrue(request_mock.called) @@ -511,15 +511,15 @@ def test_run(self, clone_mock, filter_mock, request_mock): self.assertEqual(self.manager.data['payload'], '{"y": 2, "x": 1}') -class WebhookManagerTestCase(unittest.TestCase): +class ScriptEndpointManagerTestCase(unittest.TestCase): def setUp(self): - self.model = Webhook - self.manager = Webhook.please + self.model = ScriptEndpoint + self.manager = ScriptEndpoint.please - @mock.patch('syncano.models.manager.WebhookManager.request') - @mock.patch('syncano.models.manager.WebhookManager._filter') - @mock.patch('syncano.models.manager.WebhookManager._clone') + @mock.patch('syncano.models.manager.ScriptEndpointManager.request') + @mock.patch('syncano.models.manager.ScriptEndpointManager._filter') + @mock.patch('syncano.models.manager.ScriptEndpointManager._clone') def test_run(self, clone_mock, filter_mock, request_mock): clone_mock.return_value = self.manager request_mock.return_value = { @@ -533,7 +533,7 @@ def test_run(self, clone_mock, filter_mock, request_mock): self.assertFalse(request_mock.called) result = self.manager.run(1, 2, a=1, b=2, payload={'x': 1, 'y': 2}) - self.assertIsInstance(result, WebhookTrace) + self.assertIsInstance(result, ScriptEndpointTrace) self.assertEqual(result.status, 'success') self.assertEqual(result.duration, 937) self.assertEqual(result.result, 1) diff --git a/tests/test_models.py b/tests/test_models.py index 0a2f14a..c6883c3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -74,7 +74,7 @@ def test_create(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.request.assert_called_with( 'POST', - '/v1/instances/', + '/v1.1/instances/', data={'name': 'test'} ) @@ -92,7 +92,7 @@ def test_update(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.request.assert_called_with( 'PUT', - '/v1/instances/test/', + '/v1.1/instances/test/', data={'name': 'test'} ) @@ -103,13 +103,13 @@ def test_update(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.request.assert_called_with( 'PUT', - '/v1/instances/test/', + '/v1.1/instances/test/', data={'name': 'test'} ) @mock.patch('syncano.models.Instance._get_connection') def test_delete(self, connection_mock): - model = Instance(name='test', links={'self': '/v1/instances/test/'}) + model = Instance(name='test', links={'self': '/v1.1/instances/test/'}) connection_mock.return_value = connection_mock self.assertFalse(connection_mock.called) @@ -119,7 +119,7 @@ def test_delete(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.assert_called_once_with() - connection_mock.request.assert_called_once_with('DELETE', '/v1/instances/test/') + connection_mock.request.assert_called_once_with('DELETE', '/v1.1/instances/test/') model = Instance() with self.assertRaises(SyncanoValidationError): @@ -127,7 +127,7 @@ def test_delete(self, connection_mock): @mock.patch('syncano.models.Instance._get_connection') def test_reload(self, connection_mock): - model = Instance(name='test', links={'self': '/v1/instances/test/'}) + model = Instance(name='test', links={'self': '/v1.1/instances/test/'}) connection_mock.return_value = connection_mock connection_mock.request.return_value = { 'name': 'new_one', @@ -144,7 +144,7 @@ def test_reload(self, connection_mock): self.assertEqual(model.description, 'dummy desc') connection_mock.assert_called_once_with() - connection_mock.request.assert_called_once_with('GET', '/v1/instances/test/') + connection_mock.request.assert_called_once_with('GET', '/v1.1/instances/test/') model = Instance() with self.assertRaises(SyncanoValidationError): @@ -194,6 +194,6 @@ def test_save_with_revision(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.request.assert_called_with( 'POST', - '/v1/instances/', + '/v1.1/instances/', data={'name': 'test', 'expected_revision': 12} ) diff --git a/tests/test_options.py b/tests/test_options.py index 39514b3..3a7831d 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -15,15 +15,15 @@ class Meta: endpoints = { 'detail': { 'methods': ['delete', 'post', 'patch', 'get'], - 'path': '/v1/dummy/{name}/', + 'path': '/v1.1/dummy/{name}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/v1/dummy/', + 'path': '/v1.1/dummy/', }, 'dummy': { 'methods': ['post', 'get'], - 'path': '/v1/dummy/{a}/{b}/', + 'path': '/v1.1/dummy/{a}/{b}/', 'properties': ['a', 'b'] } } @@ -138,7 +138,7 @@ def test_resolve_endpoint(self): properties = {'instance_name': 'test', 'a': 'a', 'b': 'b'} path = self.options.resolve_endpoint('dummy', properties) - self.assertEqual(path, '/v1/instances/test/v1/dummy/a/b/') + self.assertEqual(path, '/v1.1/instances/test/v1.1/dummy/a/b/') def test_get_endpoint_query_params(self): properties = {'instance_name': 'test', 'x': 'y'} diff --git a/tests/test_push.py b/tests/test_push.py index a17316f..8ad587a 100644 --- a/tests/test_push.py +++ b/tests/test_push.py @@ -7,7 +7,7 @@ from syncano.models import APNSDevice, APNSMessage, GCMDevice, GCMMessage -class CodeBoxTestCase(unittest.TestCase): +class ScriptTestCase(unittest.TestCase): @mock.patch('syncano.models.GCMDevice._get_connection') def test_gcm_device(self, connection_mock): @@ -31,14 +31,14 @@ def test_gcm_device(self, connection_mock): connection_mock.assert_called_once_with() connection_mock.request.assert_called_once_with( - u'POST', u'/v1/instances/test/push_notifications/gcm/devices/', + u'POST', u'/v1.1/instances/test/push_notifications/gcm/devices/', data={"registration_id": u'86152312314401555', "device_id": "10000000001", "is_active": True, "label": "example label"} ) model.created_at = datetime.now() # to Falsify is_new() model.delete() connection_mock.request.assert_called_with( - u'DELETE', u'/v1/instances/test/push_notifications/gcm/devices/86152312314401555/' + u'DELETE', u'/v1.1/instances/test/push_notifications/gcm/devices/86152312314401555/' ) @mock.patch('syncano.models.APNSDevice._get_connection') @@ -64,7 +64,7 @@ def test_apns_device(self, connection_mock): connection_mock.assert_called_once_with() connection_mock.request.assert_called_once_with( - u'POST', u'/v1/instances/test/push_notifications/apns/devices/', + u'POST', u'/v1.1/instances/test/push_notifications/apns/devices/', data={"registration_id": u'86152312314401555', "device_id": "10000000001", "is_active": True, "label": "example label"} ) @@ -72,7 +72,7 @@ def test_apns_device(self, connection_mock): model.created_at = datetime.now() # to Falsify is_new() model.delete() connection_mock.request.assert_called_with( - u'DELETE', u'/v1/instances/test/push_notifications/apns/devices/86152312314401555/' + u'DELETE', u'/v1.1/instances/test/push_notifications/apns/devices/86152312314401555/' ) @mock.patch('syncano.models.GCMMessage._get_connection') @@ -93,7 +93,7 @@ def test_gcm_message(self, connection_mock): connection_mock.assert_called_once_with() connection_mock.request.assert_called_once_with( - u'POST', u'/v1/instances/test/push_notifications/gcm/messages/', + u'POST', u'/v1.1/instances/test/push_notifications/gcm/messages/', data={'content': '{"environment": "production", "data": "some data"}'} ) @@ -115,6 +115,6 @@ def test_apns_message(self, connection_mock): connection_mock.assert_called_once_with() connection_mock.request.assert_called_once_with( - u'POST', u'/v1/instances/test/push_notifications/apns/messages/', + u'POST', u'/v1.1/instances/test/push_notifications/apns/messages/', data={'content': '{"environment": "production", "data": "some data"}'} ) From dfc2461547ecef9fd502aa25e7e4f8dea9bc4fd4 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 4 Mar 2016 11:54:41 +0100 Subject: [PATCH 278/558] [LIB-516] correct flake8 and isort issues --- syncano/models/incentives.py | 5 +++-- syncano/models/traces.py | 2 +- tests/integration_test.py | 2 +- tests/test_incentives.py | 2 +- tests/test_manager.py | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 200629c..b79f43b 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -7,7 +7,7 @@ from . import fields from .base import Model from .instances import Instance -from .manager import ScriptManager, ScriptEndpointManager +from .manager import ScriptEndpointManager, ScriptManager class Script(Model): @@ -208,7 +208,8 @@ class ScriptEndpoint(Model): **ScriptEndpoint** has special method called ``run`` which will execute related script:: >>> ScriptEndpoint.please.run('instance-name', 'script-name') - >>> ScriptEndpoint.please.run('instance-name', 'script-name', payload={'variable_one': 1, 'variable_two': 2}) + >>> ScriptEndpoint.please.run('instance-name', 'script-name', payload={'variable_one': 1, + 'variable_two': 2}) >>> ScriptEndpoint.please.run('instance-name', 'script-name', payload="{\"variable_one\": 1, \"variable_two\": 2}") diff --git a/syncano/models/traces.py b/syncano/models/traces.py index 933e686..881b049 100644 --- a/syncano/models/traces.py +++ b/syncano/models/traces.py @@ -3,7 +3,7 @@ from . import fields from .base import Model from .custom_response import CustomResponseMixin -from .incentives import Script, Schedule, Trigger, ScriptEndpoint +from .incentives import Schedule, Script, ScriptEndpoint, Trigger class ScriptTrace(CustomResponseMixin, Model): diff --git a/tests/integration_test.py b/tests/integration_test.py index d6a7e5c..78505c1 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -7,7 +7,7 @@ import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError -from syncano.models import ApiKey, Class, Script, Instance, Object, ScriptEndpoint, registry +from syncano.models import ApiKey, Class, Instance, Object, Script, ScriptEndpoint, registry class IntegrationTest(unittest.TestCase): diff --git a/tests/test_incentives.py b/tests/test_incentives.py index 69e3119..0b3e157 100644 --- a/tests/test_incentives.py +++ b/tests/test_incentives.py @@ -4,7 +4,7 @@ from datetime import datetime from syncano.exceptions import SyncanoValidationError -from syncano.models import Script, ScriptTrace, ResponseTemplate, ScriptEndpoint, ScriptEndpointTrace +from syncano.models import ResponseTemplate, Script, ScriptEndpoint, ScriptEndpointTrace, ScriptTrace try: from unittest import mock diff --git a/tests/test_manager.py b/tests/test_manager.py index dde9965..4f38691 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -2,7 +2,7 @@ from datetime import datetime from syncano.exceptions import SyncanoDoesNotExist, SyncanoRequestError, SyncanoValueError -from syncano.models import Script, ScriptTrace, Instance, Object, User, ScriptEndpoint, ScriptEndpointTrace +from syncano.models import Instance, Object, Script, ScriptEndpoint, ScriptEndpointTrace, ScriptTrace, User try: from unittest import mock From f51423a4f71c1a52cd72ba8c549ae8d43f5d228b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 4 Mar 2016 12:04:08 +0100 Subject: [PATCH 279/558] [LIB-516] correct instance links --- syncano/models/instances.py | 8 ++++---- tests/integration_test.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 7d1ec09..9b7fde7 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -22,15 +22,15 @@ class Instance(Model): {'type': 'detail', 'name': 'self'}, {'type': 'list', 'name': 'admins'}, {'type': 'list', 'name': 'classes'}, - {'type': 'list', 'name': 'scripts'}, {'type': 'list', 'name': 'invitations'}, - {'type': 'list', 'name': 'runtimes'}, {'type': 'list', 'name': 'api_keys'}, {'type': 'list', 'name': 'triggers'}, {'type': 'list', 'name': 'users'}, - {'type': 'list', 'name': 'script_endpoints'}, + {'type': 'list', 'name': 'groups'}, {'type': 'list', 'name': 'schedules'}, - {'type': 'list', 'name': 'templates'} + {'type': 'list', 'name': 'endpoints'}, + {'type': 'list', 'name': 'snippets'}, + {'type': 'list', 'name': 'push_notifications'}, ) name = fields.StringField(max_length=64, primary_key=True) diff --git a/tests/integration_test.py b/tests/integration_test.py index 78505c1..04b207f 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -312,14 +312,14 @@ def test_count_and_with_count(self): author_two.delete() -class CodeboxIntegrationTest(InstanceMixin, IntegrationTest): +class ScriptIntegrationTest(InstanceMixin, IntegrationTest): model = Script @classmethod def tearDownClass(cls): for cb in cls.instance.scripts.all(): cb.delete() - super(CodeboxIntegrationTest, cls).tearDownClass() + super(ScriptIntegrationTest, cls).tearDownClass() def test_required_fields(self): with self.assertRaises(SyncanoValueError): From eff1d6024aa01f7658bce9703171e8e6826c7bc5 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 4 Mar 2016 14:14:45 +0100 Subject: [PATCH 280/558] [LIB-516] ability to make links by model name --- syncano/models/fields.py | 4 +++- syncano/models/instances.py | 6 ++++++ syncano/models/manager.py | 16 +++++++++++----- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 258cd21..189fe34 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -403,7 +403,9 @@ def contribute_to_class(self, cls, name): if name in self.IGNORED_LINKS: continue - setattr(cls, name, RelatedManagerDescriptor(self, name, endpoint)) + setattr(cls, name, RelatedManagerDescriptor(self, name, endpoint, + search_by_path=not link.get('child', False), + model_name=link.get('model_name'))) class ModelField(Field): diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 9b7fde7..85f7b35 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -31,6 +31,12 @@ class Instance(Model): {'type': 'list', 'name': 'endpoints'}, {'type': 'list', 'name': 'snippets'}, {'type': 'list', 'name': 'push_notifications'}, + {'type': 'list', 'name': 'scripts', 'child': True, 'model_name': 'Script'}, + {'type': 'list', 'name': 'script_endpoints', 'child': True, 'model_name': 'ScriptEndpoint'}, + {'type': 'list', 'name': 'gcm_devices', 'child': True, 'model_name': 'GCMDevice'}, + {'type': 'list', 'name': 'gcm_messages', 'child': True, 'model_name': 'GCMMessage'}, + {'type': 'list', 'name': 'apns_devices', 'child': True, 'model_name': 'APNSDevice'}, + {'type': 'list', 'name': 'apns_messages', 'child': True, 'model_name': 'APNSMessage'}, ) name = fields.StringField(max_length=64, primary_key=True) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index d745f06..7de2f3d 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -36,10 +36,12 @@ def __get__(self, instance, owner=None): class RelatedManagerDescriptor(object): - def __init__(self, field, name, endpoint): + def __init__(self, field, name, endpoint, search_by_path=True, model_name=None): + self.search_by_path = search_by_path self.field = field self.name = name self.endpoint = endpoint + self.model_name = model_name def __get__(self, instance, owner=None): if instance is None: @@ -50,12 +52,16 @@ def __get__(self, instance, owner=None): if not links: return None - path = links[self.name] + if self.search_by_path: + path = links[self.name] - if not path: - return None + if not path: + return None + + Model = registry.get_model_by_path(path) + else: + Model = registry.get_model_by_name(self.model_name) - Model = registry.get_model_by_path(path) method = getattr(Model.please, self.endpoint, Model.please.all) properties = instance._meta.get_endpoint_properties('detail') From d5b8402d19a593151d048813b56a0ebe542d83d4 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 4 Mar 2016 14:22:58 +0100 Subject: [PATCH 281/558] [LIB-516] correct instance integration test; --- tests/integration_test.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 04b207f..7fdd9e0 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -69,16 +69,11 @@ def test_create(self): name = 'i%s' % self.generate_hash()[:10] description = 'IntegrationTest' - self.assertFalse(bool(self.model.please.list())) - instance = self.model.please.create(name=name, description=description) self.assertIsNotNone(instance.pk) self.assertEqual(instance.name, name) self.assertEqual(instance.description, description) - self.assertTrue(bool(self.model.please.list())) - instance.delete() - instance = self.model(name=name, description=description) instance.save() self.assertIsNotNone(instance.pk) From 064f24a9eb7001128e568eac61a5ed3a1f26d431 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 4 Mar 2016 14:27:58 +0100 Subject: [PATCH 282/558] [LIB-516] add templates on instance --- syncano/models/instances.py | 1 + tests/integration_test.py | 1 + 2 files changed, 2 insertions(+) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 85f7b35..e88f1e7 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -33,6 +33,7 @@ class Instance(Model): {'type': 'list', 'name': 'push_notifications'}, {'type': 'list', 'name': 'scripts', 'child': True, 'model_name': 'Script'}, {'type': 'list', 'name': 'script_endpoints', 'child': True, 'model_name': 'ScriptEndpoint'}, + {'type': 'list', 'name': 'templates', 'child': True, 'model_name': 'ResponseTemplate'}, {'type': 'list', 'name': 'gcm_devices', 'child': True, 'model_name': 'GCMDevice'}, {'type': 'list', 'name': 'gcm_messages', 'child': True, 'model_name': 'GCMMessage'}, {'type': 'list', 'name': 'apns_devices', 'child': True, 'model_name': 'APNSDevice'}, diff --git a/tests/integration_test.py b/tests/integration_test.py index 7fdd9e0..e0900e8 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -73,6 +73,7 @@ def test_create(self): self.assertIsNotNone(instance.pk) self.assertEqual(instance.name, name) self.assertEqual(instance.description, description) + instance.delete() instance = self.model(name=name, description=description) instance.save() From c219e30c229c7368015b25a9f54fae09746ad8c1 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 12:15:47 +0100 Subject: [PATCH 283/558] [LIB-516] add RelatedManagerField; cleanup links; --- syncano/models/accounts.py | 21 +++--------- syncano/models/billing.py | 12 ++----- syncano/models/channels.py | 6 +--- syncano/models/classes.py | 9 ++--- syncano/models/data_views.py | 7 +--- syncano/models/fields.py | 65 ++++++++++++++++++++++++++---------- syncano/models/incentives.py | 54 ++++++++++++------------------ syncano/models/instances.py | 56 ++++++++++++++----------------- syncano/models/manager.py | 39 ---------------------- syncano/models/traces.py | 17 +++------- tests/test_fields.py | 2 +- 11 files changed, 110 insertions(+), 178 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index f024f90..6da8191 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -17,9 +17,6 @@ class Admin(Model): :ivar role: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) ROLE_CHOICES = ( {'display_name': 'full', 'value': 'full'}, {'display_name': 'write', 'value': 'write'}, @@ -30,7 +27,7 @@ class Admin(Model): last_name = fields.StringField(read_only=True, required=False) email = fields.EmailField(read_only=True, required=False) role = fields.ChoiceField(choices=ROLE_CHOICES) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() class Meta: parent = Instance @@ -52,10 +49,6 @@ class Profile(DataObjectMixin, Object): PREDEFINED_CLASS_NAME = 'user_profile' - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - PERMISSIONS_CHOICES = ( {'display_name': 'None', 'value': 'none'}, {'display_name': 'Read', 'value': 'read'}, @@ -71,7 +64,7 @@ class Profile(DataObjectMixin, Object): channel = fields.StringField(required=False) channel_room = fields.StringField(required=False, max_length=64) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) @@ -102,9 +95,6 @@ class User(Model): :ivar created_at: :class:`~syncano.models.fields.DateTimeField` :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) username = fields.StringField(max_length=64, required=True) password = fields.StringField(read_only=False, required=True) @@ -112,7 +102,7 @@ class User(Model): profile = fields.ModelField('Profile') - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) @@ -174,14 +164,11 @@ class Group(Model): :ivar created_at: :class:`~syncano.models.fields.DateTimeField` :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) label = fields.StringField(max_length=64, required=True) description = fields.StringField(read_only=False, required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/billing.py b/syncano/models/billing.py index 91ba221..343a993 100644 --- a/syncano/models/billing.py +++ b/syncano/models/billing.py @@ -17,17 +17,13 @@ class Coupon(Model): :ivar duration: :class:`~syncano.models.fields.IntegerField` """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'redeem'}, - ) CURRENCY_CHOICES = ( {'display_name': 'USD', 'value': 'usd'}, ) name = fields.StringField(max_length=32, primary_key=True) redeem_by = fields.DateField() - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() percent_off = fields.IntegerField(required=False) amount_off = fields.FloatField(required=False) currency = fields.ChoiceField(choices=CURRENCY_CHOICES) @@ -57,15 +53,11 @@ class Discount(Model): :ivar links: :class:`~syncano.models.fields.HyperlinkedField` """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - instance = fields.ModelField('Instance') coupon = fields.ModelField('Coupon') start = fields.DateField(read_only=True, required=False) end = fields.DateField(read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() class Meta: endpoints = { diff --git a/syncano/models/channels.py b/syncano/models/channels.py index 1e2d0a4..a28a701 100644 --- a/syncano/models/channels.py +++ b/syncano/models/channels.py @@ -92,10 +92,6 @@ class Channel(Model): >>> channel.poll(callback=callback) """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - TYPE_CHOICES = ( {'display_name': 'Default', 'value': 'default'}, {'display_name': 'Separate rooms', 'value': 'separate_rooms'}, @@ -113,7 +109,7 @@ class Channel(Model): group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') custom_publish = fields.BooleanField(default=False, required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() class Meta: parent = Instance diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 3962fd7..14ffbde 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -36,11 +36,6 @@ class Class(Model): **dynamically populated** with fields defined in schema attribute. """ - LINKS = [ - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'objects'}, - ] - PERMISSIONS_CHOICES = ( {'display_name': 'None', 'value': 'none'}, {'display_name': 'Read', 'value': 'read'}, @@ -52,7 +47,7 @@ class Class(Model): objects_count = fields.Field(read_only=True, required=False) schema = fields.SchemaField(read_only=False, required=True) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() status = fields.Field() metadata = fields.JSONField(read_only=False, required=False) @@ -65,6 +60,8 @@ class Class(Model): group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default='none') + objects = fields.RelatedManagerField('Object') + class Meta: parent = Instance plural_name = 'Classes' diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 2bd0d5c..2fd8716 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -18,11 +18,6 @@ class EndpointData(Model): :ivar links: :class:`~syncano.models.fields.HyperlinkedField` """ - LINKS = [ - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'data_views'}, - ] - PERMISSIONS_CHOICES = ( {'display_name': 'None', 'value': 'none'}, {'display_name': 'Read', 'value': 'read'}, @@ -42,7 +37,7 @@ class EndpointData(Model): order_by = fields.StringField(required=False) page_size = fields.IntegerField(required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() class Meta: parent = Instance diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 189fe34..27d5f88 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -8,7 +8,7 @@ from syncano.exceptions import SyncanoFieldError, SyncanoValueError from syncano.utils import force_text -from .manager import RelatedManagerDescriptor, SchemaManager +from .manager import SchemaManager from .registry import registry @@ -158,6 +158,28 @@ def contribute_to_class(self, cls, name): setattr(self, 'ValidationError', error_class) +class RelatedManagerField(Field): + + def __init__(self, model_name, endpoint='list'): + self.model_name = model_name + self.endpoint = endpoint + self.model_name = model_name + + def __get__(self, instance, owner=None): + if instance is None: + raise AttributeError("RelatedManager is accessible only via {0} instances.".format(owner.__name__)) + + Model = registry.get_model_by_name(self.model_name) + method = getattr(Model.please, self.endpoint, Model.please.all) + properties = instance._meta.get_endpoint_properties('detail') + properties = [getattr(instance, prop) for prop in properties] + + return method(*properties) + + def contribute_to_class(self, cls, name): + setattr(cls, name, self) + + class PrimaryKeyField(Field): primary_key = True @@ -385,27 +407,36 @@ def to_native(self, value): return ret -class HyperlinkedField(Field): +class LinksWrapper(object): + + def __init__(self, links_dict, ignored_links): + self.links_dict = links_dict + self.ignored_links = ignored_links + + def __getattribute__(self, item): + try: + return super(LinksWrapper, self).__getattribute__(item) + except AttributeError: + if item not in self.links_dict or item in self.ignored_links: + raise + return self.links_dict[item] + + def to_native(self): + return self.links_dict + + +class LinksField(Field): query_allowed = False IGNORED_LINKS = ('self', ) def __init__(self, *args, **kwargs): - self.links = kwargs.pop('links', []) - super(HyperlinkedField, self).__init__(*args, **kwargs) - - def contribute_to_class(self, cls, name): - super(HyperlinkedField, self).contribute_to_class(cls, name) - - for link in self.links: - name = link['name'] - endpoint = link['type'] + super(LinksField, self).__init__(*args, **kwargs) - if name in self.IGNORED_LINKS: - continue + def to_python(self, value): + return LinksWrapper(value, self.IGNORED_LINKS) - setattr(cls, name, RelatedManagerDescriptor(self, name, endpoint, - search_by_path=not link.get('child', False), - model_name=link.get('model_name'))) + def to_native(self, value): + return value class ModelField(Field): @@ -611,7 +642,7 @@ def to_native(self, value): 'field': Field, 'writable': WritableField, 'endpoint': EndpointField, - 'links': HyperlinkedField, + 'links': LinksField, 'model': ModelField, 'json': JSONField, 'schema': SchemaField, diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index b79f43b..bf6762a 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -36,14 +36,7 @@ class Script(Model): >>> s.run() >>> s.run(variable_one=1, variable_two=2) """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'runtimes'}, - # This will cause name collision between model run method - # and HyperlinkedField dynamic methods. - # {'type': 'detail', 'name': 'run'}, - {'type': 'list', 'name': 'traces'}, - ) + RUNTIME_CHOICES = ( {'display_name': 'nodejs', 'value': 'nodejs'}, {'display_name': 'python', 'value': 'python'}, @@ -56,10 +49,12 @@ class Script(Model): source = fields.StringField() runtime_name = fields.ChoiceField(choices=RUNTIME_CHOICES) config = fields.Field(required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) + traces = fields.RelatedManagerField('ScriptTrace') + please = ScriptManager() class Meta: @@ -120,11 +115,6 @@ class Schedule(Model): :ivar scheduled_next: :class:`~syncano.models.fields.DateTimeField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` """ - LINKS = [ - {'type': 'detail', 'name': 'self'}, - {'type': 'detail', 'name': 'script'}, - {'type': 'list', 'name': 'traces'}, - ] label = fields.StringField(max_length=80) script = fields.IntegerField(label='script id') @@ -133,7 +123,11 @@ class Schedule(Model): payload = fields.StringField(required=False) created_at = fields.DateTimeField(read_only=True, required=False) scheduled_next = fields.DateTimeField(read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() + + traces = fields.RelatedManagerField('ScheduleTraces') + # TODO: think of such case + # script = fields.RelatedManagerField('Script', endpoint='detail') class Meta: parent = Instance @@ -161,12 +155,7 @@ class Trigger(Model): :ivar created_at: :class:`~syncano.models.fields.DateTimeField` :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'detail', 'name': 'script'}, - {'type': 'detail', 'name': 'class_name'}, - {'type': 'list', 'name': 'traces'}, - ) + SIGNAL_CHOICES = ( {'display_name': 'post_update', 'value': 'post_update'}, {'display_name': 'post_create', 'value': 'post_create'}, @@ -177,10 +166,15 @@ class Trigger(Model): script = fields.IntegerField(label='script id') class_name = fields.StringField(label='class name', mapping='class') signal = fields.ChoiceField(choices=SIGNAL_CHOICES) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) + traces = fields.RelatedManagerField('TriggerTrace') + # TODO: handle this + # class_name = fields.RelatedManagerField + # script = fields.RelatedManagerField + class Meta: parent = Instance endpoints = { @@ -220,18 +214,16 @@ class ScriptEndpoint(Model): >>> se.run(variable_one=1, variable_two=2) """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'detail', 'name': 'script'}, - {'type': 'list', 'name': 'traces'}, - ) name = fields.SlugField(max_length=50, primary_key=True) script = fields.IntegerField(label='script id') public = fields.BooleanField(required=False, default=False) public_link = fields.ChoiceField(required=False, read_only=True) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() + traces = fields.RelatedManagerField('ScriptEndpointTrace') + # TODO: think of such case + # script = fields.RelatedManagerField('Script', endpoint='detail') please = ScriptEndpointManager() class Meta: @@ -312,15 +304,11 @@ class ResponseTemplate(Model): :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) - name = fields.StringField(max_length=64) content = fields.StringField(label='content') content_type = fields.StringField(label='content type') context = fields.JSONField(label='context') - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index e88f1e7..094c739 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -18,37 +18,37 @@ class Instance(Model): :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - {'type': 'list', 'name': 'admins'}, - {'type': 'list', 'name': 'classes'}, - {'type': 'list', 'name': 'invitations'}, - {'type': 'list', 'name': 'api_keys'}, - {'type': 'list', 'name': 'triggers'}, - {'type': 'list', 'name': 'users'}, - {'type': 'list', 'name': 'groups'}, - {'type': 'list', 'name': 'schedules'}, - {'type': 'list', 'name': 'endpoints'}, - {'type': 'list', 'name': 'snippets'}, - {'type': 'list', 'name': 'push_notifications'}, - {'type': 'list', 'name': 'scripts', 'child': True, 'model_name': 'Script'}, - {'type': 'list', 'name': 'script_endpoints', 'child': True, 'model_name': 'ScriptEndpoint'}, - {'type': 'list', 'name': 'templates', 'child': True, 'model_name': 'ResponseTemplate'}, - {'type': 'list', 'name': 'gcm_devices', 'child': True, 'model_name': 'GCMDevice'}, - {'type': 'list', 'name': 'gcm_messages', 'child': True, 'model_name': 'GCMMessage'}, - {'type': 'list', 'name': 'apns_devices', 'child': True, 'model_name': 'APNSDevice'}, - {'type': 'list', 'name': 'apns_messages', 'child': True, 'model_name': 'APNSMessage'}, - ) - name = fields.StringField(max_length=64, primary_key=True) description = fields.StringField(read_only=False, required=False) role = fields.Field(read_only=True, required=False) owner = fields.ModelField('Admin', read_only=True) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() metadata = fields.JSONField(read_only=False, required=False) created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) + # user related fields; + api_keys = fields.RelatedManagerField('ApiKey') + users = fields.RelatedManagerField('User') + admins = fields.RelatedManagerField('Admin') + groups = fields.RelatedManagerField('Group') + + # snippets and data fields; + scripts = fields.RelatedManagerField('Script') + script_endpoints = fields.RelatedManagerField('ScriptEndpoint') + templates = fields.RelatedManagerField('ResponseTemplate') + + triggers = fields.RelatedManagerField('Trigger') + schedules = fields.RelatedManagerField('Schedule') + classes = fields.RelatedManagerField('Class') + invitations = fields.RelatedManagerField('InstanceInvitation') + + # push notifications fields; + gcm_devices = fields.RelatedManagerField('GCMDevice') + gcm_messages = fields.RelatedManagerField('GCMMessage') + apns_devices = fields.RelatedManagerField('APNSDevice') + apns_messages = fields.RelatedManagerField('APNSMessage') + class Meta: endpoints = { 'detail': { @@ -84,16 +84,13 @@ class ApiKey(Model): :ivar ignore_acl: :class:`~syncano.models.fields.BooleanField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` """ - LINKS = [ - {'type': 'detail', 'name': 'self'}, - ] api_key = fields.StringField(read_only=True, required=False) 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) + links = fields.LinksField() class Meta: parent = Instance @@ -122,16 +119,13 @@ class InstanceInvitation(Model): :ivar created_at: :class:`~syncano.models.fields.DateTimeField` :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` """ - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) from .accounts import Admin email = fields.EmailField(max_length=254) role = fields.ChoiceField(choices=Admin.ROLE_CHOICES) key = fields.StringField(read_only=True, required=False) state = fields.StringField(read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 7de2f3d..652b1c0 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -34,45 +34,6 @@ def __get__(self, instance, owner=None): return self.manager.all() -class RelatedManagerDescriptor(object): - - def __init__(self, field, name, endpoint, search_by_path=True, model_name=None): - self.search_by_path = search_by_path - self.field = field - self.name = name - self.endpoint = endpoint - self.model_name = model_name - - def __get__(self, instance, owner=None): - if instance is None: - raise AttributeError("RelatedManager is accessible only via {0} instances.".format(owner.__name__)) - - links = getattr(instance, self.field.name) - - if not links: - return None - - if self.search_by_path: - path = links[self.name] - - if not path: - return None - - Model = registry.get_model_by_path(path) - else: - Model = registry.get_model_by_name(self.model_name) - - method = getattr(Model.please, self.endpoint, Model.please.all) - - properties = instance._meta.get_endpoint_properties('detail') - - if instance.__class__.__name__ == 'Instance': - registry.set_last_used_instance(getattr(instance, 'name', None)) - properties = [getattr(instance, prop) for prop in properties] - - return method(*properties) - - class Manager(ConnectionMixin): """Base class responsible for all ORM (``please``) actions.""" diff --git a/syncano/models/traces.py b/syncano/models/traces.py index 881b049..b2b609b 100644 --- a/syncano/models/traces.py +++ b/syncano/models/traces.py @@ -21,12 +21,9 @@ class ScriptTrace(CustomResponseMixin, Model): {'display_name': 'Processing', 'value': 'processing'}, {'display_name': 'Pending', 'value': 'pending'}, ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() executed_at = fields.DateTimeField(read_only=True, required=False) result = fields.JSONField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) @@ -59,12 +56,9 @@ class ScheduleTrace(Model): {'display_name': 'Timeout', 'value': 'timeout'}, {'display_name': 'Pending', 'value': 'pending'}, ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() executed_at = fields.DateTimeField(read_only=True, required=False) result = fields.StringField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) @@ -102,7 +96,7 @@ class TriggerTrace(Model): ) status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() executed_at = fields.DateTimeField(read_only=True, required=False) result = fields.StringField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) @@ -135,12 +129,9 @@ class ScriptEndpointTrace(CustomResponseMixin, Model): {'display_name': 'Timeout', 'value': 'timeout'}, {'display_name': 'Pending', 'value': 'pending'}, ) - LINKS = ( - {'type': 'detail', 'name': 'self'}, - ) status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.HyperlinkedField(links=LINKS) + links = fields.LinksField() executed_at = fields.DateTimeField(read_only=True, required=False) result = fields.JSONField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) diff --git a/tests/test_fields.py b/tests/test_fields.py index 6e9cf8f..1de4319 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -55,7 +55,7 @@ class AllFieldsModel(models.Model): choice_field = models.ChoiceField(choices=CHOICES) date_field = models.DateField() datetime_field = models.DateTimeField() - hyperlinked_field = models.HyperlinkedField() + hyperlinked_field = models.LinksField() model_field = models.ModelField('Instance') json_field = models.JSONField(schema=SCHEMA) schema_field = models.SchemaField() From 554a3a87e88c39c6d9e34a3e5173dbfe49e42ba5 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 12:24:54 +0100 Subject: [PATCH 284/558] [LIB-516] remove details links - look like do not work; need to rework; --- syncano/models/incentives.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index bf6762a..c62a326 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -126,8 +126,6 @@ class Schedule(Model): links = fields.LinksField() traces = fields.RelatedManagerField('ScheduleTraces') - # TODO: think of such case - # script = fields.RelatedManagerField('Script', endpoint='detail') class Meta: parent = Instance @@ -171,9 +169,6 @@ class Trigger(Model): updated_at = fields.DateTimeField(read_only=True, required=False) traces = fields.RelatedManagerField('TriggerTrace') - # TODO: handle this - # class_name = fields.RelatedManagerField - # script = fields.RelatedManagerField class Meta: parent = Instance @@ -222,8 +217,6 @@ class ScriptEndpoint(Model): links = fields.LinksField() traces = fields.RelatedManagerField('ScriptEndpointTrace') - # TODO: think of such case - # script = fields.RelatedManagerField('Script', endpoint='detail') please = ScriptEndpointManager() class Meta: From 1d133c8d80d59faae8fec65d5f89739562fab81a Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 12:37:37 +0100 Subject: [PATCH 285/558] [LIB-516] fixes after ci tets; --- syncano/models/fields.py | 2 ++ syncano/models/instances.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 27d5f88..7bcc67e 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -174,6 +174,8 @@ def __get__(self, instance, owner=None): properties = instance._meta.get_endpoint_properties('detail') properties = [getattr(instance, prop) for prop in properties] + if properties.get('instance_name'): + registry.set_last_used_instance(properties['instance_name']) return method(*properties) def contribute_to_class(self, cls, name): diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 094c739..adec6bd 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -67,7 +67,7 @@ def rename(self, new_name): :param new_name: the new name for the instance; :return: a populated Instance object; """ - rename_path = self.links.get('rename') + rename_path = self.links.rename data = {'new_name': new_name} connection = self._get_connection() response = connection.request('POST', rename_path, data=data) From 9d08e31674111e2f01cc414fe824b3ba0c6ec73c Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 12:51:02 +0100 Subject: [PATCH 286/558] [LIB-516] correct after test on circle ci --- syncano/models/fields.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 7bcc67e..8080364 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -173,9 +173,8 @@ def __get__(self, instance, owner=None): method = getattr(Model.please, self.endpoint, Model.please.all) properties = instance._meta.get_endpoint_properties('detail') properties = [getattr(instance, prop) for prop in properties] - - if properties.get('instance_name'): - registry.set_last_used_instance(properties['instance_name']) + if instance.__class__.name == 'Instance': + registry.set_last_used_instance(instance.name) return method(*properties) def contribute_to_class(self, cls, name): From be4eab89fd64964b6f0cba486bb240ca4dd3234f Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 13:24:32 +0100 Subject: [PATCH 287/558] [LIB-516] correct registy last used instance func --- syncano/models/archetypes.py | 2 +- syncano/models/fields.py | 2 -- syncano/models/manager.py | 2 -- syncano/models/registry.py | 12 ++++++------ tests/integration_test.py | 4 ++-- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 080bd2a..5e62571 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -174,7 +174,7 @@ def delete(self, **kwargs): connection = self._get_connection(**kwargs) connection.request('DELETE', endpoint) if self.__class__.__name__ == 'Instance': # avoid circular import; - registry.clear_instance_name() + registry.clear_used_instance() self._raw_data = {} def reload(self, **kwargs): diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 8080364..1bd2e30 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -173,8 +173,6 @@ def __get__(self, instance, owner=None): method = getattr(Model.please, self.endpoint, Model.please.all) properties = instance._meta.get_endpoint_properties('detail') properties = [getattr(instance, prop) for prop in properties] - if instance.__class__.name == 'Instance': - registry.set_last_used_instance(instance.name) return method(*properties) def contribute_to_class(self, cls, name): diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 652b1c0..bd7a83f 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -795,8 +795,6 @@ def _get_instance(self, attrs): def _get_endpoint_properties(self): defaults = {f.name: f.default for f in self.model._meta.fields if f.default is not None} defaults.update(self.properties) - if defaults.get('instance_name'): - registry.set_last_used_instance(defaults['instance_name']) return self.model._meta.resolve_endpoint(self.endpoint, defaults), defaults diff --git a/syncano/models/registry.py b/syncano/models/registry.py index e34571f..7168f75 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -14,7 +14,7 @@ def __init__(self, models=None): self.schemas = {} self.patterns = [] self._pending_lookups = {} - self.last_used_instance = None + self.instance_name = None self._default_connection = None def __str__(self): @@ -80,13 +80,13 @@ def set_default_property(self, name, value): def set_default_instance(self, value): self.set_default_property('instance_name', value) - def set_last_used_instance(self, instance): - if instance and self.last_used_instance != instance or registry.last_used_instance is None: + def set_used_instance(self, instance): + if instance and self.instance_name != instance or registry.instance_name is None: self.set_default_instance(instance) # update the registry with last used instance; - self.last_used_instance = instance + self.instance_name = instance - def clear_instance_name(self): - self.last_used_instance = None + def clear_used_instance(self): + self.instance_name = None self.set_default_instance(None) def get_schema(self, class_name): diff --git a/tests/integration_test.py b/tests/integration_test.py index e0900e8..7cfab96 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -319,7 +319,7 @@ def tearDownClass(cls): def test_required_fields(self): with self.assertRaises(SyncanoValueError): - registry.clear_instance_name() + registry.clear_used_instance() list(self.model.please.all()) def test_list(self): @@ -421,7 +421,7 @@ def tearDownClass(cls): def test_required_fields(self): with self.assertRaises(SyncanoValueError): - registry.clear_instance_name() + registry.clear_used_instance() list(self.model.please.all()) def test_list(self): From 4bd94aa7ad6f13885151c4caec7166e19a5918ab Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 13:46:44 +0100 Subject: [PATCH 288/558] [LIB-516] add register update on instance cration - when new instance is created the used_instance becomes a new created one; --- syncano/models/manager.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index bd7a83f..74052cb 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -237,10 +237,14 @@ def create(self, **kwargs): attrs.update(self.properties) attrs.update({'is_lazy': self.is_lazy}) instance = self._get_instance(attrs) - saved_instance = instance.save() + + if instance.__class__.__name__ == 'Instance': + registry.set_used_instance(instance.name) + if not self.is_lazy: - return instance - return saved_instance + return instance.save() + + return instance def bulk_create(self, *objects): """ From d28d45df1a8fec37451af0bbb47bea637254e8ce Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 13:50:21 +0100 Subject: [PATCH 289/558] [LIB-516] add register update on instance cration - when new instance is created the used_instance becomes a new created one; --- syncano/models/manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 74052cb..5fdf691 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -201,7 +201,7 @@ def batch(self, *args): response = self.connection.request( 'POST', - self.BATCH_URI.format(name=registry.last_used_instance), + self.BATCH_URI.format(name=registry.instance_name), **{'data': {'requests': requests}} ) @@ -241,10 +241,11 @@ def create(self, **kwargs): if instance.__class__.__name__ == 'Instance': registry.set_used_instance(instance.name) + saved_instance = instance.save() if not self.is_lazy: - return instance.save() + return instance - return instance + return saved_instance def bulk_create(self, *objects): """ @@ -315,7 +316,7 @@ def in_bulk(self, object_ids_list, **kwargs): response = self.connection.request( 'POST', - self.BATCH_URI.format(name=registry.last_used_instance), + self.BATCH_URI.format(name=registry.instance_name), **{'data': {'requests': requests}} ) From 505566306d835c8677e366bee37b0e850f7acec7 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 13:56:41 +0100 Subject: [PATCH 290/558] [LIB-516] add registry cleaning in tests; --- tests/test_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_manager.py b/tests/test_manager.py index 4f38691..220d5d0 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -3,6 +3,7 @@ from syncano.exceptions import SyncanoDoesNotExist, SyncanoRequestError, SyncanoValueError from syncano.models import Instance, Object, Script, ScriptEndpoint, ScriptEndpointTrace, ScriptTrace, User +from syncano.models import registry try: from unittest import mock @@ -35,6 +36,7 @@ def tearDown(self): self.model = None self.manager = None + registry.clear_used_instance() def get_name_from_fields(self): names = [f for f in self.model._meta.fields From 69b9724a58cf42c5c0ef70d906f265fadb41f6dc Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 13:58:49 +0100 Subject: [PATCH 291/558] [LIB-516] correct isort issues --- tests/test_manager.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index 220d5d0..7d65652 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -2,8 +2,7 @@ from datetime import datetime from syncano.exceptions import SyncanoDoesNotExist, SyncanoRequestError, SyncanoValueError -from syncano.models import Instance, Object, Script, ScriptEndpoint, ScriptEndpointTrace, ScriptTrace, User -from syncano.models import registry +from syncano.models import Instance, Object, Script, ScriptEndpoint, ScriptEndpointTrace, ScriptTrace, User, registry try: from unittest import mock From 8c57b4c401b9a986a6141a18fc1dcfbbc2b2f111 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 14:04:45 +0100 Subject: [PATCH 292/558] [LIB-516] correct isort issues --- tests/integration_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration_test.py b/tests/integration_test.py index 7cfab96..51a615a 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -119,6 +119,7 @@ class ClassIntegrationTest(InstanceMixin, IntegrationTest): model = Class def test_instance_name_is_required(self): + registry.clear_used_instance() with self.assertRaises(SyncanoValueError): list(self.model.please.all()) From 2dafa24da7316dea34ee947f2372da9cc34941f4 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 7 Mar 2016 14:30:46 +0100 Subject: [PATCH 293/558] [LIB-516] create - change attrs and properties update order; --- syncano/models/manager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 5fdf691..8c844c6 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -233,10 +233,11 @@ def create(self, **kwargs): are equivalent. """ + data = self.properties.copy() attrs = kwargs.copy() - attrs.update(self.properties) - attrs.update({'is_lazy': self.is_lazy}) - instance = self._get_instance(attrs) + data.update(attrs) + data.update({'is_lazy': self.is_lazy}) + instance = self._get_instance(data) if instance.__class__.__name__ == 'Instance': registry.set_used_instance(instance.name) From 466f59ce12606225b1fb02570cb5d7726cc1663a Mon Sep 17 00:00:00 2001 From: Daniel Kopka Date: Tue, 8 Mar 2016 09:06:42 +0100 Subject: [PATCH 294/558] [LIB-541] support for templates --- syncano/connection.py | 1 + syncano/models/manager.py | 23 +++++++++++++++++++++++ tests/test_manager.py | 10 ++++++++++ 3 files changed, 34 insertions(+) diff --git a/syncano/connection.py b/syncano/connection.py index 81c62d1..b4dcd82 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -286,6 +286,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.') diff --git a/syncano/models/manager.py b/syncano/models/manager.py index e485b62..c108e0e 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -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]) @@ -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): """ @@ -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 @@ -774,6 +791,12 @@ def request(self, method=None, path=None, **request): 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 + try: response = self.connection.request(method, path, **request) except SyncanoRequestError as e: diff --git a/tests/test_manager.py b/tests/test_manager.py index 7c234ce..a056d79 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -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.assertNone(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 From 2ac30242e09419f6871bc7b2cd6280950ed786fd Mon Sep 17 00:00:00 2001 From: Daniel Kopka Date: Tue, 8 Mar 2016 09:11:27 +0100 Subject: [PATCH 295/558] [LIB-541] less complex methid --- syncano/models/manager.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index c108e0e..dbd9e03 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -772,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 @@ -785,17 +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 - - 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 + self.build_request(request) try: response = self.connection.request(method, path, **request) From 22a0bbd5fefc4d57fb3d1c81e850f2c9dfb6a5ec Mon Sep 17 00:00:00 2001 From: Daniel Kopka Date: Tue, 8 Mar 2016 09:14:17 +0100 Subject: [PATCH 296/558] [LIB-541] tests --- tests/test_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_manager.py b/tests/test_manager.py index a056d79..9343b39 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -393,7 +393,7 @@ def test_template(self, clone_mock): clone_mock.return_value = self.manager self.assertTrue(self.manager._serialize) - self.assertNone(self.manager._template) + self.assertIsNone(self.manager._template) self.manager.template('test') self.assertFalse(self.manager._serialize) self.assertEqual(self.manager._template, 'test') @@ -431,6 +431,7 @@ def test_request(self, connection_mock): 'GET', u'/v1/instances/', data={'b': 2}, + headers={}, params={'a': 1} ) From d93621f7c09b6918b768a23f0e141d9f5be4b106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 8 Mar 2016 15:23:56 +0100 Subject: [PATCH 297/558] [LIB-581] allow to store multiple files in DataObject --- syncano/connection.py | 4 +++- syncano/models/archetypes.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 81c62d1..8b9b49e 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -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=(',', ': ') diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 080bd2a..4b6d3b5 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -248,8 +248,12 @@ def to_native(self): 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): From 27a335bb76c5fcb4bc35f3353b9e448edc0546c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 8 Mar 2016 15:37:33 +0100 Subject: [PATCH 298/558] [LIB-581] correct debug test --- tests/test_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 1117c70..c60ccf7 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -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') From 9e65e387086c34d177ac71863bfbec8d6486e401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 8 Mar 2016 15:51:01 +0100 Subject: [PATCH 299/558] [LIB-516] correct qa comments; --- syncano/models/fields.py | 1 - syncano/models/push_notification.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 1bd2e30..dd86a9d 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -163,7 +163,6 @@ class RelatedManagerField(Field): def __init__(self, model_name, endpoint='list'): self.model_name = model_name self.endpoint = endpoint - self.model_name = model_name def __get__(self, instance, owner=None): if instance is None: diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index 0693f9d..311c1b8 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -16,7 +16,7 @@ class DeviceBase(object): device_id = fields.StringField(required=False) is_active = fields.BooleanField(default=True) label = fields.StringField(max_length=80) - user_id = fields.IntegerField(required=False) + user = fields.IntegerField(required=False) created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) From da8b62ec4e0e5475c0251f7b735584981ca62864 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 9 Mar 2016 13:56:28 +0100 Subject: [PATCH 300/558] [VERSION BUMP] bump the version --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 5a490d8..ca775a2 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -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", From 0b987565e102b798ecb304e30ee80f48792f644d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 14 Mar 2016 17:16:57 +0100 Subject: [PATCH 301/558] [LIB-566] python3.4 support added; check if old circle tets will pass for python 2.7.5 --- .gitignore | 2 ++ .isort.cfg | 2 +- requirements.txt | 1 - syncano/connection.py | 4 +-- syncano/models/accounts.py | 2 +- syncano/models/archetypes.py | 4 +-- syncano/models/billing.py | 2 +- syncano/models/bulk.py | 1 - syncano/models/classes.py | 5 ++-- syncano/models/data_views.py | 2 +- syncano/models/fields.py | 2 ++ syncano/models/incentives.py | 2 +- syncano/models/instances.py | 2 +- syncano/models/manager.py | 7 ++--- syncano/models/registry.py | 4 +-- syncano/models/traces.py | 2 +- syncano/release_utils.py | 4 +-- tests/integration_test.py | 6 ++--- tests/integration_test_accounts.py | 3 ++- tests/integration_test_batch.py | 3 ++- tests/test_channels.py | 2 +- tests/test_connection.py | 42 +++++++++++++++++------------- tests/test_fields.py | 19 +++++++------- tests/test_incentives.py | 14 +++++----- tests/test_manager.py | 12 ++++++--- tests/test_models.py | 6 ++--- tests/test_push.py | 36 +++++++++++++++---------- tox.ini | 5 ++++ 28 files changed, 112 insertions(+), 84 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 5cefc19..a59911a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ junit reports run_it.py syncano.egg-info +.tox/* +test diff --git a/.isort.cfg b/.isort.cfg index 106b4bc..07d4742 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,4 +2,4 @@ line_length=120 multi_line_output=3 default_section=THIRDPARTY -skip=base.py +skip=base.py,.tox diff --git a/requirements.txt b/requirements.txt index ab01918..70cdd96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,3 @@ python-slugify==0.1.0 requests==2.7.0 six==1.9.0 validictory==1.0.0 -wsgiref==0.1.2 diff --git a/syncano/connection.py b/syncano/connection.py index 2cd1551..abef86d 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -236,9 +236,9 @@ def make_request(self, method_name, path, **kwargs): files = data.pop('files', None) if files is None: - files = {k: v for k, v in data.iteritems() if isinstance(v, file)} + files = {k: v for k, v in six.iteritems(data) if hasattr(v, 'read')} if data: - kwargs['data'] = data = {k: v for k, v in data.iteritems() if k not in files} + kwargs['data'] = {k: v for k, v in six.iteritems(data) if k not in files} params = self.build_params(kwargs) method = getattr(self.session, method_name.lower(), None) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 6da8191..a09dcec 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from . import fields from .base import Model diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 92b4a36..d1c75b3 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + import inspect @@ -39,7 +39,7 @@ def __new__(cls, name, bases, attrs): new_class.add_to_class(n, v) for abstract in abstracts: - for n, v in abstract.__dict__.iteritems(): + for n, v in six.iteritems(abstract.__dict__): if isinstance(v, fields.Field) or n in ['LINKS']: # extend this condition if required; new_class.add_to_class(n, v) diff --git a/syncano/models/billing.py b/syncano/models/billing.py index 343a993..40ab638 100644 --- a/syncano/models/billing.py +++ b/syncano/models/billing.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from . import fields from .base import Model diff --git a/syncano/models/bulk.py b/syncano/models/bulk.py index ba95abf..c1bbd86 100644 --- a/syncano/models/bulk.py +++ b/syncano/models/bulk.py @@ -12,7 +12,6 @@ class BaseBulkCreate(object): instances = ObjectBulkCreate(objects, manager).process() """ __metaclass__ = ABCMeta - MAX_BATCH_SIZE = 50 @abstractmethod diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 14ffbde..34367d4 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from copy import deepcopy @@ -146,7 +146,7 @@ def __new__(cls, **kwargs): raise SyncanoValidationError('Field "class_name" is required.') model = cls.get_subclass_model(instance_name, class_name) - return model(**kwargs) + return model() @classmethod def _set_up_object_class(cls, model): @@ -165,6 +165,7 @@ def create_subclass(cls, name, schema): attrs = { 'Meta': deepcopy(Object._meta), '__new__': Model.__new__, # We don't want to have maximum recursion depth exceeded error + '__init__': Model.__init__, } for field in schema: diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 2fd8716..703e252 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from . import fields from .base import Model diff --git a/syncano/models/fields.py b/syncano/models/fields.py index dd86a9d..0a23884 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -60,6 +60,8 @@ def __eq__(self, other): def __lt__(self, other): if isinstance(other, Field): return self.creation_counter < other.creation_counter + if isinstance(other, int): + return self.creation_counter < other return NotImplemented def __hash__(self): # pragma: no cover diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index c62a326..05575d8 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + import json diff --git a/syncano/models/instances.py b/syncano/models/instances.py index adec6bd..013c0d6 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from . import fields from .base import Model diff --git a/syncano/models/manager.py b/syncano/models/manager.py index ead1ece..5053077 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -82,9 +82,6 @@ def __bool__(self): # pragma: no cover except IndexError: return False - def __nonzero__(self): # pragma: no cover - return type(self).__bool__(self) - def __getitem__(self, k): """ Retrieves an item or slice from the set of results. @@ -496,7 +493,7 @@ def old_update(self, *args, **kwargs): serialized = model.to_native() - serialized = {k: v for k, v in serialized.iteritems() + serialized = {k: v for k, v in six.iteritems(serialized) if k in self.data} self.data.update(serialized) @@ -686,7 +683,7 @@ def contribute_to_class(self, model, name): # pragma: no cover def _get_serialized_data(self): model = self.serialize(self.data, self.model) serialized = model.to_native() - serialized = {k: v for k, v in serialized.iteritems() + serialized = {k: v for k, v in six.iteritems(serialized) if k in self.data} self.data.update(serialized) return serialized diff --git a/syncano/models/registry.py b/syncano/models/registry.py index 7168f75..0777d0d 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + import re @@ -21,7 +21,7 @@ def __str__(self): return 'Registry: {0}'.format(', '.join(self.models)) def __unicode__(self): - return unicode(str(self)) + return str(str(self)) def __iter__(self): for name, model in six.iteritems(self.models): diff --git a/syncano/models/traces.py b/syncano/models/traces.py index b2b609b..04677eb 100644 --- a/syncano/models/traces.py +++ b/syncano/models/traces.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals + from . import fields from .base import Model diff --git a/syncano/release_utils.py b/syncano/release_utils.py index 2358091..e75c17c 100644 --- a/syncano/release_utils.py +++ b/syncano/release_utils.py @@ -21,7 +21,7 @@ def new_func(*args, **kwargs): self.removed_in_version ), category=DeprecationWarning, - filename=original_func.func_code.co_filename, - lineno=original_func.func_code.co_firstlineno + self.lineno) + filename=original_func.__code__.co_filename, + lineno=original_func.__code__.co_firstlineno + self.lineno) return original_func(*args, **kwargs) return new_func diff --git a/tests/integration_test.py b/tests/integration_test.py index 51a615a..22ffe4d 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -188,7 +188,7 @@ def test_update(self): ) cls.description = 'dummy' - for i in xrange(3): + for i in range(3): try: cls.save() except SyncanoRequestError as e: @@ -367,7 +367,7 @@ def test_source_run(self): trace.reload() self.assertEquals(trace.status, 'success') - self.assertDictEqual(trace.result, {u'stderr': u'', u'stdout': u'IntegrationTest'}) + self.assertDictEqual(trace.result, {'stderr': '', 'stdout': 'IntegrationTest'}) script.delete() @@ -447,7 +447,7 @@ def test_script_run(self): trace = script_endpoint.run() self.assertEquals(trace.status, 'success') - self.assertDictEqual(trace.result, {u'stderr': u'', u'stdout': u'IntegrationTest'}) + self.assertDictEqual(trace.result, {'stderr': '', 'stdout': 'IntegrationTest'}) script_endpoint.delete() def test_custom_script_run(self): diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py index e6c02d2..33cfce9 100644 --- a/tests/integration_test_accounts.py +++ b/tests/integration_test_accounts.py @@ -1,8 +1,9 @@ import os -from integration_test import IntegrationTest from syncano.connection import Connection +from .integration_test import IntegrationTest + class LoginTest(IntegrationTest): diff --git a/tests/integration_test_batch.py b/tests/integration_test_batch.py index 7975468..7e16767 100644 --- a/tests/integration_test_batch.py +++ b/tests/integration_test_batch.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import six from syncano.models import Class, Object, User from tests.integration_test import InstanceMixin, IntegrationTest @@ -111,7 +112,7 @@ def test_in_bulk_get(self): # test object bulk; bulk_res = self.klass.objects.in_bulk([self.update1.id, self.update2.id, self.update3.id]) - for res_id, res in bulk_res.iteritems(): + for res_id, res in six.iteritems(bulk_res): self.assertEqual(res_id, res.id) self.assertEqual(bulk_res[self.update1.id].title, self.update1.title) diff --git a/tests/test_channels.py b/tests/test_channels.py index 326b7af..ed8bdc8 100644 --- a/tests/test_channels.py +++ b/tests/test_channels.py @@ -114,5 +114,5 @@ def test_publish(self, connection_mock): connection_mock.request.assert_called_once_with( 'POST', '/v1.1/instances/None/channels/None/publish/', - data={'room': u'1', 'payload': '{"a": 1}'} + data={'room': '1', 'payload': '{"a": 1}'} ) diff --git a/tests/test_connection.py b/tests/test_connection.py index c60ccf7..ae27942 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,12 +1,18 @@ +import json import tempfile import unittest -from urlparse import urljoin +import six from syncano import connect from syncano.connection import Connection, ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValueError from syncano.models.registry import registry +if six.PY3: + from urllib.parse import urljoin +else: + from urlparse import urljoin + try: from unittest import mock except ImportError: @@ -251,14 +257,15 @@ def test_invalid_credentials(self, post_mock): self.assertTrue(post_mock.called) self.assertIsNone(self.connection.api_key) + call_args = post_mock.call_args[0] + call_kwargs = post_mock.call_args[1] + call_kwargs['data'] = json.loads(call_kwargs['data']) - post_mock.assert_called_once_with( - urljoin(self.connection.host, '{0}/'.format(self.connection.AUTH_SUFFIX)), - headers={'content-type': self.connection.CONTENT_TYPE}, - data='{"password": "dummy", "email": "dummy"}', - timeout=30, - verify=True - ) + self.assertEqual(call_args[0], urljoin(self.connection.host, '{0}/'.format(self.connection.AUTH_SUFFIX))) + self.assertEqual(call_kwargs['headers'], {'content-type': self.connection.CONTENT_TYPE}) + self.assertEqual(call_kwargs['timeout'], 30) + self.assertTrue(call_kwargs['verify']) + self.assertDictEqual(call_kwargs['data'], {"password": "dummy", "email": "dummy"}) @mock.patch('syncano.connection.Connection.make_request') def test_successful_authentication(self, make_request): @@ -274,8 +281,8 @@ def test_successful_authentication(self, make_request): @mock.patch('syncano.connection.Connection.make_request') def test_get_account_info(self, make_request): - info = {u'first_name': u'', u'last_name': u'', u'is_active': True, - u'id': 1, u'has_password': True, u'email': u'dummy'} + info = {'first_name': '', 'last_name': '', 'is_active': True, + 'id': 1, 'has_password': True, 'email': 'dummy'} self.test_successful_authentication() make_request.return_value = info self.assertFalse(make_request.called) @@ -286,8 +293,8 @@ def test_get_account_info(self, make_request): @mock.patch('syncano.connection.Connection.make_request') def test_get_account_info_with_api_key(self, make_request): - info = {u'first_name': u'', u'last_name': u'', u'is_active': True, - u'id': 1, u'has_password': True, u'email': u'dummy'} + info = {'first_name': '', 'last_name': '', 'is_active': True, + 'id': 1, 'has_password': True, 'email': 'dummy'} make_request.return_value = info self.assertFalse(make_request.called) self.assertIsNone(self.connection.api_key) @@ -305,7 +312,7 @@ def test_get_account_info_invalid_key(self, make_request): try: self.connection.get_account_info(api_key='invalid') self.assertTrue(False) - except SyncanoRequestError, e: + except SyncanoRequestError as e: self.assertIsNotNone(self.connection.api_key) self.assertTrue(make_request.called) self.assertEqual(e, err) @@ -317,14 +324,13 @@ def test_get_account_info_missing_key(self, make_request): try: self.connection.get_account_info() self.assertTrue(False) - except SyncanoValueError, e: + except SyncanoValueError: self.assertIsNone(self.connection.api_key) self.assertFalse(make_request.called) - self.assertIn('api_key', e.message) @mock.patch('syncano.connection.Connection.make_request') def test_get_user_info(self, make_request_mock): - info = {u'profile': {}} + info = {'profile': {}} make_request_mock.return_value = info self.assertFalse(make_request_mock.called) self.connection.api_key = 'Ala has a cat' @@ -336,7 +342,7 @@ def test_get_user_info(self, make_request_mock): @mock.patch('syncano.connection.Connection.make_request') def test_get_user_info_without_instance(self, make_request_mock): - info = {u'profile': {}} + info = {'profile': {}} make_request_mock.return_value = info self.assertFalse(make_request_mock.called) self.connection.api_key = 'Ala has a cat' @@ -347,7 +353,7 @@ def test_get_user_info_without_instance(self, make_request_mock): @mock.patch('syncano.connection.Connection.make_request') def test_get_user_info_without_auth_keys(self, make_request_mock): - info = {u'profile': {}} + info = {'profile': {}} make_request_mock.return_value = info self.assertFalse(make_request_mock.called) diff --git a/tests/test_fields.py b/tests/test_fields.py index 1de4319..743dbe5 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,3 +1,4 @@ +import json import unittest from datetime import datetime from functools import wraps @@ -105,11 +106,11 @@ def test_field_str(self): @skip_base_class def test_field_unicode(self): - expected = u'<{0}: {1}>'.format( + expected = '<{0}: {1}>'.format( self.field.__class__.__name__, self.field_name ) - out = unicode(self.field) + out = str(self.field) self.assertEqual(out, expected) @skip_base_class @@ -248,11 +249,11 @@ class StringFieldTestCase(BaseTestCase): def test_to_python(self): self.assertEqual(self.field.to_python(None), None) self.assertEqual(self.field.to_python('test'), 'test') - self.assertEqual(self.field.to_python(10), u'10') - self.assertEqual(self.field.to_python(10.0), u'10.0') - self.assertEqual(self.field.to_python(True), u'True') - self.assertEqual(self.field.to_python({'a': 1}), u"{'a': 1}") - self.assertEqual(self.field.to_python([1, 2]), u"[1, 2]") + self.assertEqual(self.field.to_python(10), '10') + self.assertEqual(self.field.to_python(10.0), '10.0') + self.assertEqual(self.field.to_python(True), 'True') + self.assertEqual(self.field.to_python({'a': 1}), "{'a': 1}") + self.assertEqual(self.field.to_python([1, 2]), "[1, 2]") class IntegerFieldTestCase(BaseTestCase): @@ -524,5 +525,5 @@ def test_to_native(self): schema = SchemaManager(value) self.assertEqual(self.field.to_native(None), None) - self.assertEqual(self.field.to_native(schema), '[{"type": "string", "name": "username"}]') - self.assertEqual(self.field.to_native(value), '[{"type": "string", "name": "username"}]') + self.assertListEqual(json.loads(self.field.to_native(schema)), [{"type": "string", "name": "username"}]) + self.assertListEqual(json.loads(self.field.to_native(value)), [{"type": "string", "name": "username"}]) diff --git a/tests/test_incentives.py b/tests/test_incentives.py index 0b3e157..6b2b3f4 100644 --- a/tests/test_incentives.py +++ b/tests/test_incentives.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +import json import unittest from datetime import datetime @@ -31,9 +31,11 @@ def test_run(self, connection_mock): self.assertIsInstance(result, ScriptTrace) connection_mock.assert_called_once_with(a=1, b=2) - connection_mock.request.assert_called_once_with( - 'POST', '/v1.1/instances/test/snippets/scripts/10/run/', data={'payload': '{"a": 1, "b": 2}'} - ) + call_args = connection_mock.request.call_args[0] + call_kwargs = connection_mock.request.call_args[1] + call_kwargs['data']['payload'] = json.loads(call_kwargs['data']['payload']) + self.assertEqual(('POST', '/v1.1/instances/test/snippets/scripts/10/run/'), call_args) + self.assertDictEqual(call_kwargs['data'], {'payload': {"a": 1, "b": 2}}) model = Script() with self.assertRaises(SyncanoValidationError): @@ -52,7 +54,7 @@ def test_run(self, connection_mock): connection_mock.request.return_value = { 'status': 'success', 'duration': 937, - 'result': {u'stdout': 1, u'stderr': u''}, + 'result': {'stdout': 1, 'stderr': ''}, 'executed_at': '2015-03-16T11:52:14.172830Z' } @@ -64,7 +66,7 @@ def test_run(self, connection_mock): self.assertIsInstance(result, ScriptEndpointTrace) self.assertEqual(result.status, 'success') self.assertEqual(result.duration, 937) - self.assertEqual(result.result, {u'stdout': 1, u'stderr': u''}) + self.assertEqual(result.result, {'stdout': 1, 'stderr': ''}) self.assertIsInstance(result.executed_at, datetime) connection_mock.assert_called_once_with(x=1, y=2) diff --git a/tests/test_manager.py b/tests/test_manager.py index ee2bdc7..4466c72 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1,3 +1,4 @@ +import json import unittest from datetime import datetime @@ -430,7 +431,7 @@ def test_request(self, connection_mock): request_mock.assert_called_once_with( 'GET', - u'/v1.1/instances/', + '/v1.1/instances/', data={'b': 2}, headers={}, params={'a': 1} @@ -520,7 +521,7 @@ def test_run(self, clone_mock, filter_mock, request_mock): self.assertEqual(self.manager.method, 'POST') self.assertEqual(self.manager.endpoint, 'run') - self.assertEqual(self.manager.data['payload'], '{"y": 2, "x": 1}') + self.assertDictEqual(json.loads(self.manager.data['payload']), {"y": 2, "x": 1}) class ScriptEndpointManagerTestCase(unittest.TestCase): @@ -559,7 +560,7 @@ def test_run(self, clone_mock, filter_mock, request_mock): self.assertEqual(self.manager.method, 'POST') self.assertEqual(self.manager.endpoint, 'run') - self.assertEqual(self.manager.data['payload'], '{"y": 2, "x": 1}') + self.assertDictEqual(json.loads(self.manager.data['payload']), {"y": 2, "x": 1}) class ObjectManagerTestCase(unittest.TestCase): @@ -613,7 +614,10 @@ def test_filter(self, get_subclass_model_mock, clone_mock): self.assertEqual(self.manager.query['query'], '{"name": {"_gt": "test"}}') self.manager.filter(name__gt='test', description='test') - self.assertEqual(self.manager.query['query'], '{"description": {"_eq": "test"}, "name": {"_gt": "test"}}') + self.assertDictEqual( + json.loads(self.manager.query['query']), + {"description": {"_eq": "test"}, "name": {"_gt": "test"}} + ) with self.assertRaises(SyncanoValueError): self.manager.filter(dummy_field=4) diff --git a/tests/test_models.py b/tests/test_models.py index c6883c3..ae63899 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -17,7 +17,7 @@ def setUp(self): def test_init(self): self.assertTrue(hasattr(self.model, '_raw_data')) - self.assertEquals(self.model._raw_data, {}) + self.assertEqual(self.model._raw_data, {}) model = Instance(name='test', dummy_field='dummy') self.assertTrue('name' in model._raw_data) @@ -40,11 +40,11 @@ def test_str(self): self.assertEqual(out, expected) def test_unicode(self): - expected = u'<{0}: {1}>'.format( + expected = '<{0}: {1}>'.format( self.model.__class__.__name__, self.model.pk ) - out = unicode(self.model) + out = str(self.model) self.assertEqual(out, expected) def test_eq(self): diff --git a/tests/test_push.py b/tests/test_push.py index 8ad587a..e71b974 100644 --- a/tests/test_push.py +++ b/tests/test_push.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- - +import json import unittest from datetime import datetime @@ -31,14 +31,14 @@ def test_gcm_device(self, connection_mock): connection_mock.assert_called_once_with() connection_mock.request.assert_called_once_with( - u'POST', u'/v1.1/instances/test/push_notifications/gcm/devices/', - data={"registration_id": u'86152312314401555', "device_id": "10000000001", "is_active": True, + 'POST', '/v1.1/instances/test/push_notifications/gcm/devices/', + data={"registration_id": '86152312314401555', "device_id": "10000000001", "is_active": True, "label": "example label"} ) model.created_at = datetime.now() # to Falsify is_new() model.delete() connection_mock.request.assert_called_with( - u'DELETE', u'/v1.1/instances/test/push_notifications/gcm/devices/86152312314401555/' + 'DELETE', '/v1.1/instances/test/push_notifications/gcm/devices/86152312314401555/' ) @mock.patch('syncano.models.APNSDevice._get_connection') @@ -64,15 +64,15 @@ def test_apns_device(self, connection_mock): connection_mock.assert_called_once_with() connection_mock.request.assert_called_once_with( - u'POST', u'/v1.1/instances/test/push_notifications/apns/devices/', - data={"registration_id": u'86152312314401555', "device_id": "10000000001", "is_active": True, + 'POST', '/v1.1/instances/test/push_notifications/apns/devices/', + data={"registration_id": '86152312314401555', "device_id": "10000000001", "is_active": True, "label": "example label"} ) model.created_at = datetime.now() # to Falsify is_new() model.delete() connection_mock.request.assert_called_with( - u'DELETE', u'/v1.1/instances/test/push_notifications/apns/devices/86152312314401555/' + 'DELETE', '/v1.1/instances/test/push_notifications/apns/devices/86152312314401555/' ) @mock.patch('syncano.models.GCMMessage._get_connection') @@ -92,9 +92,16 @@ def test_gcm_message(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.assert_called_once_with() - connection_mock.request.assert_called_once_with( - u'POST', u'/v1.1/instances/test/push_notifications/gcm/messages/', - data={'content': '{"environment": "production", "data": "some data"}'} + + call_args = connection_mock.request.call_args[0] + call_kwargs = connection_mock.request.call_args[1] + + call_kwargs['data']['content'] = json.loads(call_kwargs['data']['content']) + + self.assertEqual(('POST', '/v1.1/instances/test/push_notifications/gcm/messages/'), call_args) + self.assertDictEqual( + {'data': {'content': {"environment": "production", "data": "some data"}}}, + call_kwargs, ) @mock.patch('syncano.models.APNSMessage._get_connection') @@ -114,7 +121,8 @@ def test_apns_message(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.assert_called_once_with() - connection_mock.request.assert_called_once_with( - u'POST', u'/v1.1/instances/test/push_notifications/apns/messages/', - data={'content': '{"environment": "production", "data": "some data"}'} - ) + call_args = connection_mock.request.call_args[0] + call_kwargs = connection_mock.request.call_args[1] + call_kwargs['data']['content'] = json.loads(call_kwargs['data']['content']) + self.assertEqual(('POST', '/v1.1/instances/test/push_notifications/apns/messages/'), call_args) + self.assertDictEqual(call_kwargs['data'], {'content': {"environment": "production", "data": "some data"}}) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6c4b0ec --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py27,py34 +[testenv] +deps= -rrequirements.txt +commands=coverage run -m unittest discover -p 'test*.py' From c30bc458bd1011d06edb5d9385a239cbb3893a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 14 Mar 2016 17:27:56 +0100 Subject: [PATCH 302/558] [LIB-566] add __nonzero__ to support bool evaluation in python 2 --- syncano/models/manager.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 5053077..f099aea 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -75,6 +75,13 @@ def __len__(self): # pragma: no cover def __iter__(self): # pragma: no cover return iter(self.iterator()) + def __nonzero__(self): + try: + self[0] + return True + except IndexError: + return False + def __bool__(self): # pragma: no cover try: self[0] From b56afd084ce524992d2e65cb4da4f97ac18ad126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 14 Mar 2016 18:04:16 +0100 Subject: [PATCH 303/558] [LIB-566] disable profile test for now; --- syncano/models/accounts.py | 3 ++- syncano/models/classes.py | 5 +++-- tests/integration_test_user_profile.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index a09dcec..fa2d9df 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -100,7 +100,8 @@ class User(Model): password = fields.StringField(read_only=False, required=True) user_key = fields.StringField(read_only=True, required=False) - profile = fields.ModelField('Profile') + # TODO: correct this: add relation which handle DataObject + profile = fields.JSONField() links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 34367d4..dd2e286 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -146,7 +146,9 @@ def __new__(cls, **kwargs): raise SyncanoValidationError('Field "class_name" is required.') model = cls.get_subclass_model(instance_name, class_name) - return model() + import pdb + pdb.set_trace() + return model(**kwargs) @classmethod def _set_up_object_class(cls, model): @@ -165,7 +167,6 @@ def create_subclass(cls, name, schema): attrs = { 'Meta': deepcopy(Object._meta), '__new__': Model.__new__, # We don't want to have maximum recursion depth exceeded error - '__init__': Model.__init__, } for field in schema: diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user_profile.py index 37a8def..a5c1092 100644 --- a/tests/integration_test_user_profile.py +++ b/tests/integration_test_user_profile.py @@ -26,7 +26,8 @@ def test_profile_klass(self): self.assertTrue(klass) self.assertEqual(klass.instance_name, self.instance.name) - def test_profile_change_schema(self): + # TODO: correct this: add relation which handle DataObject + def _test_profile_change_schema(self): klass = self.user.profile.get_class_object() klass.schema = [ {'name': 'profile_pic', 'type': 'string'} From fa1fce8449a862719a59d18897ef8dcd47207c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 14 Mar 2016 18:06:27 +0100 Subject: [PATCH 304/558] [LIB-566] disable profile test for now; --- syncano/models/classes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index dd2e286..a548bf2 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -146,8 +146,6 @@ def __new__(cls, **kwargs): raise SyncanoValidationError('Field "class_name" is required.') model = cls.get_subclass_model(instance_name, class_name) - import pdb - pdb.set_trace() return model(**kwargs) @classmethod From 4909ff1f208e64fb0040b66c1f8ff426895c343e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 14:58:15 +0100 Subject: [PATCH 305/558] [LIB-566] small fixes; fighthing with python3 error --- syncano/models/archetypes.py | 1 + syncano/models/bulk.py | 4 ++-- syncano/models/classes.py | 1 + syncano/models/fields.py | 1 + syncano/models/manager.py | 1 - 5 files changed, 5 insertions(+), 3 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index d1c75b3..ce42147 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -83,6 +83,7 @@ def build_doc(cls, name, meta): class Model(six.with_metaclass(ModelMetaclass)): """Base class for all models. """ + def __init__(self, **kwargs): self.is_lazy = kwargs.pop('is_lazy', False) self._raw_data = {} diff --git a/syncano/models/bulk.py b/syncano/models/bulk.py index c1bbd86..acd2f72 100644 --- a/syncano/models/bulk.py +++ b/syncano/models/bulk.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- +import six from abc import ABCMeta, abstractmethod from syncano.exceptions import SyncanoValidationError, SyncanoValueError -class BaseBulkCreate(object): +class BaseBulkCreate(six.with_metaclass(ABCMeta)): """ Helper class for making bulk create; Usage: instances = ObjectBulkCreate(objects, manager).process() """ - __metaclass__ = ABCMeta MAX_BATCH_SIZE = 50 @abstractmethod diff --git a/syncano/models/classes.py b/syncano/models/classes.py index a548bf2..ad50f23 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -101,6 +101,7 @@ class Object(Model): This model is special because each instance will be **dynamically populated** with fields defined in related :class:`~syncano.models.base.Class` schema attribute. """ + PERMISSIONS_CHOICES = ( {'display_name': 'None', 'value': 'none'}, {'display_name': 'Read', 'value': 'read'}, diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 0a23884..a85d5d7 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -163,6 +163,7 @@ def contribute_to_class(self, cls, name): class RelatedManagerField(Field): def __init__(self, model_name, endpoint='list'): + super(RelatedManagerField, self).__init__() self.model_name = model_name self.endpoint = endpoint diff --git a/syncano/models/manager.py b/syncano/models/manager.py index f099aea..b24019c 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -746,7 +746,6 @@ def serialize(self, data, model=None): properties = deepcopy(self.properties) properties.update(data) - return model(**properties) if self._serialize else data def build_request(self, request): From 4135893d9e29544f800a6b00118f6d6f317db03e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 15 Mar 2016 15:04:29 +0100 Subject: [PATCH 306/558] [LIB-566] correcct isort issues --- syncano/models/bulk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/bulk.py b/syncano/models/bulk.py index acd2f72..4786ff1 100644 --- a/syncano/models/bulk.py +++ b/syncano/models/bulk.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- -import six from abc import ABCMeta, abstractmethod +import six from syncano.exceptions import SyncanoValidationError, SyncanoValueError From 2751098f5e8f69d736081dadca63abef5d0c5801 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 15 Mar 2016 17:04:08 +0100 Subject: [PATCH 307/558] [LIB-566] more hacking than coding - py3 support with py2 supp; --- syncano/models/classes.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index ad50f23..19f2f9c 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -163,18 +163,26 @@ def _get_class_name(cls, kwargs): @classmethod def create_subclass(cls, name, schema): + meta = deepcopy(Object._meta) attrs = { - 'Meta': deepcopy(Object._meta), + 'Meta': meta, '__new__': Model.__new__, # We don't want to have maximum recursion depth exceeded error + 'please': ObjectManager() } + model = type(str(name), (Model, ), attrs) + for field in schema: field_type = field.get('type') field_class = fields.MAPPING[field_type] query_allowed = ('order_index' in field or 'filter_index' in field) - attrs[field['name']] = field_class(required=False, read_only=False, - query_allowed=query_allowed) - model = type(str(name), (Object, ), attrs) + field_class(required=False, read_only=False, query_allowed=query_allowed).contribute_to_class( + model, field.get('name') + ) + + for field in meta.fields: + setattr(model, field.name, field) + cls._set_up_object_class(model) return model From 0003fc59ac31c914a888df1f4f47f00307e44778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 17:24:24 +0100 Subject: [PATCH 308/558] [LIB-566] restore profile; add pk field; --- syncano/models/accounts.py | 3 +-- syncano/models/classes.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index fa2d9df..a09dcec 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -100,8 +100,7 @@ class User(Model): password = fields.StringField(read_only=False, required=True) user_key = fields.StringField(read_only=True, required=False) - # TODO: correct this: add relation which handle DataObject - profile = fields.JSONField() + profile = fields.ModelField('Profile') links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 19f2f9c..d3e869f 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -181,6 +181,8 @@ def create_subclass(cls, name, schema): ) for field in meta.fields: + if field.primary_key: + setattr(model, 'pk', field) setattr(model, field.name, field) cls._set_up_object_class(model) From b839367094f6fc9d46c040543611d30b2e456a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 17:49:32 +0100 Subject: [PATCH 309/558] [LIB-566] correct test --- tests/integration_test.py | 4 ++-- tests/integration_test_batch.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 22ffe4d..12cd67f 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -7,7 +7,7 @@ import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError -from syncano.models import ApiKey, Class, Instance, Object, Script, ScriptEndpoint, registry +from syncano.models import ApiKey, Class, Instance, Model, Object, Script, ScriptEndpoint, registry class IntegrationTest(unittest.TestCase): @@ -303,7 +303,7 @@ def test_count_and_with_count(self): self.assertEqual(count, 2) for o in objects: - self.assertTrue(isinstance(o, self.model)) + self.assertTrue(isinstance(o, Model)) author_one.delete() author_two.delete() diff --git a/tests/integration_test_batch.py b/tests/integration_test_batch.py index 7e16767..c215d8d 100644 --- a/tests/integration_test_batch.py +++ b/tests/integration_test_batch.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import six -from syncano.models import Class, Object, User +from syncano.models import Class, Model, Object, User from tests.integration_test import InstanceMixin, IntegrationTest @@ -25,7 +25,7 @@ def test_batch_create(self): results = Object.please.bulk_create(*objects) for r in results: - self.assertTrue(isinstance(r, Object)) + self.assertTrue(isinstance(r, Model)) self.assertTrue(r.id) self.assertTrue(r.title) From db1c254de35950cb7cad01497669d16040882566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 17:54:44 +0100 Subject: [PATCH 310/558] [LIB-566] correct test --- tests/integration_test_batch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_batch.py b/tests/integration_test_batch.py index c215d8d..c7e4b6e 100644 --- a/tests/integration_test_batch.py +++ b/tests/integration_test_batch.py @@ -37,7 +37,7 @@ def test_batch_create(self): ) for r in results: - self.assertTrue(isinstance(r, Object)) + self.assertTrue(isinstance(r, Model)) self.assertTrue(r.id) self.assertTrue(r.title) From ab3a70454e4d2c30f1bd235b2937b131cca13cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 18:05:21 +0100 Subject: [PATCH 311/558] [LIB-566] change circle test to run tox --- circle.yml | 4 ++-- requirements-test.txt | 1 + tox.ini | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 requirements-test.txt diff --git a/circle.yml b/circle.yml index f34cd99..ad2de2b 100644 --- a/circle.yml +++ b/circle.yml @@ -5,11 +5,11 @@ machine: dependencies: pre: - pip install -U setuptools - - pip install -r requirements.txt + - pip install -r requirements-test.txt test: override: - - ./run_tests.sh + - tox general: artifacts: diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..aff493c --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1 @@ +tox==2.3.1 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 6c4b0ec..15a86d5 100644 --- a/tox.ini +++ b/tox.ini @@ -2,4 +2,4 @@ envlist = py27,py34 [testenv] deps= -rrequirements.txt -commands=coverage run -m unittest discover -p 'test*.py' +commands=./run_tests.sh From 9c74046a75291b06d06b519e475845215b0c0313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 18:14:57 +0100 Subject: [PATCH 312/558] [LIB-566] add pyenv dependencies to circle --- circle.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/circle.yml b/circle.yml index ad2de2b..1b90f0e 100644 --- a/circle.yml +++ b/circle.yml @@ -6,6 +6,8 @@ dependencies: pre: - pip install -U setuptools - pip install -r requirements-test.txt + post: + - pyenv local 3.4.3 2.7.10 test: override: From 8689682994dd1325448516464c4b7f49ed04ef9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 18:28:16 +0100 Subject: [PATCH 313/558] correct generate_hash method --- tests/integration_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 12cd67f..713bacf 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -32,7 +32,8 @@ def tearDownClass(cls): @classmethod def generate_hash(cls): - return md5('%s%s' % (uuid4(), datetime.now())).hexdigest() + hash_feed = '{}{}'.format(uuid4(), datetime.now()) + return md5(hash_feed.encode('ascii')).hexdigest() class InstanceMixin(object): From 323724cb657019c0a63d9b4a89adc945fe021dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 18:41:07 +0100 Subject: [PATCH 314/558] [LIB-566] change python 27 version --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 1b90f0e..dcfc0a2 100644 --- a/circle.yml +++ b/circle.yml @@ -7,7 +7,7 @@ dependencies: - pip install -U setuptools - pip install -r requirements-test.txt post: - - pyenv local 3.4.3 2.7.10 + - pyenv local 3.4.3 2.7.6 test: override: From 3e40c611ce3d4a4594d43864f4adcaa934af046b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 18:49:48 +0100 Subject: [PATCH 315/558] [LIB-566] add integration variuables to tox.ini --- tox.ini | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tox.ini b/tox.ini index 15a86d5..4b097b3 100644 --- a/tox.ini +++ b/tox.ini @@ -3,3 +3,11 @@ envlist = py27,py34 [testenv] deps= -rrequirements.txt commands=./run_tests.sh +setenv = + INTEGRATION_API_KEY = $INTEGRATION_API_KEY + INTEGRATION_API_EMAIL = $INTEGRATION_API_EMAIL + INTEGRATION_API_PASSWORD = $INTEGRATION_API_PASSWORD + INTEGRATION_API_ROOT = $INTEGRATION_API_ROOT + INTEGRATION_INSTANCE_NAME = $INTEGRATION_INSTANCE_NAME + INTEGRATION_USER_NAME = $INTEGRATION_USER_NAME + INTEGRATION_USER_PASSWORD = $INTEGRATION_USER_PASSWORD From 7e1fe6d175d7a159a23aacca7a65494c268644fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 19:17:35 +0100 Subject: [PATCH 316/558] [LIB-566] change order in tox.ini --- tox.ini | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 4b097b3..035f712 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,14 @@ [tox] envlist = py27,py34 [testenv] -deps= -rrequirements.txt -commands=./run_tests.sh setenv = + INTEGRATION_API_ROOT = $INTEGRATION_API_ROOT INTEGRATION_API_KEY = $INTEGRATION_API_KEY INTEGRATION_API_EMAIL = $INTEGRATION_API_EMAIL INTEGRATION_API_PASSWORD = $INTEGRATION_API_PASSWORD - INTEGRATION_API_ROOT = $INTEGRATION_API_ROOT INTEGRATION_INSTANCE_NAME = $INTEGRATION_INSTANCE_NAME INTEGRATION_USER_NAME = $INTEGRATION_USER_NAME INTEGRATION_USER_PASSWORD = $INTEGRATION_USER_PASSWORD + +deps= -rrequirements.txt +commands=./run_tests.sh \ No newline at end of file From e92dd945bc1c8f337c3d26e85951977f91855511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 19:32:46 +0100 Subject: [PATCH 317/558] [LIB-566] passenv instead of setenv in tox.ini --- tox.ini | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tox.ini b/tox.ini index 035f712..fc2d2cf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,6 @@ [tox] envlist = py27,py34 [testenv] -setenv = - INTEGRATION_API_ROOT = $INTEGRATION_API_ROOT - INTEGRATION_API_KEY = $INTEGRATION_API_KEY - INTEGRATION_API_EMAIL = $INTEGRATION_API_EMAIL - INTEGRATION_API_PASSWORD = $INTEGRATION_API_PASSWORD - INTEGRATION_INSTANCE_NAME = $INTEGRATION_INSTANCE_NAME - INTEGRATION_USER_NAME = $INTEGRATION_USER_NAME - INTEGRATION_USER_PASSWORD = $INTEGRATION_USER_PASSWORD - +passenv = INTEGRATION_API_ROOT INTEGRATION_API_KEY INTEGRATION_API_EMAIL INTEGRATION_API_PASSWORD INTEGRATION_INSTANCE_NAME INTEGRATION_USER_NAME INTEGRATION_USER_PASSWORD deps= -rrequirements.txt commands=./run_tests.sh \ No newline at end of file From ca023d39f327b3fd5f669d9bccc265f30aa71881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 19:43:44 +0100 Subject: [PATCH 318/558] [LIB-566] remove not supported in py3 assert: assertItemsEqual --- tests/integration_test_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py index 33cfce9..36d6e90 100644 --- a/tests/integration_test_accounts.py +++ b/tests/integration_test_accounts.py @@ -40,7 +40,7 @@ def check_connection(self, con): obj_list = response['objects'] self.assertEqual(len(obj_list), 2) - self.assertItemsEqual([o['name'] for o in obj_list], ['user_profile', self.CLASS_NAME]) + self.assertEqual(sorted([o['name'] for o in obj_list]), sorted(['user_profile', self.CLASS_NAME])) def test_admin_login(self): con = Connection(host=self.API_ROOT, From 3ff1d249a6fd1045ea30459466df3933213a303d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 15 Mar 2016 20:41:56 +0100 Subject: [PATCH 319/558] [LIB-566] restore profile test; --- tests/integration_test_user_profile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user_profile.py index a5c1092..37a8def 100644 --- a/tests/integration_test_user_profile.py +++ b/tests/integration_test_user_profile.py @@ -26,8 +26,7 @@ def test_profile_klass(self): self.assertTrue(klass) self.assertEqual(klass.instance_name, self.instance.name) - # TODO: correct this: add relation which handle DataObject - def _test_profile_change_schema(self): + def test_profile_change_schema(self): klass = self.user.profile.get_class_object() klass.schema = [ {'name': 'profile_pic', 'type': 'string'} From 947706487e2c38041014eb485cac8ebf58b63fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 16 Mar 2016 09:54:51 +0100 Subject: [PATCH 320/558] [LIB-566] add six.u in some places --- syncano/models/fields.py | 4 ++-- syncano/models/registry.py | 2 +- tests/test_fields.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index a85d5d7..1fbf021 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -162,8 +162,8 @@ def contribute_to_class(self, cls, name): class RelatedManagerField(Field): - def __init__(self, model_name, endpoint='list'): - super(RelatedManagerField, self).__init__() + def __init__(self, model_name, endpoint='list', *args, **kwargs): + super(RelatedManagerField, self).__init__(*args, **kwargs) self.model_name = model_name self.endpoint = endpoint diff --git a/syncano/models/registry.py b/syncano/models/registry.py index 0777d0d..e6be17b 100644 --- a/syncano/models/registry.py +++ b/syncano/models/registry.py @@ -21,7 +21,7 @@ def __str__(self): return 'Registry: {0}'.format(', '.join(self.models)) def __unicode__(self): - return str(str(self)) + return six.u(str(self)) def __iter__(self): for name, model in six.iteritems(self.models): diff --git a/tests/test_fields.py b/tests/test_fields.py index 743dbe5..090b83a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -4,6 +4,7 @@ from functools import wraps from time import mktime +import six from syncano import models from syncano.exceptions import SyncanoValidationError, SyncanoValueError from syncano.models.manager import SchemaManager @@ -106,7 +107,7 @@ def test_field_str(self): @skip_base_class def test_field_unicode(self): - expected = '<{0}: {1}>'.format( + expected = six.u('<{0}: {1}>').format( self.field.__class__.__name__, self.field_name ) From 32b4ccb450901d50af1ccb22a0478c13cbd0ff6a Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 17 Mar 2016 16:05:24 +0100 Subject: [PATCH 321/558] [LIB-598] update docs and docs links; --- README.rst | 4 +- docs/source/conf.py | 3 + docs/source/refs/syncano.models.accounts.rst | 7 ++ .../source/refs/syncano.models.archetypes.rst | 7 ++ docs/source/refs/syncano.models.base.rst | 7 -- docs/source/refs/syncano.models.billing.rst | 7 ++ docs/source/refs/syncano.models.channels.rst | 7 ++ docs/source/refs/syncano.models.classes.rst | 7 ++ .../refs/syncano.models.custom_response.rst | 7 ++ .../source/refs/syncano.models.data_views.rst | 7 ++ .../source/refs/syncano.models.incentives.rst | 7 ++ docs/source/refs/syncano.models.instances.rst | 7 ++ .../refs/syncano.models.push_notification.rst | 7 ++ docs/source/refs/syncano.models.rst | 14 +++- docs/source/refs/syncano.models.traces.rst | 7 ++ syncano/__init__.py | 3 +- syncano/models/accounts.py | 6 +- syncano/models/billing.py | 4 +- syncano/models/channels.py | 4 +- syncano/models/classes.py | 5 +- syncano/models/custom_response.py | 33 ++++---- syncano/models/incentives.py | 9 +-- syncano/models/instances.py | 22 +++++- syncano/models/manager.py | 61 ++++++++------- syncano/models/push_notification.py | 76 ++++++++++--------- 25 files changed, 219 insertions(+), 109 deletions(-) create mode 100644 docs/source/refs/syncano.models.accounts.rst create mode 100644 docs/source/refs/syncano.models.archetypes.rst delete mode 100644 docs/source/refs/syncano.models.base.rst create mode 100644 docs/source/refs/syncano.models.billing.rst create mode 100644 docs/source/refs/syncano.models.channels.rst create mode 100644 docs/source/refs/syncano.models.classes.rst create mode 100644 docs/source/refs/syncano.models.custom_response.rst create mode 100644 docs/source/refs/syncano.models.data_views.rst create mode 100644 docs/source/refs/syncano.models.incentives.rst create mode 100644 docs/source/refs/syncano.models.instances.rst create mode 100644 docs/source/refs/syncano.models.push_notification.rst create mode 100644 docs/source/refs/syncano.models.traces.rst diff --git a/README.rst b/README.rst index 49a0af5..84854b4 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,9 @@ Syncano Python QuickStart Guide ----------------------- -You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/v1.0/docs/python). +You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/docs/python). -For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/v1.0/docs/getting-started-with-syncano) should be very helpful. +For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/docs/getting-started-with-syncano) should be very helpful. In case you need help working with the library - email us at libraries@syncano.com - we will be happy to help! diff --git a/docs/source/conf.py b/docs/source/conf.py index ee4c172..228e9e5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -108,3 +108,6 @@ autodoc_member_order = 'bysource' highlight_language = 'python' + +from syncano.models.fields import RelatedManagerField +RelatedManagerField.__get__ = lambda self, *args, **kwargs: self diff --git a/docs/source/refs/syncano.models.accounts.rst b/docs/source/refs/syncano.models.accounts.rst new file mode 100644 index 0000000..b97b663 --- /dev/null +++ b/docs/source/refs/syncano.models.accounts.rst @@ -0,0 +1,7 @@ +syncano.models.accounts +======================= + +.. automodule:: syncano.models.accounts + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.archetypes.rst b/docs/source/refs/syncano.models.archetypes.rst new file mode 100644 index 0000000..498041a --- /dev/null +++ b/docs/source/refs/syncano.models.archetypes.rst @@ -0,0 +1,7 @@ +syncano.models.archetypes +========================= + +.. automodule:: syncano.models.archetypes + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.base.rst b/docs/source/refs/syncano.models.base.rst deleted file mode 100644 index bfc76bd..0000000 --- a/docs/source/refs/syncano.models.base.rst +++ /dev/null @@ -1,7 +0,0 @@ -syncano.models.base -=================== - -.. automodule:: syncano.models.base - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/refs/syncano.models.billing.rst b/docs/source/refs/syncano.models.billing.rst new file mode 100644 index 0000000..a89281c --- /dev/null +++ b/docs/source/refs/syncano.models.billing.rst @@ -0,0 +1,7 @@ +syncano.models.billing +====================== + +.. automodule:: syncano.models.billing + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.channels.rst b/docs/source/refs/syncano.models.channels.rst new file mode 100644 index 0000000..e1f93a0 --- /dev/null +++ b/docs/source/refs/syncano.models.channels.rst @@ -0,0 +1,7 @@ +syncano.models.channels +======================= + +.. automodule:: syncano.models.channels + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.classes.rst b/docs/source/refs/syncano.models.classes.rst new file mode 100644 index 0000000..6482438 --- /dev/null +++ b/docs/source/refs/syncano.models.classes.rst @@ -0,0 +1,7 @@ +syncano.models.classes +====================== + +.. automodule:: syncano.models.classes + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.custom_response.rst b/docs/source/refs/syncano.models.custom_response.rst new file mode 100644 index 0000000..4fcfc3c --- /dev/null +++ b/docs/source/refs/syncano.models.custom_response.rst @@ -0,0 +1,7 @@ +syncano.models.custom_response +============================== + +.. automodule:: syncano.models.custom_response + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.data_views.rst b/docs/source/refs/syncano.models.data_views.rst new file mode 100644 index 0000000..f4d810a --- /dev/null +++ b/docs/source/refs/syncano.models.data_views.rst @@ -0,0 +1,7 @@ +syncano.models.data_views +========================= + +.. automodule:: syncano.models.data_views + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.incentives.rst b/docs/source/refs/syncano.models.incentives.rst new file mode 100644 index 0000000..a387150 --- /dev/null +++ b/docs/source/refs/syncano.models.incentives.rst @@ -0,0 +1,7 @@ +syncano.models.incentives +========================= + +.. automodule:: syncano.models.incentives + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.instances.rst b/docs/source/refs/syncano.models.instances.rst new file mode 100644 index 0000000..725f813 --- /dev/null +++ b/docs/source/refs/syncano.models.instances.rst @@ -0,0 +1,7 @@ +syncano.models.instances +======================== + +.. automodule:: syncano.models.instances + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.push_notification.rst b/docs/source/refs/syncano.models.push_notification.rst new file mode 100644 index 0000000..ea58efe --- /dev/null +++ b/docs/source/refs/syncano.models.push_notification.rst @@ -0,0 +1,7 @@ +syncano.models.push_notification +================================ + +.. automodule:: syncano.models.push_notification + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.rst b/docs/source/refs/syncano.models.rst index f89b391..e22a4b5 100644 --- a/docs/source/refs/syncano.models.rst +++ b/docs/source/refs/syncano.models.rst @@ -6,11 +6,21 @@ Submodules .. toctree:: - syncano.models.base - syncano.models.fields syncano.models.manager + syncano.models.accounts + syncano.models.archetypes + syncano.models.billing + syncano.models.channels + syncano.models.classes + syncano.models.custom_response + syncano.models.data_views + syncano.models.incentives + syncano.models.instances + syncano.models.fields syncano.models.options + syncano.models.push_notification syncano.models.registry + syncano.models.traces Module contents --------------- diff --git a/docs/source/refs/syncano.models.traces.rst b/docs/source/refs/syncano.models.traces.rst new file mode 100644 index 0000000..e530491 --- /dev/null +++ b/docs/source/refs/syncano.models.traces.rst @@ -0,0 +1,7 @@ +syncano.models.traces +===================== + +.. automodule:: syncano.models.traces + :members: + :undoc-members: + :show-inheritance: diff --git a/syncano/__init__.py b/syncano/__init__.py index ca775a2..70d1097 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -69,8 +69,7 @@ def connect(*args, **kwargs): # OR connection = syncano.connect(api_key='') # OR - connection = syncano.connect(social_backend='github', - token='sfdsdfsdf') + connection = syncano.connect(social_backend='github', token='sfdsdfsdf') # User login connection = syncano.connect(username='', password='', api_key='', instance_name='') diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index a09dcec..38346d0 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -9,7 +9,7 @@ class Admin(Model): """ - OO wrapper around instance admins `endpoint `_. + OO wrapper around instance admins `link `_. :ivar first_name: :class:`~syncano.models.fields.StringField` :ivar last_name: :class:`~syncano.models.fields.StringField` @@ -86,7 +86,7 @@ class Meta: class User(Model): """ - OO wrapper around users `endpoint `_. + OO wrapper around users `link `_. :ivar username: :class:`~syncano.models.fields.StringField` :ivar password: :class:`~syncano.models.fields.StringField` @@ -156,7 +156,7 @@ def remove_from_group(self, group_id): class Group(Model): """ - OO wrapper around groups `endpoint `_. + OO wrapper around groups `link `_. :ivar label: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` diff --git a/syncano/models/billing.py b/syncano/models/billing.py index 40ab638..89ee8ab 100644 --- a/syncano/models/billing.py +++ b/syncano/models/billing.py @@ -6,7 +6,7 @@ class Coupon(Model): """ - OO wrapper around coupons `endpoint `_. + OO wrapper around coupons `link `_. :ivar name: :class:`~syncano.models.fields.StringField` :ivar redeem_by: :class:`~syncano.models.fields.DateField` @@ -44,7 +44,7 @@ class Meta: class Discount(Model): """ - OO wrapper around discounts `endpoint `_. + OO wrapper around discounts `link `_. :ivar instance: :class:`~syncano.models.fields.ModelField` :ivar coupon: :class:`~syncano.models.fields.ModelField` diff --git a/syncano/models/channels.py b/syncano/models/channels.py index a28a701..3e8c003 100644 --- a/syncano/models/channels.py +++ b/syncano/models/channels.py @@ -66,7 +66,7 @@ class Channel(Model): """ .. _long polling: http://en.wikipedia.org/wiki/Push_technology#Long_polling - OO wrapper around channels `endpoint `_. + OO wrapper around channels `link `_. :ivar name: :class:`~syncano.models.fields.StringField` :ivar type: :class:`~syncano.models.fields.ChoiceField` @@ -157,7 +157,7 @@ def publish(self, payload, room=None): class Message(Model): """ - OO wrapper around channel hisotry `endpoint `_. + OO wrapper around channel hisotry `link `_. :ivar room: :class:`~syncano.models.fields.StringField` :ivar action: :class:`~syncano.models.fields.ChoiceField` diff --git a/syncano/models/classes.py b/syncano/models/classes.py index d3e869f..2aad942 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -14,7 +14,7 @@ class Class(Model): """ - OO wrapper around instance classes `endpoint `_. + OO wrapper around instance classes `link `_. :ivar name: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` @@ -30,6 +30,7 @@ class Class(Model): :ivar group: :class:`~syncano.models.fields.IntegerField` :ivar group_permissions: :class:`~syncano.models.fields.ChoiceField` :ivar other_permissions: :class:`~syncano.models.fields.ChoiceField` + :ivar objects: :class:`~syncano.models.fields.RelatedManagerField` .. note:: This model is special because each related :class:`~syncano.models.base.Object` will be @@ -84,7 +85,7 @@ def save(self, **kwargs): class Object(Model): """ - OO wrapper around data objects `endpoint `_. + OO wrapper around data objects `link `_. :ivar revision: :class:`~syncano.models.fields.IntegerField` :ivar created_at: :class:`~syncano.models.fields.DateTimeField` diff --git a/syncano/models/custom_response.py b/syncano/models/custom_response.py index e9fa71d..3881b6d 100644 --- a/syncano/models/custom_response.py +++ b/syncano/models/custom_response.py @@ -8,37 +8,38 @@ class CustomResponseHandler(object): A helper class which allows to define and maintain custom response handlers. Consider an example: - Script code: + Script code:: - >> set_response(HttpResponse(status_code=200, content='{"one": 1}', content_type='application/json')) + set_response(HttpResponse(status_code=200, content='{"one": 1}', content_type='application/json')) - When suitable ScriptTrace is used: + When suitable ScriptTrace is used:: - >> trace = ScriptTrace.please.get(id=, script=) + trace = ScriptTrace.please.get(id=, script=) Then trace object will have a content attribute, which will be a dict created from json (simple: json.loads under the hood); - So this is possible: + So this is possible:: - >> trace.content['one'] + trace.content['one'] - And the trace.content is equal to: - >> {'one': 1} + And the trace.content is equal to:: - The handler can be easily overwrite: + {'one': 1} - def custom_handler(response): - return json.loads(response['response']['content'])['one'] + The handler can be easily overwrite:: - trace.response_handler.overwrite_handler('application/json', custom_handler) + def custom_handler(response): + return json.loads(response['response']['content'])['one'] - or globally: + trace.response_handler.overwrite_handler('application/json', custom_handler) - ScriptTrace.response_handler.overwrite_handler('application/json', custom_handler) + or globally:: - Then trace.content is equal to: - >> 1 + ScriptTrace.response_handler.overwrite_handler('application/json', custom_handler) + + Then trace.content is equal to:: + 1 Currently supported content_types (but any handler can be defined): * application/json diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 05575d8..67410d8 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -12,7 +12,7 @@ class Script(Model): """ - OO wrapper around scripts `endpoint `_. + OO wrapper around scripts `link `_. :ivar label: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` @@ -104,7 +104,7 @@ def run(self, **payload): class Schedule(Model): """ - OO wrapper around script schedules `endpoint `_. + OO wrapper around script schedules `link `_. :ivar label: :class:`~syncano.models.fields.StringField` :ivar script: :class:`~syncano.models.fields.IntegerField` @@ -143,7 +143,7 @@ class Meta: class Trigger(Model): """ - OO wrapper around triggers `endpoint `_. + OO wrapper around triggers `link `_. :ivar label: :class:`~syncano.models.fields.StringField` :ivar script: :class:`~syncano.models.fields.IntegerField` @@ -185,9 +185,8 @@ class Meta: class ScriptEndpoint(Model): - # TODO: update docs when ready; """ - OO wrapper around script endpoints `endpoint `_. + OO wrapper around script endpoints `link `_. :ivar name: :class:`~syncano.models.fields.SlugField` :ivar script: :class:`~syncano.models.fields.IntegerField` diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 013c0d6..4c020fc 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -6,7 +6,7 @@ class Instance(Model): """ - OO wrapper around instances `endpoint `_. + OO wrapper around instances `link `_. :ivar name: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` @@ -16,6 +16,20 @@ class Instance(Model): :ivar metadata: :class:`~syncano.models.fields.JSONField` :ivar created_at: :class:`~syncano.models.fields.DateTimeField` :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + :ivar api_keys: :class:`~syncano.models.fields.RelatedManagerField` + :ivar users: :class:`~syncano.models.fields.RelatedManagerField` + :ivar admins: :class:`~syncano.models.fields.RelatedManagerField` + :ivar scripts: :class:`~syncano.models.fields.RelatedManagerField` + :ivar script_endpoints: :class:`~syncano.models.fields.RelatedManagerField` + :ivar templates: :class:`~syncano.models.fields.RelatedManagerField` + :ivar triggers: :class:`~syncano.models.fields.RelatedManagerField` + :ivar schedules: :class:`~syncano.models.fields.RelatedManagerField` + :ivar classes: :class:`~syncano.models.fields.RelatedManagerField` + :ivar invitations: :class:`~syncano.models.fields.RelatedManagerField` + :ivar gcm_devices: :class:`~syncano.models.fields.RelatedManagerField` + :ivar gcm_messages: :class:`~syncano.models.fields.RelatedManagerField` + :ivar apns_devices: :class:`~syncano.models.fields.RelatedManagerField` + :ivar apns_messages: :class:`~syncano.models.fields.RelatedManagerField` """ name = fields.StringField(max_length=64, primary_key=True) @@ -64,6 +78,7 @@ 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; """ @@ -77,7 +92,7 @@ def rename(self, new_name): class ApiKey(Model): """ - OO wrapper around instance api keys `endpoint `_. + OO wrapper around instance api keys `link `_. :ivar api_key: :class:`~syncano.models.fields.StringField` :ivar allow_user_create: :class:`~syncano.models.fields.BooleanField` @@ -108,8 +123,7 @@ class Meta: class InstanceInvitation(Model): """ - OO wrapper around instance invitations - `endpoint `_. + OO wrapper around instance invitations `link `_. :ivar email: :class:`~syncano.models.fields.EmailField` :ivar role: :class:`~syncano.models.fields.ChoiceField` diff --git a/syncano/models/manager.py b/syncano/models/manager.py index b24019c..7d2212d 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -131,14 +131,13 @@ def batch(self, *args): Usage:: klass = instance.classes.get(name='some_class') - Object.please.batch( klass.objects.as_batch().delete(id=652), klass.objects.as_batch().delete(id=653), ... ) - and:: + and:: Object.please.batch( klass.objects.as_batch().update(id=652, arg='some_b'), @@ -146,13 +145,15 @@ def batch(self, *args): ... ) - and:: + and:: + Object.please.batch( klass.objects.as_batch().create(arg='some_c'), klass.objects.as_batch().create(arg='some_c'), ... ) - and:: + + and:: Object.please.batch( klass.objects.as_batch().delete(id=653), @@ -161,28 +162,29 @@ def batch(self, *args): ... ) - are posible. + are posible. + + But:: - But:: Object.please.batch( klass.objects.as_batch().get_or_create(id=653, arg='some_a') ) - will not work as expected. + will not work as expected. - Some snippet for working with instance users: + Some snippet for working with instance users:: - instance = Instance.please.get(name='Nabuchodonozor') + instance = Instance.please.get(name='Nabuchodonozor') + model_users = instance.users.batch( + instance.users.as_batch().delete(id=7), + instance.users.as_batch().update(id=9, username='username_a'), + instance.users.as_batch().create(username='username_b', password='5432'), + ... + ) - model_users = instance.users.batch( - instance.users.as_batch().delete(id=7), - instance.users.as_batch().update(id=9, username='username_a'), - instance.users.as_batch().create(username='username_b', password='5432'), - ... - ) + And sample response will be:: - And sample response will be: - [{u'code': 204}, , , ...] + [{u'code': 204}, , , ...] :param args: a arg is on of the: klass.objects.as_batch().create(...), klass.objects.as_batch().update(...), klass.objects.as_batch().delete(...) @@ -258,14 +260,15 @@ def bulk_create(self, *objects): Creates many new instances based on provided list of objects. Usage:: - instance = Instance.please.get(name='instance_a') + instance = Instance.please.get(name='instance_a') instances = instance.users.bulk_create( User(username='user_a', password='1234'), User(username='user_b', password='4321') ) - .. warning:: + Warning:: + This method is restricted to handle 50 objects at once. """ return ModelBulkCreate(objects, self).process() @@ -904,9 +907,11 @@ def count(self): Return the queryset count; Usage:: + 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 count of the returned objects: count = DataObjects.please.list(...).count(); """ self.method = 'GET' @@ -920,12 +925,14 @@ def count(self): @clone def with_count(self, page_size=20): """ - Return the queryset count; + Return the queryset with 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(); """ @@ -981,12 +988,14 @@ def filter(self, **kwargs): def bulk_create(self, *objects): """ Creates many new objects. - Usage: - created_objects = Object.please.bulk_create( - Object(instance_name='instance_a', class_name='some_class', title='one'), - Object(instance_name='instance_a', class_name='some_class', title='two'), - Object(instance_name='instance_a', class_name='some_class', title='three') - ) + Usage:: + + created_objects = Object.please.bulk_create( + Object(instance_name='instance_a', class_name='some_class', title='one'), + Object(instance_name='instance_a', class_name='some_class', title='two'), + Object(instance_name='instance_a', class_name='some_class', title='three') + ) + :param objects: a list of the instances of data objects to be created; :return: a created and populated list of objects; When error occurs a plain dict is returned in that place; """ diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index 311c1b8..a3bba80 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -35,8 +35,7 @@ class GCMDevice(DeviceBase, Model): Usage:: - Create a new Device: - + Create a new Device: gcm_device = GCMDevice( label='example label', registration_id=86152312314401555, @@ -46,17 +45,15 @@ class GCMDevice(DeviceBase, Model): gcm_device.save() - Note:: - - another save on the same object will always fail (altering the Device data is currently not possible); - - Delete a Device: + Read: + gcm_device = GCMDevice.please.get(registration_id=86152312314401554) + Delete: gcm_device.delete() - Read a Device data: + .. note:: - gcm_device = GCMDevice.please.get(registration_id=86152312314401554) + another save on the same object will always fail (altering the Device data is currently not possible); """ @@ -81,7 +78,7 @@ class APNSDevice(DeviceBase, Model): Usage:: - Create + Create a new Device: apns_device = APNSDevice( label='example label', registration_id='4719084371920471208947120984731208947910827409128470912847120894', @@ -90,20 +87,22 @@ class APNSDevice(DeviceBase, Model): ) apns_device.save() - Note:: - - another save on the same object will always fail (altering the Device data is currently not possible); - - Also note the different format (from GCM) of registration_id required by APNS; the device_id have different - format too. - - Read + Read: apns_device = APNSDevice.please.get(registration_id='4719084371920471208947120984731208947910827409128470912847120894') - Delete + Delete: apns_device.delete() + .. note:: + + another save on the same object will always fail (altering the Device data is currently not possible); + + .. note:: + + Also note the different format (from GCM) of registration_id required by APNS; the device_id have different + format too. + """ class Meta: parent = Instance @@ -142,8 +141,7 @@ class GCMMessage(MessageBase, Model): Usage:: - Create - + Create a new Message: message = GCMMessage( content={ @@ -156,20 +154,24 @@ class GCMMessage(MessageBase, Model): ) message.save() - The data parameter is passed as-it-is to the GCM server; Base checking is made on syncano CORE; - For more details read the GCM documentation; - Note:: - Every save after initial one will raise an error; + Read: - Read gcm_message = GCMMessage.please.get(id=1) - Debugging: + Debugging: + gcm_message.status - on of the (scheduled, error, partially_delivered, delivered) gcm_message.result - a result from GCM server; - Note:: + + The data parameter is passed as-it-is to the GCM server; Base checking is made on syncano CORE; + For more details read the GCM documentation; + + .. note:: + Every save after initial one will raise an error; + + .. note:: The altering of existing Message is not possible. It also not possible to delete message. """ @@ -195,7 +197,7 @@ class APNSMessage(MessageBase, Model): Usage:: - Create + Create new Message: apns_message = APNSMessage( content={ 'registration_ids': [gcm_device.registration_id], @@ -205,19 +207,21 @@ class APNSMessage(MessageBase, Model): apns_message.save() - The 'aps' data is send 'as-it-is' to APNS, some validation is made on syncano CORE; - For more details read the APNS documentation; - - Note:: - Every save after initial one will raise an error; + Read: - Read apns_message = APNSMessage.please.get(id=1) - Debugging + Debugging: + apns_message.status - one of the following: scheduled, error, partially_delivered, delivered; apns_message.result - a result from APNS server; + The 'aps' data is send 'as-it-is' to APNS, some validation is made on syncano CORE; + For more details read the APNS documentation; + + .. note:: + Every save after initial one will raise an error; + """ class Meta: parent = Instance From 4579c1c43ef5ee78a419a1717ffe6725f8a279fa Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 17 Mar 2016 16:13:55 +0100 Subject: [PATCH 322/558] [LIB-598] correct flake issue --- syncano/models/instances.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 4c020fc..bac58aa 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -123,7 +123,8 @@ class Meta: class InstanceInvitation(Model): """ - OO wrapper around instance invitations `link `_. + OO wrapper around instance + invitations `link `_. :ivar email: :class:`~syncano.models.fields.EmailField` :ivar role: :class:`~syncano.models.fields.ChoiceField` From c204d022620289b35fd0b183d6047b1653f83803 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 17 Mar 2016 16:19:38 +0100 Subject: [PATCH 323/558] [LIB-598] correct isort issues; --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 228e9e5..f0ef361 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,6 +17,7 @@ from os.path import abspath, dirname import sphinx_rtd_theme +from syncano.models.fields import RelatedManagerField sys.path.insert(1, dirname(dirname(dirname(abspath(__file__))))) @@ -109,5 +110,4 @@ autodoc_member_order = 'bysource' highlight_language = 'python' -from syncano.models.fields import RelatedManagerField RelatedManagerField.__get__ = lambda self, *args, **kwargs: self From d319f77e7f8af5bfe37b8fb1157674c9a91db380 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 17 Mar 2016 16:56:47 +0100 Subject: [PATCH 324/558] [LIB-598] change profile field to be not read_only --- syncano/models/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 38346d0..4579143 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -100,7 +100,7 @@ class User(Model): password = fields.StringField(read_only=False, required=True) user_key = fields.StringField(read_only=True, required=False) - profile = fields.ModelField('Profile') + profile = fields.ModelField('Profile', read_only=False) links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) From 88de62c3aca86673921f9abaacf23107b14c3f21 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 17 Mar 2016 17:04:08 +0100 Subject: [PATCH 325/558] [LIB-598] change profile field: add default: empty dict --- syncano/models/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 4579143..082cccf 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -100,7 +100,7 @@ class User(Model): password = fields.StringField(read_only=False, required=True) user_key = fields.StringField(read_only=True, required=False) - profile = fields.ModelField('Profile', read_only=False) + profile = fields.ModelField('Profile', read_only=False, default={}) links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) From 49b35010d13a65e75ef6025f38eb37a185115d15 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 18 Mar 2016 11:25:29 +0100 Subject: [PATCH 326/558] [LIB-598] reload profile instead of user; --- syncano/models/channels.py | 4 ++-- tests/integration_test_user_profile.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/syncano/models/channels.py b/syncano/models/channels.py index 3e8c003..4a68e21 100644 --- a/syncano/models/channels.py +++ b/syncano/models/channels.py @@ -66,7 +66,7 @@ class Channel(Model): """ .. _long polling: http://en.wikipedia.org/wiki/Push_technology#Long_polling - OO wrapper around channels `link `_. + OO wrapper around channels `link http://docs.syncano.io/docs/realtime-communication`_. :ivar name: :class:`~syncano.models.fields.StringField` :ivar type: :class:`~syncano.models.fields.ChoiceField` @@ -157,7 +157,7 @@ def publish(self, payload, room=None): class Message(Model): """ - OO wrapper around channel hisotry `link `_. + OO wrapper around channel hisotry `link http://docs.syncano.io/docs/realtime-communication`_. :ivar room: :class:`~syncano.models.fields.StringField` :ivar action: :class:`~syncano.models.fields.ChoiceField` diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user_profile.py index 37a8def..d126a34 100644 --- a/tests/integration_test_user_profile.py +++ b/tests/integration_test_user_profile.py @@ -33,7 +33,7 @@ def test_profile_change_schema(self): ] klass.save() - self.user.reload() # force to refresh profile model; + self.user.profile.reload() # force to refresh profile model; self.user.profile.profile_pic = self.SAMPLE_PROFILE_PIC self.user.profile.save() From 4ca7cbda73813b35e4549d05a6c090a82ff719e6 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 18 Mar 2016 11:35:34 +0100 Subject: [PATCH 327/558] [LIB-598] remove assetEquals - deprecated --- tests/integration_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 713bacf..aa0dcc7 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -367,7 +367,7 @@ def test_source_run(self): sleep(1) trace.reload() - self.assertEquals(trace.status, 'success') + self.assertEqual(trace.status, 'success') self.assertDictEqual(trace.result, {'stderr': '', 'stdout': 'IntegrationTest'}) script.delete() @@ -386,7 +386,7 @@ def test_custom_response_run(self): sleep(1) trace.reload() - self.assertEquals(trace.status, 'success') + self.assertEqual(trace.status, 'success') self.assertDictEqual(trace.content, {'one': 1}) self.assertEqual(trace.content_type, 'application/json') self.assertEqual(trace.status_code, 200) @@ -447,7 +447,7 @@ def test_script_run(self): ) trace = script_endpoint.run() - self.assertEquals(trace.status, 'success') + self.assertEqual(trace.status, 'success') self.assertDictEqual(trace.result, {'stderr': '', 'stdout': 'IntegrationTest'}) script_endpoint.delete() From b380544103d9e508f1ca552d6abad504a98b6a93 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 18 Mar 2016 12:09:12 +0100 Subject: [PATCH 328/558] [LIB-598] return to old reload --- tests/integration_test_user_profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user_profile.py index d126a34..37a8def 100644 --- a/tests/integration_test_user_profile.py +++ b/tests/integration_test_user_profile.py @@ -33,7 +33,7 @@ def test_profile_change_schema(self): ] klass.save() - self.user.profile.reload() # force to refresh profile model; + self.user.reload() # force to refresh profile model; self.user.profile.profile_pic = self.SAMPLE_PROFILE_PIC self.user.profile.save() From 61fef06bd24cc91cfb2410ca9a7012d003aa327e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Sat, 19 Mar 2016 17:22:01 +0100 Subject: [PATCH 329/558] [LIB-598] small fixes in lib logic: store classes schema and registry used instance; --- syncano/__init__.py | 2 +- syncano/models/classes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 70d1097..9f3d36a 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -84,5 +84,5 @@ def connect(*args, **kwargs): instance = kwargs.get('instance_name', INSTANCE) if instance is not None: - registry.set_default_instance(instance) + registry.set_used_instance(instance) return registry diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 2aad942..63ddc42 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -79,7 +79,7 @@ class Meta: def save(self, **kwargs): if self.schema: # do not allow add empty schema to registry; - registry.set_schema(self.name, self.schema) # update the registry schema here; + registry.set_schema(self.name, self.schema.schema) # update the registry schema here; return super(Class, self).save(**kwargs) From 18accb91c5589d55a8c4aa9e6ccf671ddb1a518c Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Sat, 19 Mar 2016 17:38:03 +0100 Subject: [PATCH 330/558] [LIB-598] correct test; --- tests/test_connection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index ae27942..ded59b5 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -35,12 +35,13 @@ def test_connect(self, open_mock): @mock.patch('syncano.models.registry') @mock.patch('syncano.INSTANCE') def test_env_instance(self, instance_mock, registry_mock, *args): - self.assertFalse(registry_mock.set_default_instance.called) + instance_mock.return_value = 'test_instance' + self.assertFalse(registry_mock.set_used_instance.called) connect(1, 2, 3, a=1, b=2, c=3) - self.assertTrue(registry_mock.set_default_instance.called) - registry_mock.set_default_instance.assert_called_once_with(instance_mock) + self.assertTrue(registry_mock.set_used_instance.called) + registry_mock.set_used_instance.assert_called_once_with(instance_mock) class ConnectionTestCase(unittest.TestCase): From 0d23667a81bb761424edd24c47dc04c403328b6e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 21 Mar 2016 12:21:02 +0100 Subject: [PATCH 331/558] [LIB-600] add fields support: array and object; --- syncano/models/fields.py | 63 ++++++++++++++++++++++++++++++++++------ tests/test_fields.py | 43 +++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 1fbf021..606f9eb 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -12,6 +12,20 @@ from .registry import registry +class JSONToPythonMixin(object): + + def to_python(self, value): + if value is None: + return + + if isinstance(value, six.string_types): + try: + value = json.loads(value) + except (ValueError, TypeError): + raise SyncanoValueError('Invalid value: can not be parsed') + return value + + class Field(object): """Base class for all field types.""" @@ -508,7 +522,7 @@ def to_native(self, value): return {self.name: value} -class JSONField(WritableField): +class JSONField(JSONToPythonMixin, WritableField): query_allowed = False schema = None @@ -524,14 +538,6 @@ def validate(self, value, model_instance): except ValueError as e: raise self.ValidationError(e) - def to_python(self, value): - if value is None: - return - - if isinstance(value, six.string_types): - value = json.loads(value) - return value - def to_native(self, value): if value is None: return @@ -541,6 +547,41 @@ def to_native(self, value): return value +class ArrayField(JSONToPythonMixin, WritableField): + + def validate(self, value, model_instance): + super(ArrayField, self).validate(value, model_instance) + + if isinstance(value, six.string_types): + try: + value = json.loads(value) + except (ValueError, TypeError): + raise SyncanoValueError('Expected an array') + + if not isinstance(value, list): + raise SyncanoValueError('Expected an array') + + for element in value: + if not isinstance(element, six.string_types + (bool, int, float)): + raise SyncanoValueError( + 'Curently supported types for array items are: string types, bool, float and int') + + +class ObjectField(JSONToPythonMixin, WritableField): + + def validate(self, value, model_instance): + super(ObjectField, self).validate(value, model_instance) + + if isinstance(value, six.string_types): + try: + value = json.loads(value) + except (ValueError, TypeError): + raise SyncanoValueError('Expected an object') + + if not isinstance(value, dict): + raise SyncanoValueError('Expected an object') + + class SchemaField(JSONField): query_allowed = False not_indexable_types = ['text', 'file'] @@ -565,6 +606,8 @@ class SchemaField(JSONField): 'datetime', 'file', 'reference' + 'array', + 'object', ], }, 'order_index': { @@ -647,4 +690,6 @@ def to_native(self, value): 'model': ModelField, 'json': JSONField, 'schema': SchemaField, + 'array': ArrayField, + 'object': ObjectField, } diff --git a/tests/test_fields.py b/tests/test_fields.py index 090b83a..c4b9818 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -61,6 +61,8 @@ class AllFieldsModel(models.Model): model_field = models.ModelField('Instance') json_field = models.JSONField(schema=SCHEMA) schema_field = models.SchemaField() + array_field = models.ArrayField() + object_field = models.ObjectField() class Meta: endpoints = { @@ -528,3 +530,44 @@ def test_to_native(self): self.assertEqual(self.field.to_native(None), None) self.assertListEqual(json.loads(self.field.to_native(schema)), [{"type": "string", "name": "username"}]) self.assertListEqual(json.loads(self.field.to_native(value)), [{"type": "string", "name": "username"}]) + + +class ArrayFieldTestCase(BaseTestCase): + field_name = 'array_field' + + def test_validate(self): + + with self.assertRaises(SyncanoValueError): + self.field.validate("a", self.instance) + + with self.assertRaises(SyncanoValueError): + self.field.validate([1, 2, [12, 13]], self.instance) + + self.field.validate([1, 2, 3], self.instance) + self.field.validate("[1, 2, 3]", self.instance) + + def test_to_python(self): + with self.assertRaises(SyncanoValueError): + self.field.to_python('a') + + self.field.to_python([1, 2, 3, 4]) + self.field.to_python("[1, 2, 3, 4]") + + +class ObjectFieldTestCase(BaseTestCase): + field_name = 'object_field' + + def test_validate(self): + + with self.assertRaises(SyncanoValueError): + self.field.validate("a", self.instance) + + self.field.validate({'raz': 1, 'dwa': 2}, self.instance) + self.field.validate("{\"raz\": 1, \"dwa\": 2}", self.instance) + + def test_to_python(self): + with self.assertRaises(SyncanoValueError): + self.field.to_python('a') + + self.field.to_python({'raz': 1, 'dwa': 2}) + self.field.to_python("{\"raz\": 1, \"dwa\": 2}") From f0e9533a5f152f92ebcde80f8a31539b84bf0207 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 21 Mar 2016 12:27:09 +0100 Subject: [PATCH 332/558] [LIB-600] add missing coma --- syncano/models/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 606f9eb..1738bd2 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -605,7 +605,7 @@ class SchemaField(JSONField): 'boolean', 'datetime', 'file', - 'reference' + 'reference', 'array', 'object', ], From 0fea9f1dd6d49707d3fc57004aaae819ecb473ca Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Mon, 21 Mar 2016 14:31:54 +0100 Subject: [PATCH 333/558] [LIB-600] allow fields to be null when validating; --- syncano/models/fields.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 1738bd2..d6c51ef 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -552,6 +552,9 @@ class ArrayField(JSONToPythonMixin, WritableField): def validate(self, value, model_instance): super(ArrayField, self).validate(value, model_instance) + if not self.required and not value: + return + if isinstance(value, six.string_types): try: value = json.loads(value) @@ -572,6 +575,9 @@ class ObjectField(JSONToPythonMixin, WritableField): def validate(self, value, model_instance): super(ObjectField, self).validate(value, model_instance) + if not self.required and not value: + return + if isinstance(value, six.string_types): try: value = json.loads(value) From c0c1d112a97270b1e0155277059589ae641c4002 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 22 Mar 2016 10:54:06 +0100 Subject: [PATCH 334/558] [LIB-600] fixes after qa --- syncano/models/fields.py | 2 +- tests/test_fields.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index d6c51ef..7660631 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -567,7 +567,7 @@ def validate(self, value, model_instance): for element in value: if not isinstance(element, six.string_types + (bool, int, float)): raise SyncanoValueError( - 'Curently supported types for array items are: string types, bool, float and int') + 'Currently supported types for array items are: string types, bool, float and int') class ObjectField(JSONToPythonMixin, WritableField): diff --git a/tests/test_fields.py b/tests/test_fields.py index c4b9818..7a14dd7 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -563,11 +563,11 @@ def test_validate(self): self.field.validate("a", self.instance) self.field.validate({'raz': 1, 'dwa': 2}, self.instance) - self.field.validate("{\"raz\": 1, \"dwa\": 2}", self.instance) + self.field.validate('{"raz": 1, "dwa": 2}', self.instance) def test_to_python(self): with self.assertRaises(SyncanoValueError): self.field.to_python('a') self.field.to_python({'raz': 1, 'dwa': 2}) - self.field.to_python("{\"raz\": 1, \"dwa\": 2}") + self.field.to_python('{"raz": 1, "dwa": 2}') From 5532b52b57495c1e89e7a8c2fe263a51fd4134fc Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 22 Mar 2016 14:16:48 +0100 Subject: [PATCH 335/558] [FIXES] after docs examples run --- syncano/models/fields.py | 1 + syncano/models/incentives.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 7660631..c825fc2 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -432,6 +432,7 @@ def __getattribute__(self, item): try: return super(LinksWrapper, self).__getattribute__(item) except AttributeError: + item = item.replace('_', '-') if item not in self.links_dict or item in self.ignored_links: raise return self.links_dict[item] diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 67410d8..e36c9fc 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -261,7 +261,8 @@ def run(self, **payload): connection = self._get_connection(**payload) response = connection.request('POST', endpoint, **{'data': payload}) - if 'result' in response and 'stdout' in response['result']: + + if isinstance(response, dict) and 'result' in response and 'stdout' in response['result']: response.update({'instance_name': self.instance_name, 'script_name': self.name}) return ScriptEndpointTrace(**response) From 3787c0f044984923bde37be6f366b7c56a1d862c Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 22 Mar 2016 19:37:11 +0100 Subject: [PATCH 336/558] [DOCS] remove RelatedManger in docs config, remove from docs the empty available models section; correct comment in connect --- docs/source/conf.py | 3 +-- docs/source/index.rst | 1 - docs/source/models.rst | 10 ---------- syncano/__init__.py | 1 + 4 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 docs/source/models.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index f0ef361..4e2e169 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,7 +17,6 @@ from os.path import abspath, dirname import sphinx_rtd_theme -from syncano.models.fields import RelatedManagerField sys.path.insert(1, dirname(dirname(dirname(abspath(__file__))))) @@ -110,4 +109,4 @@ autodoc_member_order = 'bysource' highlight_language = 'python' -RelatedManagerField.__get__ = lambda self, *args, **kwargs: self +# RelatedManagerField.__get__ = lambda self, *args, **kwargs: self diff --git a/docs/source/index.rst b/docs/source/index.rst index c6ba193..e54ea4a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,7 +19,6 @@ Contents: getting_started interacting - models refs/syncano diff --git a/docs/source/models.rst b/docs/source/models.rst deleted file mode 100644 index 8e6217c..0000000 --- a/docs/source/models.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _models: - -================ -Available models -================ - - -.. automodule:: syncano.models.base - :members: - :noindex: diff --git a/syncano/__init__.py b/syncano/__init__.py index 9f3d36a..2caccb3 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -64,6 +64,7 @@ def connect(*args, **kwargs): :return: A models registry Usage:: + # Admin login connection = syncano.connect(email='', password='') # OR From 6056b649fdca5a61ce579ae522a540a4fa91bace Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 22 Mar 2016 19:43:21 +0100 Subject: [PATCH 337/558] [DOCS] make change with RelatedManagerField in docs conf --- docs/source/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 4e2e169..a753817 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,6 +19,8 @@ import sphinx_rtd_theme sys.path.insert(1, dirname(dirname(dirname(abspath(__file__))))) +from syncano.models.fields import RelatedManagerField + needs_sphinx = '1.0' extensions = [ @@ -109,4 +111,4 @@ autodoc_member_order = 'bysource' highlight_language = 'python' -# RelatedManagerField.__get__ = lambda self, *args, **kwargs: self +RelatedManagerField.__get__ = lambda self, *args, **kwargs: self From bf6bbe075ac6676ed8144f858298711706bdbc74 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 23 Mar 2016 10:05:54 +0100 Subject: [PATCH 338/558] [FIXES] add doc conf.py file to skip in isort --- .isort.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.isort.cfg b/.isort.cfg index 07d4742..4879d18 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -2,4 +2,4 @@ line_length=120 multi_line_output=3 default_section=THIRDPARTY -skip=base.py,.tox +skip=base.py,.tox,conf.py From 10150f33127238ee325c0a25c12bfb7555552640 Mon Sep 17 00:00:00 2001 From: Zhe Brah Date: Wed, 23 Mar 2016 10:41:03 +0100 Subject: [PATCH 339/558] [INFRA-260] version bump --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 9f3d36a..84e9cb8 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '4.2.0' +__version__ = '5.0.0' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From 7943a2929dd12077bb046f7ba6d9d3453eeaa6e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 31 Mar 2016 11:56:10 +0200 Subject: [PATCH 340/558] [LIB-624] add possibility to resend invitation; --- syncano/models/instances.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index bac58aa..23c434a 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -157,3 +157,13 @@ class Meta: 'path': '/invitations/', } } + + def resend(self): + """ + Resend the invitation. + :return: InstanceInvitation instance; + """ + resend_path = self.links.resend + connection = self._get_connection() + connection.request('POST', resend_path) # empty response here: 204 no content + return self From 7b4ee52d8a5e665f46326f497fa6db7dd867b150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 31 Mar 2016 15:06:56 +0200 Subject: [PATCH 341/558] [LIB-601] add possibility to increment and decrement objects integer and float fields; --- syncano/models/fields.py | 3 + syncano/models/manager.py | 14 +--- syncano/models/manager_mixins.py | 112 +++++++++++++++++++++++++++++++ tests/integration_test.py | 56 ++++++++++++++++ 4 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 syncano/models/manager_mixins.py diff --git a/syncano/models/fields.py b/syncano/models/fields.py index c825fc2..4cb89a9 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -39,6 +39,7 @@ class Field(object): has_endpoint_data = False query_allowed = True + allow_increment = False creation_counter = 0 @@ -220,6 +221,7 @@ def to_python(self, value): class IntegerField(WritableField): + allow_increment = True def to_python(self, value): value = super(IntegerField, self).to_python(value) @@ -245,6 +247,7 @@ def to_python(self, value): class FloatField(WritableField): + allow_increment = True def to_python(self, value): value = super(FloatField, self).to_python(value) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 7d2212d..08e6fd1 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -1,11 +1,11 @@ import json from copy import deepcopy -from functools import wraps import six from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValidationError, SyncanoValueError from syncano.models.bulk import ModelBulkCreate, ObjectBulkCreate +from syncano.models.manager_mixins import IncrementMixin, clone from .registry import registry @@ -13,16 +13,6 @@ REPR_OUTPUT_SIZE = 20 -def clone(func): - """Decorator which will ensure that we are working on copy of ``self``. - """ - @wraps(func) - def inner(self, *args, **kwargs): - self = self._clone() - return func(self, *args, **kwargs) - return inner - - class ManagerDescriptor(object): def __init__(self, manager): @@ -882,7 +872,7 @@ def run(self, *args, **kwargs): return registry.ScriptEndpointTrace(**response) -class ObjectManager(Manager): +class ObjectManager(IncrementMixin, Manager): """ Custom :class:`~syncano.models.manager.Manager` class for :class:`~syncano.models.base.Object` model. diff --git a/syncano/models/manager_mixins.py b/syncano/models/manager_mixins.py new file mode 100644 index 0000000..7f0e0e7 --- /dev/null +++ b/syncano/models/manager_mixins.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +from six import wraps +from syncano.exceptions import SyncanoValueError + + +def clone(func): + """Decorator which will ensure that we are working on copy of ``self``. + """ + @wraps(func) + def inner(self, *args, **kwargs): + self = self._clone() + return func(self, *args, **kwargs) + return inner + + +class IncrementMixin(object): + + @clone + def increment(self, field_name, value, **kwargs): + """ + A manager method which increments given field with given value. + + Usage:: + + data_object = Object.please.increment( + field_name='argA', + value=10, + class_name='testclass', + id=1715 + ) + + :param field_name: the field name to increment; + :param value: the increment value; + :param kwargs: class_name and id usually; + :return: the processed (incremented) data object; + """ + self.properties.update(kwargs) + model = self.model.get_subclass_model(**self.properties) + + self.validate(field_name, value, model) + + return self.process(field_name, value, **kwargs) + + @clone + def decrement(self, field_name, value, **kwargs): + """ + A manager method which decrements given field with given value. + + Usage:: + + data_object = Object.please.decrement( + field_name='argA', + value=10, + class_name='testclass', + id=1715 + ) + + :param field_name: the field name to decrement; + :param value: the decrement value; + :param kwargs: class_name and id usually; + :return: the processed (incremented) data object; + """ + self.properties.update(kwargs) + model = self.model.get_subclass_model(**self.properties) + + self.validate(field_name, value, model, operation_type='decrement') + + return self.process(field_name, value, operation_type='decrement', **kwargs) + + def process(self, field_name, value, operation_type='increment', **kwargs): + self.endpoint = 'detail' + self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') + self.data = kwargs.copy() + + if operation_type == 'increment': + increment_data = {'_increment': value} + elif operation_type == 'decrement': + increment_data = {'_increment': -value} + else: + raise SyncanoValueError('Operation not supported') + + self.data.update( + {field_name: increment_data} + ) + + response = self.request() + return response + + @classmethod + def validate(cls, field_name, value, model, operation_type='increment'): + if not isinstance(value, (int, float)): + raise SyncanoValueError('Provide an integer or float as a {} value.'.format(operation_type)) + + if not value >= 0: + raise SyncanoValueError('Value should be positive.') + + if not cls._check_field_type_for_increment(model, field_name): + raise SyncanoValueError('{} works only on integer and float fields.'.format(operation_type.capitalize())) + + @classmethod + def _check_field_type_for_increment(cls, model, field_name): + fields = {} + for field in model._meta.fields: + fields[field.name] = field.allow_increment + + if field_name not in fields: + raise SyncanoValueError('Object has not specified field.') + + if fields[field_name]: + return True + + return False diff --git a/tests/integration_test.py b/tests/integration_test.py index aa0dcc7..f766b25 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -309,6 +309,62 @@ def test_count_and_with_count(self): author_one.delete() author_two.delete() + def test_increment_and_decrement_on_integer(self): + author = self.model.please.create( + instance_name=self.instance.name, class_name=self.author.name, + first_name='john', last_name='doe') + + book = self.model.please.create( + instance_name=self.instance.name, class_name=self.book.name, + name='test', description='test', quantity=10, cost=10.5, + published_at=datetime.now(), author=author, available=True) + + incremented_book = Object.please.increment( + 'quantity', + 5, + id=book.id, + class_name=self.book.name, + ) + + self.assertEqual(incremented_book.quantity, 15) + + decremented_book = Object.please.decrement( + 'quantity', + 7, + id=book.id, + class_name=self.book.name, + ) + + self.assertEqual(decremented_book.quantity, 8) + + def test_increment_and_decrement_on_float(self): + author = self.model.please.create( + instance_name=self.instance.name, class_name=self.author.name, + first_name='john', last_name='doe') + + book = self.model.please.create( + instance_name=self.instance.name, class_name=self.book.name, + name='test', description='test', quantity=10, cost=10.5, + published_at=datetime.now(), author=author, available=True) + + incremented_book = Object.please.increment( + 'cost', + 5.5, + id=book.id, + class_name=self.book.name, + ) + + self.assertEqual(incremented_book.cost, 16) + + decremented_book = Object.please.decrement( + 'cost', + 7.6, + id=book.id, + class_name=self.book.name, + ) + + self.assertEqual(decremented_book.cost, 8.4) + class ScriptIntegrationTest(InstanceMixin, IntegrationTest): model = Script From f7b98ac496c231b6ae787e4e3b9176b28c227cf4 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 6 Apr 2016 11:29:30 +0200 Subject: [PATCH 342/558] [LIB-637] add rename mixin for the rename functionality, add this to the ResponseTemplate models; --- syncano/models/incentives.py | 11 ++++++++++- syncano/models/instances.py | 17 ++--------------- syncano/models/mixins.py | 18 ++++++++++++++++++ tests/integration_test_reponse_templates.py | 10 ++++++++++ 4 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 syncano/models/mixins.py diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index e36c9fc..6beeb4c 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -8,6 +8,7 @@ from .base import Model from .instances import Instance from .manager import ScriptEndpointManager, ScriptManager +from .mixins import RenameMixin class Script(Model): @@ -284,7 +285,7 @@ def reset_link(self): self.public_link = response['public_link'] -class ResponseTemplate(Model): +class ResponseTemplate(RenameMixin, Model): """ OO wrapper around templates. @@ -330,3 +331,11 @@ def render(self, context=None): connection = self._get_connection() return connection.request('POST', endpoint, data={'context': context}) + + def rename(self, new_name): + rename_path = self.links.rename + data = {'new_name': new_name} + connection = self._get_connection() + response = connection.request('POST', rename_path, data=data) + self.to_python(response) + return self diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 23c434a..66f49f6 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -2,9 +2,10 @@ from . import fields from .base import Model +from .mixins import RenameMixin -class Instance(Model): +class Instance(RenameMixin, Model): """ OO wrapper around instances `link `_. @@ -75,20 +76,6 @@ 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.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): """ diff --git a/syncano/models/mixins.py b/syncano/models/mixins.py new file mode 100644 index 0000000..8c2d016 --- /dev/null +++ b/syncano/models/mixins.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + + +class RenameMixin(object): + + def rename(self, new_name): + """ + A method for changing the name of the object; Corresponds to the Mixin in CORE; + + :param new_name: the new name for the object; + :return: a populated object; + """ + rename_path = self.links.rename + data = {'new_name': new_name} + connection = self._get_connection() + response = connection.request('POST', rename_path, data=data) + self.to_python(response) + return self diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index a145c95..6816cb8 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -49,3 +49,13 @@ def test_render_api(self): rendered = render_template.render(context={'objects': [3]}) self.assertEqual(rendered, '
  • 3
  • ') + + def test_rename(self): + name = 'some_old_new_name_for_template' + new_name = 'some_new_name_for_template' + + template = ResponseTemplate.please.create(name=name, content='
    ', content_type='text/html', + context={'two': 2}) + template = template.rename(new_name=new_name) + + self.assertEqual(template.name, new_name) From be3c7b6bcf0b0a629e964685b657a81fa1f7a1e5 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 7 Apr 2016 09:35:14 +0200 Subject: [PATCH 343/558] [RELEASE] bump the version --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index ec7d973..b5741f6 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.0.0' +__version__ = '5.0.1' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From 76ba552e4c34ab4cbe3c75609f24807bd71a9c0e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 7 Apr 2016 10:01:38 +0200 Subject: [PATCH 344/558] [GITIGNORE] add .python_version to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a59911a..cc1d3f5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ run_it.py syncano.egg-info .tox/* test +.python-version From ac12d4e180e61b8df154a5a34e59d5e51c98f080 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 7 Apr 2016 13:39:31 +0200 Subject: [PATCH 345/558] [LIB-507] correct docs a little: remove connect_instance, and correct some example calss; --- docs/source/getting_started.rst | 11 ++++++----- docs/source/interacting.rst | 4 +--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 3eed7eb..823b1b7 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -72,16 +72,17 @@ Making Connections >>> import syncano >>> connection = syncano.connect(email='YOUR_EMAIL', password='YOUR_PASSWORD') -If you want to connect directly to chosen instance you can use :func:`~syncano.connect_instance` function:: +If you want to use instance in connection you can use :func:`~syncano.connect` function, +then you can omit the instance_name in other calls:: >>> import syncano - >>> connection = syncano.connect_instance('instance_name', email='YOUR_EMAIL', password='YOUR_PASSWORD') + >>> connection = syncano.connect(instance_name='instance_name', email='YOUR_EMAIL', password='YOUR_PASSWORD') If you have obtained your ``Account Key`` from the website you can omit ``email`` & ``password`` and pass ``Account Key`` directly to connection: >>> import syncano >>> connection = syncano.connect(api_key='YOUR_API_KEY') - >>> connection = syncano.connect_instance('instance_name', api_key='YOUR_API_KEY') + >>> connection = syncano.connect(instance_name='instance_name', api_key='YOUR_API_KEY') Troubleshooting Connections @@ -127,8 +128,8 @@ Each model has a different set of fields and commands. For more information chec Next Steps ---------- -If you'd like more information on interacting with Syncano, check out the :ref:`interacting tutorial` or if you -want to know what kind of models are available check out the :ref:`available models ` list. +If you'd like more information on interacting with Syncano, check out the :ref:`interacting tutorial` +or if you want to know what kind of models are available check out the :ref:`available models ` list. diff --git a/docs/source/interacting.rst b/docs/source/interacting.rst index b77f2f4..aab3e4f 100644 --- a/docs/source/interacting.rst +++ b/docs/source/interacting.rst @@ -206,8 +206,6 @@ to :meth:`~syncano.models.manager.Manager.list` method:: >>> ApiKey.please.list(instance_name='test-one') [...] - >>> ApiKey.please.list('test-one') - [...] This performs a **GET** request to ``/v1/instances/test-one/api_keys/``. @@ -226,7 +224,7 @@ all :class:`~syncano.models.base.Instance` objects will have backward relation t >>> instance = Instance.please.get('test-one') >>> instance.api_keys.list() [...] - >>> instance.api_keys.get(1) + >>> instance.api_keys.get(id=1) .. note:: From 6636691e410a203dce7e0a7325fece49e5e413f9 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 7 Apr 2016 14:04:39 +0200 Subject: [PATCH 346/558] [LIB-363] add timezone field for Schedule model; --- syncano/models/incentives.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 6beeb4c..c48b648 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -122,6 +122,7 @@ class Schedule(Model): interval_sec = fields.IntegerField(read_only=False, required=False) crontab = fields.StringField(max_length=40, required=False) payload = fields.StringField(required=False) + timezone = fields.StringField(required=False) created_at = fields.DateTimeField(read_only=True, required=False) scheduled_next = fields.DateTimeField(read_only=True, required=False) links = fields.LinksField() From b4ee89d92eee248964ae1ee0eb6157c30fe43341 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 14 Apr 2016 14:36:53 +0200 Subject: [PATCH 347/558] [LIB-646] add geo point support to LIB; --- syncano/connection.py | 8 ++++- syncano/models/fields.py | 63 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index abef86d..a7161f1 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -233,7 +233,13 @@ def make_request(self, method_name, path, **kwargs): :raises SyncanoRequestError: if something went wrong during the request """ data = kwargs.get('data', {}) - files = data.pop('files', None) + files = data.pop('files', {}) + + if 'requests' in data: # batch requests + for request in data['requests']: + per_request_files = request['body'].pop('files', {}) + if per_request_files: + raise SyncanoValueError('Batch do not support files upload.') if files is None: files = {k: v for k, v in six.iteritems(data) if hasattr(v, 'read')} diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 4cb89a9..5cb39b4 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -1,5 +1,6 @@ import json import re +from collections import namedtuple from datetime import date, datetime import six @@ -618,6 +619,7 @@ class SchemaField(JSONField): 'reference', 'array', 'object', + 'geopoint', ], }, 'order_index': { @@ -679,6 +681,66 @@ def to_native(self, value): return value +GeoPointStruct = namedtuple('GeoPointHelper', ['longitude', 'latitude']) + + +class GeoPoint(Field): + + def validate(self, value, model_instance): + super(GeoPoint, self).validate(value, model_instance) + + if not self.required and not value: + return + + if isinstance(value, six.string_types): + try: + value = json.loads(value) + except (ValueError, TypeError): + raise SyncanoValueError('Expected an object') + + if not isinstance(value, GeoPointStruct): + raise SyncanoValueError('Expected an GeoPointStruct') + + def to_native(self, value): + if value is None: + return + + geo_struct = {'longitude': value[0], 'latitude': value[1]} + + if not isinstance(value, six.string_types): + geo_struct = json.dumps(geo_struct) + + return geo_struct + + def to_python(self, value): + if value is None: + return + + if isinstance(value, six.string_types): + try: + value = json.loads(value) + except (ValueError, TypeError): + raise SyncanoValueError('Invalid value: can not be parsed.') + + longitude = None + latitude = None + + if isinstance(value, dict): + longitude = value.get('longitude') + latitude = value.get('latitude') + elif isinstance(value, (tuple, list)): + try: + longitude = value[0] + latitude = value[1] + except IndexError: + raise SyncanoValueError('Can not parse the geo struct.') + + if not longitude or not latitude: + raise SyncanoValueError('Expected the `longitude` and `latitude` fields.') + + return GeoPointStruct(longitude, latitude) + + MAPPING = { 'string': StringField, 'text': StringField, @@ -702,4 +764,5 @@ def to_native(self, value): 'schema': SchemaField, 'array': ArrayField, 'object': ObjectField, + 'geopoint': GeoPoint, } From e09ebaa432df8b7cfd561ddeeb1939fab5620c83 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 14 Apr 2016 14:54:19 +0200 Subject: [PATCH 348/558] [LIB-646] simplify the make_request a little --- syncano/connection.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index a7161f1..aebed4b 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -235,11 +235,7 @@ def make_request(self, method_name, path, **kwargs): data = kwargs.get('data', {}) files = data.pop('files', {}) - if 'requests' in data: # batch requests - for request in data['requests']: - per_request_files = request['body'].pop('files', {}) - if per_request_files: - raise SyncanoValueError('Batch do not support files upload.') + self._check_batch_files(data) if files is None: files = {k: v for k, v in six.iteritems(data) if hasattr(v, 'read')} @@ -408,6 +404,14 @@ def get_user_info(self, api_key=None, user_key=None): return self.make_request('GET', self.USER_INFO_SUFFIX.format(name=self.instance_name), headers={ 'X-API-KEY': self.api_key, 'X-USER-KEY': self.user_key}) + @classmethod + def _check_batch_files(cls, data): + if 'requests' in data: # batch requests + for request in data['requests']: + per_request_files = request['body'].pop('files', {}) + if per_request_files: + raise SyncanoValueError('Batch do not support files upload.') + class ConnectionMixin(object): """Injects connection attribute with support of basic validation.""" From a2bd74a08a6fd6307e30706f923cc6accdd07f1b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 14 Apr 2016 15:05:28 +0200 Subject: [PATCH 349/558] [LIB-646] change the default param in get --- syncano/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index aebed4b..82393bd 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -233,7 +233,7 @@ def make_request(self, method_name, path, **kwargs): :raises SyncanoRequestError: if something went wrong during the request """ data = kwargs.get('data', {}) - files = data.pop('files', {}) + files = data.pop('files', None) self._check_batch_files(data) From 92af564161a4d0124a8dbdb4835c134d22612f92 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 14 Apr 2016 15:13:16 +0200 Subject: [PATCH 350/558] [LIB-646] correct check for batch requests --- syncano/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 82393bd..9fb30d2 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -408,7 +408,7 @@ def get_user_info(self, api_key=None, user_key=None): def _check_batch_files(cls, data): if 'requests' in data: # batch requests for request in data['requests']: - per_request_files = request['body'].pop('files', {}) + per_request_files = request.pop('body', {}).pop('files', {}) if per_request_files: raise SyncanoValueError('Batch do not support files upload.') From a1acf37d63bbfa179a9ad5aef1279143f6b729f8 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 14 Apr 2016 15:18:47 +0200 Subject: [PATCH 351/558] [LIB-646] correct check for batch requests --- syncano/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/connection.py b/syncano/connection.py index 9fb30d2..d42ace4 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -408,7 +408,7 @@ def get_user_info(self, api_key=None, user_key=None): def _check_batch_files(cls, data): if 'requests' in data: # batch requests for request in data['requests']: - per_request_files = request.pop('body', {}).pop('files', {}) + per_request_files = request.get('body', {}).get('files', {}) if per_request_files: raise SyncanoValueError('Batch do not support files upload.') From dac69d5ba98bb35bef3dea610a47a9eaad9aa991 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 15 Apr 2016 16:07:26 +0200 Subject: [PATCH 352/558] [LIB-646] small fixes in GeoPoint definition --- syncano/models/fields.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 5cb39b4..b9b13fb 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -674,14 +674,15 @@ def to_native(self, value): return if not isinstance(value, six.string_types): - value.update({ - 'environment': PUSH_ENV, - }) + if 'environment' not in value: + value.update({ + 'environment': PUSH_ENV, + }) value = json.dumps(value) return value -GeoPointStruct = namedtuple('GeoPointHelper', ['longitude', 'latitude']) +GeoPointStruct = namedtuple('GeoPointHelper', ['latitude', 'longitude']) class GeoPoint(Field): @@ -705,10 +706,8 @@ def to_native(self, value): if value is None: return - geo_struct = {'longitude': value[0], 'latitude': value[1]} - - if not isinstance(value, six.string_types): - geo_struct = json.dumps(geo_struct) + geo_struct = {'latitude': value[0], 'longitude': value[1]} + geo_struct = json.dumps(geo_struct) return geo_struct @@ -726,19 +725,19 @@ def to_python(self, value): latitude = None if isinstance(value, dict): - longitude = value.get('longitude') latitude = value.get('latitude') + longitude = value.get('longitude') elif isinstance(value, (tuple, list)): try: - longitude = value[0] - latitude = value[1] + latitude = value[0] + longitude = value[1] except IndexError: raise SyncanoValueError('Can not parse the geo struct.') if not longitude or not latitude: raise SyncanoValueError('Expected the `longitude` and `latitude` fields.') - return GeoPointStruct(longitude, latitude) + return GeoPointStruct(latitude, longitude) MAPPING = { From c3af5ce9c37db52588c8741a1eccd77cec6b77e4 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Apr 2016 10:49:17 +0200 Subject: [PATCH 353/558] [LIB-548] re-work a little template on another endpoint handling in LIB, add more tests; --- syncano/models/manager.py | 6 ++++-- tests/integration_test_reponse_templates.py | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 08e6fd1..0a2f867 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -727,7 +727,6 @@ def _clone(self): def serialize(self, data, model=None): """Serializes passed data to related :class:`~syncano.models.base.Model` class.""" model = model or self.model - if data == '': return @@ -777,7 +776,7 @@ def request(self, method=None, path=None, **request): raise self.model.DoesNotExist("{} not found.".format(obj_id)) raise - if 'next' not in response: + if 'next' not in response and not self._template: return self.serialize(response) return response @@ -799,6 +798,9 @@ def iterator(self): response = self._get_response() results = 0 while True: + if self._template: + yield response + break objects = response.get('objects') next_url = response.get('next') diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index 6816cb8..407d2fa 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from syncano.models import ResponseTemplate +from syncano.models import Class, ResponseTemplate from tests.integration_test import InstanceMixin, IntegrationTest @@ -59,3 +59,17 @@ def test_rename(self): template = template.rename(new_name=new_name) self.assertEqual(template.name, new_name) + + def test_render_on_endpoint_list(self): + template_response = Class.please.template('objects_html_table').all() + + self.assertIn('', template_response) + self.assertIn('user_profile', template_response) + self.assertTrue(isinstance(template_response, basestring)) + + def test_render_on_endpoint_one_elem(self): + template_response = Class.please.template('objects_html_table').get(name='user_profile') + + self.assertIn('
    ', template_response) + self.assertIn('user_profile', template_response) + self.assertTrue(isinstance(template_response, basestring)) From 004c5a80d218b3a2e4751ed8a83e2f5d838dc4a8 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Apr 2016 10:59:24 +0200 Subject: [PATCH 354/558] [LIB-548] correct test assert --- tests/integration_test_reponse_templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index 407d2fa..323fc9c 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -63,8 +63,8 @@ def test_rename(self): def test_render_on_endpoint_list(self): template_response = Class.please.template('objects_html_table').all() - self.assertIn('
    ', template_response) - self.assertIn('user_profile', template_response) + self.assertIn('
    ', template_response[0]) # all() returns a list (precise: iterator) + self.assertIn('user_profile', template_response[0]) self.assertTrue(isinstance(template_response, basestring)) def test_render_on_endpoint_one_elem(self): From b328d1e0c8005ed5eb9ca1c823dacf9af4bbe3ec Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 19 Apr 2016 11:09:35 +0200 Subject: [PATCH 355/558] [LIB-548] remove type check from test; --- tests/integration_test_reponse_templates.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py index 323fc9c..51045d1 100644 --- a/tests/integration_test_reponse_templates.py +++ b/tests/integration_test_reponse_templates.py @@ -65,11 +65,9 @@ def test_render_on_endpoint_list(self): self.assertIn('
    ', template_response[0]) # all() returns a list (precise: iterator) self.assertIn('user_profile', template_response[0]) - self.assertTrue(isinstance(template_response, basestring)) def test_render_on_endpoint_one_elem(self): template_response = Class.please.template('objects_html_table').get(name='user_profile') self.assertIn('
    ', template_response) self.assertIn('user_profile', template_response) - self.assertTrue(isinstance(template_response, basestring)) From 0d77738a7c7b8fc6378d089adcbf224e1200267a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 11:29:08 +0200 Subject: [PATCH 356/558] [LIB-660] Add possibility to edit Device, add send_message method; --- syncano/__init__.py | 2 +- syncano/models/fields.py | 18 +++++++++----- syncano/models/push_notification.py | 38 ++++++++++++++++++++--------- tests/integration_test_push.py | 8 ++++++ 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index b5741f6..d728cf6 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.0.1' +__version__ = '5.0.2' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 4cb89a9..d3bbaea 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -435,10 +435,15 @@ def __getattribute__(self, item): try: return super(LinksWrapper, self).__getattribute__(item) except AttributeError: - item = item.replace('_', '-') - if item not in self.links_dict or item in self.ignored_links: + value = self.links_dict.get(item) + if not value: + item = item.replace('_', '-') + value = self.links_dict.get(item) + + if not value: raise - return self.links_dict[item] + + return value def to_native(self): return self.links_dict @@ -672,9 +677,10 @@ def to_native(self, value): return if not isinstance(value, six.string_types): - value.update({ - 'environment': PUSH_ENV, - }) + if 'environment' not in value: + value.update({ + 'environment': PUSH_ENV, + }) value = json.dumps(value) return value diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index a3bba80..e5de3fc 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -18,14 +18,28 @@ class DeviceBase(object): label = fields.StringField(max_length=80) user = fields.IntegerField(required=False) + links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) class Meta: abstract = True - def is_new(self): - return self.created_at is None + def send_message(self, content): + """ + A method which allows to send message directly to the device; + :param contet: Message content structure - object like; + :return: + """ + print(self.links.links_dict) + send_message_path = self.links.send_message + data = { + 'content': content + } + connection = self._get_connection() + response = connection.request('POST', send_message_path, data=data) + self.to_python(response) + return self class GCMDevice(DeviceBase, Model): @@ -51,9 +65,9 @@ class GCMDevice(DeviceBase, Model): Delete: gcm_device.delete() - .. note:: - - another save on the same object will always fail (altering the Device data is currently not possible); + Update: + gcm_device.label = 'some new label' + gcm_device.save() """ @@ -61,11 +75,11 @@ class Meta: parent = Instance endpoints = { 'detail': { - 'methods': ['delete', 'get'], + 'methods': ['delete', 'get', 'put', 'patch'], 'path': '/push_notifications/gcm/devices/{registration_id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['post', 'get'], 'path': '/push_notifications/gcm/devices/', } } @@ -94,9 +108,9 @@ class APNSDevice(DeviceBase, Model): Delete: apns_device.delete() - .. note:: - - another save on the same object will always fail (altering the Device data is currently not possible); + Update: + apns_device.label = 'some new label' + apns_device.save() .. note:: @@ -108,11 +122,11 @@ class Meta: parent = Instance endpoints = { 'detail': { - 'methods': ['delete', 'get'], + 'methods': ['delete', 'get', 'put', 'patch'], 'path': '/push_notifications/apns/devices/{registration_id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['post', 'get'], 'path': '/push_notifications/apns/devices/', } } diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py index a6d67ff..839c4fb 100644 --- a/tests/integration_test_push.py +++ b/tests/integration_test_push.py @@ -63,6 +63,14 @@ def _test_device(self, device, manager): self.assertEqual(device_.registration_id, device.registration_id) self.assertEqual(device_.device_id, device.device_id) + # test update: + new_label = 'totally new label' + device.label = new_label + device.save() + + device_ = manager.get(instance_name=self.instance.name, registration_id=device.registration_id) + self.assertEqual(new_label, device_.label) + device.delete() self.assertFalse(manager.all(instance_name=self.instance.name)) From efa19b7feca9473f3b9fa4986b1b5c38eb345735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 11:41:32 +0200 Subject: [PATCH 357/558] [LIB-660] correct test a litle --- tests/test_push.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_push.py b/tests/test_push.py index e71b974..2b8f812 100644 --- a/tests/test_push.py +++ b/tests/test_push.py @@ -35,7 +35,7 @@ def test_gcm_device(self, connection_mock): data={"registration_id": '86152312314401555', "device_id": "10000000001", "is_active": True, "label": "example label"} ) - model.created_at = datetime.now() # to Falsify is_new() + model.links = 'something' # to Falsify is_new() model.delete() connection_mock.request.assert_called_with( 'DELETE', '/v1.1/instances/test/push_notifications/gcm/devices/86152312314401555/' @@ -69,7 +69,7 @@ def test_apns_device(self, connection_mock): "label": "example label"} ) - model.created_at = datetime.now() # to Falsify is_new() + model.links = 'something' # to Falsify is_new() model.delete() connection_mock.request.assert_called_with( 'DELETE', '/v1.1/instances/test/push_notifications/apns/devices/86152312314401555/' From 875a519c4b7a0ec301aaaf4b2adfd3f925321866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 11:48:15 +0200 Subject: [PATCH 358/558] [LIB-660] correct test a litle --- tests/test_push.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_push.py b/tests/test_push.py index 2b8f812..598bb9c 100644 --- a/tests/test_push.py +++ b/tests/test_push.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import json import unittest -from datetime import datetime from mock import mock from syncano.models import APNSDevice, APNSMessage, GCMDevice, GCMMessage From 0fd2b35a981a6e1ed2464a818ea5c6797cfd384c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 13:32:45 +0200 Subject: [PATCH 359/558] [LIB-660] add config for apns and gcm - correct test; lets build it :) --- syncano/connection.py | 16 +++- syncano/models/push_notification.py | 76 +++++++++++++++++ tests/ceritificates/ApplePushDevelopment.p12 | Bin 0 -> 3209 bytes tests/integration_test_push.py | 83 ++++++++++++++++++- 4 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 tests/ceritificates/ApplePushDevelopment.p12 diff --git a/syncano/connection.py b/syncano/connection.py index abef86d..577fe0a 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -271,7 +271,7 @@ def make_request(self, method_name, path, **kwargs): # remove 'data' and 'content-type' to avoid "ValueError: Data must not be a string." params.pop('data') params['headers'].pop('content-type') - params['files'] = files + params['files'] = self._process_apns_cert_files(files) if response.status_code == 201: url = '{}{}/'.format(url, content['id']) @@ -402,6 +402,20 @@ def get_user_info(self, api_key=None, user_key=None): return self.make_request('GET', self.USER_INFO_SUFFIX.format(name=self.instance_name), headers={ 'X-API-KEY': self.api_key, 'X-USER-KEY': self.user_key}) + def _process_apns_cert_files(self, files): + files = files.copy() + for key in files.keys(): + # remove certificates files (which are bool - True if cert exist, False otherwise) + value = files[key] + if isinstance(value, bool): + files.pop(key) + continue + + if key in ['production_certificate', 'development_certificate']: + value = (value.name, value, 'application/x-pkcs12', {'Expires': '0'}) + files[key] = value + return files + class ConnectionMixin(object): """Injects connection attribute with support of basic validation.""" diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index e5de3fc..95fb080 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -249,3 +249,79 @@ class Meta: 'path': '/push_notifications/apns/messages/', } } + + +class GCMConfig(Model): + """ + A model which stores information with GCM Push keys; + + Usage:: + + Add (modify) new keys: + gcm_config = GCMConfig(production_api_key='ccc', development_api_key='ddd') + gcm_config.save() + + or: + gcm_config = GCMConfig().please.get() + gcm_config.production_api_key = 'ccc' + gcm_config.development_api_key = 'ddd' + gcm_config.save() + + """ + production_api_key = fields.StringField(required=False) + development_api_key = fields.StringField(required=False) + + def is_new(self): + return False # this is predefined - never will be new + + class Meta: + parent = Instance + endpoints = { + 'list': { + 'methods': ['get', 'put'], + 'path': '/push_notifications/gcm/config/', + }, + 'detail': { + 'methods': ['get', 'put'], + 'path': '/push_notifications/gcm/config/', + }, + } + + +class APNSConfig(Model): + """ + A model which stores information with APNS Push certificates; + + Usage:: + + Add (modify) new keys: + cert_file = open('cert_file.p12', 'r') + apns_config = APNSConfig(development_certificate=cert_file) + apns_config.save() + cert_file.close() + + """ + production_certificate_name = fields.StringField(required=False) + production_certificate = fields.FileField(required=False) + production_bundle_identifier = fields.StringField(required=False) + production_expiration_date = fields.DateField(read_only=True) + development_certificate_name = fields.StringField(required=False) + development_certificate = fields.FileField(required=False) + development_bundle_identifier = fields.StringField(required=False) + development_expiration_date = fields.DateField(read_only=True) + + def is_new(self): + return False # this is predefined - never will be new + + class Meta: + parent = Instance + endpoints = { + 'list': { + 'methods': ['get', 'put'], + 'path': '/push_notifications/apns/config/', + }, + 'detail': { + 'methods': ['get', 'put'], + 'path': '/push_notifications/apns/config/', + }, + } diff --git a/tests/ceritificates/ApplePushDevelopment.p12 b/tests/ceritificates/ApplePushDevelopment.p12 new file mode 100644 index 0000000000000000000000000000000000000000..a8a0aa84ab2cfec601b67cd78edc9ea456ec8225 GIT binary patch literal 3209 zcmY+FX*3j!+s4N*7{=IllO=#$rh2F$k+!(8ewdW zLPd5$md2W;f6qDp_kG^?!+q{^e%HC~kM|FOh2ViSvw)0s~>y7Lc>5)goUWn zVIiu2u`&Wn-|}xnUxcNv_={cwX#js$!M_O&LWrK}zZW1h^av;&BU86X^4bm2J|K`0 z5RIk3|DGY&_}6t$4FVY+w9WqP(j|cTytJ5K*g`R@X-&-HOh8}-KFcSm^b&|59h4>G zQ0B?NOBj<5_@S7S<+!6>;H0jb10&t#HZwtw8k#jVUv+8O1EKeub_olItFTP0`ETga z&s}@f@h|-mUz@~x3(u^GwIte@SC4!>`6NgyOxy@p;4GFy_}KpWB-BNDtfDtD^#Bzp{L{miZ(Z`II~)WM=h} zVNDq57x`_$K)#=yhk*|7$xTtNhjn2zxY^-9gkCFIso)FG z_d?S?$uP%-ypKP=S53qSUAdDcA3rk2m)tv*-coaK-f~8FWKs9vV!-1mI~BK%+W4Eo zE#5Qvh|eCt!^k6w+_+{M$)2G0ap8j`_=y`88TV`1jIb>PHZ*%^e3$)>me8833Q&SupS0bRytU?Qu!TT1&zUqXG1h&5$vl zPR{U}a%-buZYo-v6A+pEg^_&uKzyKPMi7pFXuQ+jf6AjV**3BJ=5a`5L|fduD+-vR zIp$iVR@Vd8J|dzH&uXZ&HBnN|Zbm&;j=40J?bRT+)Q!0-^|PY_PO|H^hNu#ZkN~kc z_i>vc$MX249DZF+x>?g%!fM+Ct~d*m?1v~8thd_4b|mG6#cDv*me+aWA0P@ENH3Qt zG2Mmp*H^0Pkc0c*X7Kr>|69O!U@qguyAj?xIPhzSbO@isx5WAyA{`3#MNYSEV4|Pv z=P3^vZjgg(y6M7AW+zR8cvU6L4 zCX|Vm>hMfSxoWSup%~72s%L0=rMuA1;H?u351PtTGt{5VRNeU{W0=Imu)*~5`xY9T z9JM2^K(i-FvDS-u{`l)A@3dlw)tk;+H{m{k<6g8EP;N~Qv(*etdR*-ouV?FN!5L@D z$CcYcYzpr%N@4r?g(pG-o%Ow+xi`6g#6)zEZG_D0p=IX&wvFflcDzlvsa9vP3L0Hf z4Q_oMkX}s$?@0(iy44h9ZV&LG^E9vg3Km&#K&(#*o;xh%wT5XX10z zUXM{BDf5E5#P@0O7^%Z|Gu1UQ^HOru`%s3tBnB)e2k~AHuRxW1N8f!E>Q-pIXql&F zX+uoYB-1p{`0iXOeEh`T4G(3zQB9lh8_OyGqQJL?)xqHA6{`#quqD^)o@<`!SgW*n zHdbdkkOctdkj6Ef!zJ9w;cA(V?N$#mdtL<0g%4BrU9@wTRMYXdT+~XlJ%c0YGq-f) z$7AeFJxLpvE;fF1U{$|ABHV+!2;$=2@?PMb#BHGI^8z0v+3Oh`41or;Ok5a!pF+9n z+xpW)xU(J+Ci;0%JtKjtY^VPQ#8M;uVHLfD7X$#$st(BFW0C0^b*)Rtjk6oC)L5Ph zPHLq(*Dr*_oLkvtDGA-U@A#y(?_YfTIy7IugE_E;P zxmQGSHOzQJH!PYkDx|CSBWmBFIxC#l3B7FywU~WJsz+1*X^Kui-Bou1uhVNb zpWg|zoli!fF?|?6$Z@ZMKAll3eXYgSYd7O1{v>kt4BLNm`5<(#3G6S7r;>B~%3)Tr z`?FjxHG78{;v|>S*}i~hO)D&ZqR?cYS6z>G0?nU%y8f8R2>SC>d_^Qw`>RT`-Xy#A zAa{F8<0*$?P?@qV(zwK9w#6bMBGV zGu{IoX%!_GeSp-}xJ`BQtjgSPzxt~O(c0=Pg|Q`e0+gX*M%+|8X>GaNiSk=ia46-S z9%;%&Y?jn0#yX?rjqi;1X-66_e^f|cE=6dTpR7EL;5sIk`NYULid~N?>QS^-SOm=i zW$1H>E5poN!_?DRj}p$7h&ION+_xlYeySlQktcY>7g?gs%t_14Ic;g%3|N7^<^cl` zRq>an<)1L&9x0h~A4|F~27sb^4T~+)^@aq&x67$H};Lx2b zs2?JQLvz4TDYAi%wxYu8s27>rCxuER0t^*=smJ=|pZOjmNT`74Buiz5=UAsQVKI1y zWjBXyTl14*S56q)CHk?OzDGtiIGQH;t=-}N{`060kfmc!vREqnVKG;@(SpZ&_bZ-9 zNu9oDWyXR}H*%{bN0U{vhuyHNj;iwt;cZ&o(T^G-;yw^K1}(Fl(fyWEn7 zKLH&^#G6fgatPplS$(J2KYS_aJ3mo1qp^Xdd!{x)_?3ImQb`cQ+7I4J)>?;VVJ0`+ z(2h4&={sgN+jmUuS+wMfhZb>g?AyAzKK@qXMCHKZtMg1fx!WDd7+Ylye#^CmhEF3O z%*upqanid*n;{!K*LCNO>Iuq=%gA+m{;_V?Ls8S3AEV2+{la;Zl6GL9QwF>jzZX># z_>T&aXlGh)jpwcol>8lwN3|H*pqM0;)jUiCB`?#C>&*YVvK{7jy9=tG(!iM@=Z&tH zfE$@c&Pt%t9jiSyWUes|-NT_4mQ-uKBFo`Gut5EIzsHm4l+9^s_V`B*6G#72Bce)5oAx?<{KHGv_!`PG=@>^V<5(w+Xm^1$H2(pisO>ud literal 0 HcmV?d00001 diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py index 839c4fb..6165021 100644 --- a/tests/integration_test_push.py +++ b/tests/integration_test_push.py @@ -1,10 +1,68 @@ # -*- coding: utf-8 -*- +import uuid + from syncano.exceptions import SyncanoRequestError -from syncano.models import APNSDevice, APNSMessage, GCMDevice, GCMMessage +from syncano.models import APNSConfig, APNSDevice, APNSMessage, GCMConfig, GCMDevice, GCMMessage from tests.integration_test import InstanceMixin, IntegrationTest -class PushNotificationTest(InstanceMixin, IntegrationTest): +class PushIntegrationTest(IntegrationTest): + + @classmethod + def setUpClass(cls): + super(PushIntegrationTest, cls).setUpClass() + + cls.gcm_config = GCMConfig( + development_api_key=uuid.uuid4().hex + ) + cls.gcm_config.save() + + with open('certificates/ApplePushDevelopment.pk12', 'r') as cert: + cls.apns_config = APNSConfig( + development_certificate=cert, + development_certificate_name='test', + development_bundle_identifier='test1234' + ) + cls.apns_config.save() + + cls.environment = 'development' + cls.gcm_device = GCMDevice( + instance_name=cls.instance.name, + label='example label', + registration_id=86152312314401555, + device_id='10000000001', + ) + cls.gcm_device.save() + + cls.apns_device = APNSDevice( + instance_name=cls.instance.name, + label='example label', + registration_id='4719084371920471208947120984731208947910827409128470912847120894', + device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a', + ) + cls.apns_device.save() + + +class PushNotificationTest(InstanceMixin, PushIntegrationTest): + + def test_gcm_config_update(self): + gcm_config = GCMConfig.please.get() + new_key = uuid.uuid4().hex + gcm_config.development_api_key = new_key + gcm_config.save() + + gcm_config_ = GCMConfig.please.get() + self.assertEqual(gcm_config_.development_api_key, new_key) + + def test_apns_config_update(self): + apns_config = APNSConfig.please.get() + new_cert_name = 'new cert name' + apns_config.development_certificate_name = new_cert_name + apns_config.save() + + apns_config_ = APNSConfig.please.get() + self.assertEqual(apns_config_.development_certificate_name, new_cert_name) + def test_gcm_device(self): device = GCMDevice( instance_name=self.instance.name, @@ -24,29 +82,46 @@ def test_apns_device(self): self._test_device(device, APNSDevice.please) + def test_send_message_gcm(self): + + self.assertEqual(0, len(list(GCMMessage.please.all()))) + + self.gcm_device.send_message(contet={'environment': self.environment, 'data': {'c': 'more_c'}}) + + self.assertEqual(1, len(list(GCMMessage.please.all()))) + + def test_send_message_apns(self): + self.assertEqual(0, len(list(APNSMessage.please.all()))) + + self.apns_device.send_message(contet={'environment': 'development', 'aps': {'alert': 'alert test'}}) + + self.assertEqual(1, len(list(APNSMessage.please.all()))) + def test_gcm_message(self): message = GCMMessage( instance_name=self.instance.name, content={ 'registration_ids': ['TESTIDREGISRATION', ], + 'environment': 'production', 'data': { 'param1': 'test' } } ) - self._test_message(message, GCMMessage.please) + self._test_message(message, GCMMessage.please) # we want this to fail; no productions keys; def test_apns_message(self): message = APNSMessage( instance_name=self.instance.name, content={ 'registration_ids': ['TESTIDREGISRATION', ], + 'environment': 'production', 'aps': {'alert': 'semo example label'} } ) - self._test_message(message, APNSMessage.please) + self._test_message(message, APNSMessage.please) # we want this to fail; no productions certs; def _test_device(self, device, manager): From 93a7e5f0706ed9edeceb9625e659646de8de6148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 14:55:31 +0200 Subject: [PATCH 360/558] [LIB-660] correct setup test --- tests/integration_test_push.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py index 6165021..3b4f41c 100644 --- a/tests/integration_test_push.py +++ b/tests/integration_test_push.py @@ -13,7 +13,8 @@ def setUpClass(cls): super(PushIntegrationTest, cls).setUpClass() cls.gcm_config = GCMConfig( - development_api_key=uuid.uuid4().hex + development_api_key=uuid.uuid4().hex, + instance_name=cls.instance.name ) cls.gcm_config.save() @@ -21,7 +22,8 @@ def setUpClass(cls): cls.apns_config = APNSConfig( development_certificate=cert, development_certificate_name='test', - development_bundle_identifier='test1234' + development_bundle_identifier='test1234', + instance_name=cls.instance.name ) cls.apns_config.save() From 432574b4f8885370dd42f066af9ff5cb7ecc4b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 15:02:58 +0200 Subject: [PATCH 361/558] [LIB-660] correct setup test --- tests/integration_test_push.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py index 3b4f41c..a838485 100644 --- a/tests/integration_test_push.py +++ b/tests/integration_test_push.py @@ -6,7 +6,7 @@ from tests.integration_test import InstanceMixin, IntegrationTest -class PushIntegrationTest(IntegrationTest): +class PushIntegrationTest(InstanceMixin, IntegrationTest): @classmethod def setUpClass(cls): @@ -45,7 +45,7 @@ def setUpClass(cls): cls.apns_device.save() -class PushNotificationTest(InstanceMixin, PushIntegrationTest): +class PushNotificationTest(PushIntegrationTest): def test_gcm_config_update(self): gcm_config = GCMConfig.please.get() From f171f043d6ae20afce1af1483710e1983b7c71fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 15:08:47 +0200 Subject: [PATCH 362/558] [LIB-660] correct typo --- .../ApplePushDevelopment.p12 | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{ceritificates => certificates}/ApplePushDevelopment.p12 (100%) diff --git a/tests/ceritificates/ApplePushDevelopment.p12 b/tests/certificates/ApplePushDevelopment.p12 similarity index 100% rename from tests/ceritificates/ApplePushDevelopment.p12 rename to tests/certificates/ApplePushDevelopment.p12 From 16e1f84498641d9f6dc8f142c955687b7b6b80b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 15:15:53 +0200 Subject: [PATCH 363/558] [LIB-660] correct typo --- tests/integration_test_push.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py index a838485..82f9153 100644 --- a/tests/integration_test_push.py +++ b/tests/integration_test_push.py @@ -18,7 +18,7 @@ def setUpClass(cls): ) cls.gcm_config.save() - with open('certificates/ApplePushDevelopment.pk12', 'r') as cert: + with open('certificates/ApplePushDevelopment.p12', 'r') as cert: cls.apns_config = APNSConfig( development_certificate=cert, development_certificate_name='test', From 2f2177c99ad36fd50e5f80d056882f2b0744acd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 15:23:26 +0200 Subject: [PATCH 364/558] [LIB-660] correct cert path --- tests/integration_test_push.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py index 82f9153..bf7a433 100644 --- a/tests/integration_test_push.py +++ b/tests/integration_test_push.py @@ -18,7 +18,7 @@ def setUpClass(cls): ) cls.gcm_config.save() - with open('certificates/ApplePushDevelopment.p12', 'r') as cert: + with open('tests/certificates/ApplePushDevelopment.p12', 'r') as cert: cls.apns_config = APNSConfig( development_certificate=cert, development_certificate_name='test', From 8303c16b9f4f47749f3e566d22c3de9cf2eae050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 15:31:01 +0200 Subject: [PATCH 365/558] [LIB-660] test fixes --- tests/integration_test_push.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py index bf7a433..b0a2c43 100644 --- a/tests/integration_test_push.py +++ b/tests/integration_test_push.py @@ -88,14 +88,14 @@ def test_send_message_gcm(self): self.assertEqual(0, len(list(GCMMessage.please.all()))) - self.gcm_device.send_message(contet={'environment': self.environment, 'data': {'c': 'more_c'}}) + self.gcm_device.send_message(content={'environment': self.environment, 'data': {'c': 'more_c'}}) self.assertEqual(1, len(list(GCMMessage.please.all()))) def test_send_message_apns(self): self.assertEqual(0, len(list(APNSMessage.please.all()))) - self.apns_device.send_message(contet={'environment': 'development', 'aps': {'alert': 'alert test'}}) + self.apns_device.send_message(content={'environment': 'development', 'aps': {'alert': 'alert test'}}) self.assertEqual(1, len(list(APNSMessage.please.all()))) @@ -127,11 +127,9 @@ def test_apns_message(self): def _test_device(self, device, manager): - self.assertFalse(manager.all(instance_name=self.instance.name)) - device.save() - self.assertEqual(len(list(manager.all(instance_name=self.instance.name,))), 1) + self.assertEqual(len(list(manager.all(instance_name=self.instance.name,))), 2) # test get: device_ = manager.get(instance_name=self.instance.name, registration_id=device.registration_id) @@ -150,8 +148,6 @@ def _test_device(self, device, manager): device.delete() - self.assertFalse(manager.all(instance_name=self.instance.name)) - def _test_message(self, message, manager): self.assertFalse(manager.all(instance_name=self.instance.name)) From 58a31196b42e11674c90d4d9f6f41a52deb85f74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 15:37:34 +0200 Subject: [PATCH 366/558] [LIB-660] test fixes --- tests/integration_test_push.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py index b0a2c43..4434452 100644 --- a/tests/integration_test_push.py +++ b/tests/integration_test_push.py @@ -69,7 +69,7 @@ def test_gcm_device(self): device = GCMDevice( instance_name=self.instance.name, label='example label', - registration_id=86152312314401555, + registration_id=86152312314401666, device_id='10000000001', ) self._test_device(device, GCMDevice.please) @@ -78,7 +78,7 @@ def test_apns_device(self): device = APNSDevice( instance_name=self.instance.name, label='example label', - registration_id='4719084371920471208947120984731208947910827409128470912847120894', + registration_id='4719084371920471208947120984731208947910827409128470912847120222', device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a', ) From 670087186c8b500b447e9f20f5652b4d3ac68612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 16:04:15 +0200 Subject: [PATCH 367/558] [LIB-660] add binary mode to read file --- syncano/connection.py | 2 +- tests/integration_test_push.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index 577fe0a..60fa058 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -404,7 +404,7 @@ def get_user_info(self, api_key=None, user_key=None): def _process_apns_cert_files(self, files): files = files.copy() - for key in files.keys(): + for key in [file_name for file_name in files.keys()]: # remove certificates files (which are bool - True if cert exist, False otherwise) value = files[key] if isinstance(value, bool): diff --git a/tests/integration_test_push.py b/tests/integration_test_push.py index 4434452..2eaed62 100644 --- a/tests/integration_test_push.py +++ b/tests/integration_test_push.py @@ -18,7 +18,7 @@ def setUpClass(cls): ) cls.gcm_config.save() - with open('tests/certificates/ApplePushDevelopment.p12', 'r') as cert: + with open('tests/certificates/ApplePushDevelopment.p12', 'rb') as cert: cls.apns_config = APNSConfig( development_certificate=cert, development_certificate_name='test', From 68bc1027498c9e92272777952d25c03b10e147fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Apr 2016 16:12:36 +0200 Subject: [PATCH 368/558] [LIB-660] correct comment --- syncano/models/push_notification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index 95fb080..bdaa3e8 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -295,7 +295,7 @@ class APNSConfig(Model): Usage:: Add (modify) new keys: - cert_file = open('cert_file.p12', 'r') + cert_file = open('cert_file.p12', 'rb') apns_config = APNSConfig(development_certificate=cert_file) apns_config.save() cert_file.close() From 16809b235de9bcfef6737f9c7f88ff6e88689f9e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 26 Apr 2016 13:54:25 +0200 Subject: [PATCH 369/558] [LIB-653] GeoPoint re work, add tests, add possibility to filter; --- syncano/models/fields.py | 55 +++++++++++++++++++++--- syncano/models/manager.py | 2 +- tests/integration_test_geo.py | 78 +++++++++++++++++++++++++++++++++++ tests/test_fields.py | 20 +++++++++ 4 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 tests/integration_test_geo.py diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 12f10f8..e939e24 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -1,6 +1,5 @@ import json import re -from collections import namedtuple from datetime import date, datetime import six @@ -687,10 +686,33 @@ def to_native(self, value): return value -GeoPointStruct = namedtuple('GeoPointHelper', ['latitude', 'longitude']) +class GeoPoint(Field): + KILOMETERS = '_in_kilometers' + MILES = '_in_miles' -class GeoPoint(Field): + class GeoPointStruct(object): + + def __init__(self, latitude=None, longitude=None, distance=None, unit=None): + if not latitude and not longitude: + raise SyncanoValueError('Provide latitude and longitude') + self.latitude = latitude + self.longitude = longitude + self.distance = distance + self.unit = unit if unit in [GeoPoint.MILES, GeoPoint.KILOMETERS] else GeoPoint.KILOMETERS + + def __str__(self): + return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) + + def __repr__(self): + return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) + + def to_native(self): + geo_struct_dump = {'latitude': self.latitude, 'longitude': self.longitude} + if self.distance is not None: + distance_key = 'distance' + self.unit + geo_struct_dump[distance_key] = self.distance + return geo_struct_dump def validate(self, value, model_instance): super(GeoPoint, self).validate(value, model_instance) @@ -704,18 +726,39 @@ def validate(self, value, model_instance): except (ValueError, TypeError): raise SyncanoValueError('Expected an object') - if not isinstance(value, GeoPointStruct): + if not isinstance(value, GeoPoint.GeoPointStruct): raise SyncanoValueError('Expected an GeoPointStruct') def to_native(self, value): if value is None: return - geo_struct = {'latitude': value[0], 'longitude': value[1]} + if isinstance(value, bool): + return value # exists lookup + + if isinstance(value, dict): + value = GeoPoint.GeoPointStruct(**value) + + geo_struct = value.to_native() geo_struct = json.dumps(geo_struct) return geo_struct + def to_query(self, value, lookup_type): + """ + Returns field's value prepared for usage in HTTP request query. + """ + super(GeoPoint, self).to_query(value, lookup_type) + + if lookup_type not in ['near', 'exists']: + raise SyncanoValueError('Lookup {} not supported for geopoint field'.format(lookup_type)) + + if isinstance(value, dict): + value = GeoPoint.GeoPointStruct(**value) + return value.to_native() + + return value + def to_python(self, value): if value is None: return @@ -742,7 +785,7 @@ def to_python(self, value): if not longitude or not latitude: raise SyncanoValueError('Expected the `longitude` and `latitude` fields.') - return GeoPointStruct(latitude, longitude) + return GeoPoint.GeoPointStruct(latitude=latitude, longitude=longitude) MAPPING = { diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 0a2f867..cd72e0f 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -882,7 +882,7 @@ class for :class:`~syncano.models.base.Object` model. LOOKUP_SEPARATOR = '__' ALLOWED_LOOKUPS = [ 'gt', 'gte', 'lt', 'lte', - 'eq', 'neq', 'exists', 'in', + 'eq', 'neq', 'exists', 'in', 'near' ] def __init__(self): diff --git a/tests/integration_test_geo.py b/tests/integration_test_geo.py new file mode 100644 index 0000000..88530e8 --- /dev/null +++ b/tests/integration_test_geo.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +import six +from syncano.models import Class, GeoPoint, Object +from tests.integration_test import InstanceMixin, IntegrationTest + + +class GeoPointApiTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + cls.city_model = Class.please.create(name='city', schema=[ + {"name": "city", "type": "string"}, + {"name": "location", "type": "geopoint", "filter_index": True}, + ]) + + cls.warsaw = Object.please.create(class_name='city', location=(52.2240698, 20.9942933), city='Warsaw') + cls.paris = Object.please.create(class_name='city', location=(52.4731384, 13.5425588), city='Berlin') + cls.berlin = Object.please.create(class_name='city', location=(48.8589101, 2.3125377), city='Paris') + cls.london = Object.please.create(class_name='city', city='London') + + cls.list_london = ['London'] + cls.list_warsaw = ['Warsaw'] + cls.list_warsaw_berlin = ['Warsaw', 'Berlin'] + cls.list_warsaw_berlin_paris = ['Warsaw', 'Berlin', 'Paris'] + + def test_filtering_on_geo_point_near(self): + + distances = { + 100: self.list_warsaw, + 600: self.list_warsaw_berlin, + 1400: self.list_warsaw_berlin_paris + } + + for distance, cities in six.iteritems(distances): + objects = Object.please.list(class_name="city").filter( + geo_point__near={ + "latitude": 52.2297, + "longitude": 21.0122, + "distance": distance, + "unit": GeoPoint.KILOMETERS + } + ) + + result_list = self._prepare_result_list(objects) + + self.assertListEqual(result_list, cities) + + def test_filtering_on_geo_pint_near_miles(self): + objects = Object.please.list(class_name="city").filter( + geo_point__near={ + "latitude": 52.2297, + "longitude": 21.0122, + "distance": 10, + "unit": GeoPoint.MILES + } + ) + result_list = self._prepare_result_list(objects) + self.assertListEqual(result_list, self.list_warsaw) + + def test_filtering_on_geo_point_exists(self): + objects = Object.please.list(class_name="geo_klass").filter( + geo_point__exists=True + ) + + result_list = [o.city for o in objects] + + self.assertListEqual(result_list, self.list_warsaw_berlin_paris) + + objects = Object.please.list(class_name="geo_klass").filter( + geo_point__exists=False + ) + + result_list = self._prepare_result_list(objects) + + self.assertListEqual(result_list, self.list_london) + + def _prepare_result_list(self, objects): + return [o.city for o in objects] diff --git a/tests/test_fields.py b/tests/test_fields.py index 7a14dd7..819a02a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -63,6 +63,7 @@ class AllFieldsModel(models.Model): schema_field = models.SchemaField() array_field = models.ArrayField() object_field = models.ObjectField() + geo_field = models.GeoPoint() class Meta: endpoints = { @@ -571,3 +572,22 @@ def test_to_python(self): self.field.to_python({'raz': 1, 'dwa': 2}) self.field.to_python('{"raz": 1, "dwa": 2}') + + +class GeoPointTestCase(BaseTestCase): + field_name = 'geo_field' + + def test_validate(self): + + with self.assertRaises(SyncanoValueError): + self.field.validate(12, self.instance) + + self.field.validate((52.12, 12.02), self.instance) + self.field.validate({'latitude': 52.12, 'longitude': 12.02}, self.instance) + + def test_to_python(self): + with self.assertRaises(SyncanoValueError): + self.field.to_python(12) + + self.field.to_python((52.12, 12.02)) + self.field.to_python({'latitude': 52.12, 'longitude': 12.02}) From 251c56f35c3493f5739cbc082ab90dfaa362370d Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 26 Apr 2016 14:06:09 +0200 Subject: [PATCH 370/558] [LIB-653] correct flake issues --- tests/integration_test_geo.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration_test_geo.py b/tests/integration_test_geo.py index 88530e8..c9358b3 100644 --- a/tests/integration_test_geo.py +++ b/tests/integration_test_geo.py @@ -47,13 +47,13 @@ def test_filtering_on_geo_point_near(self): def test_filtering_on_geo_pint_near_miles(self): objects = Object.please.list(class_name="city").filter( - geo_point__near={ - "latitude": 52.2297, - "longitude": 21.0122, - "distance": 10, - "unit": GeoPoint.MILES - } - ) + geo_point__near={ + "latitude": 52.2297, + "longitude": 21.0122, + "distance": 10, + "unit": GeoPoint.MILES + } + ) result_list = self._prepare_result_list(objects) self.assertListEqual(result_list, self.list_warsaw) From 5437253031c231275ba2fbc59659bdfd7edb3e90 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 26 Apr 2016 14:17:11 +0200 Subject: [PATCH 371/558] [LIB-653] correct field geo test; --- tests/test_fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 819a02a..c6b9dbb 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -5,6 +5,7 @@ from time import mktime import six +from models import GeoPoint from syncano import models from syncano.exceptions import SyncanoValidationError, SyncanoValueError from syncano.models.manager import SchemaManager @@ -582,8 +583,7 @@ def test_validate(self): with self.assertRaises(SyncanoValueError): self.field.validate(12, self.instance) - self.field.validate((52.12, 12.02), self.instance) - self.field.validate({'latitude': 52.12, 'longitude': 12.02}, self.instance) + self.field.validate(GeoPoint.GeoPointStruct(**{'latitude': 52.12, 'longitude': 12.02}), self.instance) def test_to_python(self): with self.assertRaises(SyncanoValueError): From 5d9ccf25126baeeb329581e1d4ef76c3b3c3d89f Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 26 Apr 2016 14:21:23 +0200 Subject: [PATCH 372/558] [LIB-653] correct field geo test; --- tests/test_fields.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index c6b9dbb..1c021da 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -5,7 +5,6 @@ from time import mktime import six -from models import GeoPoint from syncano import models from syncano.exceptions import SyncanoValidationError, SyncanoValueError from syncano.models.manager import SchemaManager @@ -583,7 +582,7 @@ def test_validate(self): with self.assertRaises(SyncanoValueError): self.field.validate(12, self.instance) - self.field.validate(GeoPoint.GeoPointStruct(**{'latitude': 52.12, 'longitude': 12.02}), self.instance) + self.field.validate(models.GeoPoint.GeoPointStruct(**{'latitude': 52.12, 'longitude': 12.02}), self.instance) def test_to_python(self): with self.assertRaises(SyncanoValueError): From 5e57ea987b8f74a18155d17fca3db5a7d47d2549 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 26 Apr 2016 14:40:36 +0200 Subject: [PATCH 373/558] [LIB-653] correct field geo test; --- tests/integration_test_geo.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests/integration_test_geo.py b/tests/integration_test_geo.py index c9358b3..d16ef08 100644 --- a/tests/integration_test_geo.py +++ b/tests/integration_test_geo.py @@ -8,15 +8,19 @@ class GeoPointApiTest(InstanceMixin, IntegrationTest): @classmethod def setUpClass(cls): - cls.city_model = Class.please.create(name='city', schema=[ + cls.city_model = Class.please.create(instance_name=cls.instance.name, name='city', schema=[ {"name": "city", "type": "string"}, {"name": "location", "type": "geopoint", "filter_index": True}, ]) - cls.warsaw = Object.please.create(class_name='city', location=(52.2240698, 20.9942933), city='Warsaw') - cls.paris = Object.please.create(class_name='city', location=(52.4731384, 13.5425588), city='Berlin') - cls.berlin = Object.please.create(class_name='city', location=(48.8589101, 2.3125377), city='Paris') - cls.london = Object.please.create(class_name='city', city='London') + cls.warsaw = Object.please.create(instance_name=cls.instance.name, + class_name='city', location=(52.2240698, 20.9942933), city='Warsaw') + cls.paris = Object.please.create(instance_name=cls.instance.name, + class_name='city', location=(52.4731384, 13.5425588), city='Berlin') + cls.berlin = Object.please.create(instance_name=cls.instance.name, + class_name='city', location=(48.8589101, 2.3125377), city='Paris') + cls.london = Object.please.create(instance_name=cls.instance.name, + class_name='city', city='London') cls.list_london = ['London'] cls.list_warsaw = ['Warsaw'] @@ -32,7 +36,7 @@ def test_filtering_on_geo_point_near(self): } for distance, cities in six.iteritems(distances): - objects = Object.please.list(class_name="city").filter( + objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( geo_point__near={ "latitude": 52.2297, "longitude": 21.0122, @@ -46,7 +50,7 @@ def test_filtering_on_geo_point_near(self): self.assertListEqual(result_list, cities) def test_filtering_on_geo_pint_near_miles(self): - objects = Object.please.list(class_name="city").filter( + objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( geo_point__near={ "latitude": 52.2297, "longitude": 21.0122, @@ -58,7 +62,7 @@ def test_filtering_on_geo_pint_near_miles(self): self.assertListEqual(result_list, self.list_warsaw) def test_filtering_on_geo_point_exists(self): - objects = Object.please.list(class_name="geo_klass").filter( + objects = Object.please.list(instance_name=self.instance.name, class_name="geo_klass").filter( geo_point__exists=True ) @@ -66,7 +70,7 @@ def test_filtering_on_geo_point_exists(self): self.assertListEqual(result_list, self.list_warsaw_berlin_paris) - objects = Object.please.list(class_name="geo_klass").filter( + objects = Object.please.list(instance_name=self.instance.name, class_name="geo_klass").filter( geo_point__exists=False ) From e6fcd34794d5daf12e9db69d7df53d0ade27b3ac Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 26 Apr 2016 14:47:09 +0200 Subject: [PATCH 374/558] [LIB-653] correct field geo test; --- tests/integration_test_geo.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration_test_geo.py b/tests/integration_test_geo.py index d16ef08..31f21e3 100644 --- a/tests/integration_test_geo.py +++ b/tests/integration_test_geo.py @@ -8,6 +8,8 @@ class GeoPointApiTest(InstanceMixin, IntegrationTest): @classmethod def setUpClass(cls): + super(GeoPointApiTest, cls).setUpClass() + cls.city_model = Class.please.create(instance_name=cls.instance.name, name='city', schema=[ {"name": "city", "type": "string"}, {"name": "location", "type": "geopoint", "filter_index": True}, From fd1e13d32d46083cef6de2bea7d46efa310bbfd1 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Tue, 26 Apr 2016 14:53:46 +0200 Subject: [PATCH 375/558] [LIB-653] correct field geo test; --- tests/integration_test_geo.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration_test_geo.py b/tests/integration_test_geo.py index 31f21e3..bf5f575 100644 --- a/tests/integration_test_geo.py +++ b/tests/integration_test_geo.py @@ -39,7 +39,7 @@ def test_filtering_on_geo_point_near(self): for distance, cities in six.iteritems(distances): objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( - geo_point__near={ + location__near={ "latitude": 52.2297, "longitude": 21.0122, "distance": distance, @@ -51,9 +51,9 @@ def test_filtering_on_geo_point_near(self): self.assertListEqual(result_list, cities) - def test_filtering_on_geo_pint_near_miles(self): + def test_filtering_on_geo_point_near_miles(self): objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( - geo_point__near={ + location__near={ "latitude": 52.2297, "longitude": 21.0122, "distance": 10, @@ -64,16 +64,16 @@ def test_filtering_on_geo_pint_near_miles(self): self.assertListEqual(result_list, self.list_warsaw) def test_filtering_on_geo_point_exists(self): - objects = Object.please.list(instance_name=self.instance.name, class_name="geo_klass").filter( - geo_point__exists=True + objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( + location__exists=True ) result_list = [o.city for o in objects] self.assertListEqual(result_list, self.list_warsaw_berlin_paris) - objects = Object.please.list(instance_name=self.instance.name, class_name="geo_klass").filter( - geo_point__exists=False + objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( + location__exists=False ) result_list = self._prepare_result_list(objects) From a1c873f292e4b61e93b6666495d22e63d988696e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 27 Apr 2016 14:45:21 +0200 Subject: [PATCH 376/558] [LIB-653] correct interfaces a little: split GeoPoint from GeoLookup, add possibility to define lookup near in such way: GeoLookup(GeoPoint(lat, lng), distance, unit) --- syncano/models/fields.py | 86 +++++++++++++++++++++++------------ tests/integration_test_geo.py | 13 ++++-- tests/test_fields.py | 4 +- 3 files changed, 69 insertions(+), 34 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index e939e24..839a670 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -686,36 +686,52 @@ def to_native(self, value): return value -class GeoPoint(Field): +class GeoPoint(object): + + def __init__(self, latitude, longitude): + self.latitude = latitude + self.longitude = longitude + + def __str__(self): + return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) + + def __repr__(self): + return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) + + def to_native(self): + geo_struct_dump = {'latitude': self.latitude, 'longitude': self.longitude} + return geo_struct_dump + + +class GeoLookup(object): KILOMETERS = '_in_kilometers' MILES = '_in_miles' - class GeoPointStruct(object): + def __init__(self, geo_point, distance, unit=None): + if not isinstance(geo_point, GeoPoint): + raise SyncanoValueError('GeoPoint expected.') - def __init__(self, latitude=None, longitude=None, distance=None, unit=None): - if not latitude and not longitude: - raise SyncanoValueError('Provide latitude and longitude') - self.latitude = latitude - self.longitude = longitude - self.distance = distance - self.unit = unit if unit in [GeoPoint.MILES, GeoPoint.KILOMETERS] else GeoPoint.KILOMETERS + if unit is not None and unit not in [self.KILOMETERS, self.MILES]: + raise SyncanoValueError('Possible `unit` values: GeoLookup.MILES, GeoLookup.KILOMETERS') - def __str__(self): - return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) + self.geo_point = geo_point + self.distance = distance + self.unit = unit or self.KILOMETERS - def __repr__(self): - return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) + def to_native(self): + distance_key = 'distance_'.format(self.unit) + return { + 'latitude': self.geo_point.latitude, + 'longitude': self.geo_point.longitude, + distance_key: self.distance + } - def to_native(self): - geo_struct_dump = {'latitude': self.latitude, 'longitude': self.longitude} - if self.distance is not None: - distance_key = 'distance' + self.unit - geo_struct_dump[distance_key] = self.distance - return geo_struct_dump + +class GeoPointField(Field): def validate(self, value, model_instance): - super(GeoPoint, self).validate(value, model_instance) + super(GeoPointField, self).validate(value, model_instance) if not self.required and not value: return @@ -726,8 +742,8 @@ def validate(self, value, model_instance): except (ValueError, TypeError): raise SyncanoValueError('Expected an object') - if not isinstance(value, GeoPoint.GeoPointStruct): - raise SyncanoValueError('Expected an GeoPointStruct') + if not isinstance(value, GeoPoint): + raise SyncanoValueError('Expected an GeoPoint') def to_native(self, value): if value is None: @@ -737,7 +753,7 @@ def to_native(self, value): return value # exists lookup if isinstance(value, dict): - value = GeoPoint.GeoPointStruct(**value) + value = GeoPoint(latitude=value['latitude'], longitude=value['longitude']) geo_struct = value.to_native() geo_struct = json.dumps(geo_struct) @@ -748,16 +764,28 @@ def to_query(self, value, lookup_type): """ Returns field's value prepared for usage in HTTP request query. """ - super(GeoPoint, self).to_query(value, lookup_type) + super(GeoPointField, self).to_query(value, lookup_type) if lookup_type not in ['near', 'exists']: raise SyncanoValueError('Lookup {} not supported for geopoint field'.format(lookup_type)) + if lookup_type in ['exists']: + if isinstance(value, bool): + return value + else: + raise SyncanoValueError('Bool expected in {} lookup.'.format(lookup_type)) + if isinstance(value, dict): - value = GeoPoint.GeoPointStruct(**value) - return value.to_native() + value = GeoLookup( + GeoPoint(latitude=value['latitude'], longitude=value['longitude']), + distance=value['distance'], + unit=value.get('unit') + ) - return value + if not isinstance(value, GeoLookup): + raise SyncanoValueError('Expected GeoLookup.') + + return value.to_native() def to_python(self, value): if value is None: @@ -785,7 +813,7 @@ def to_python(self, value): if not longitude or not latitude: raise SyncanoValueError('Expected the `longitude` and `latitude` fields.') - return GeoPoint.GeoPointStruct(latitude=latitude, longitude=longitude) + return GeoPoint(latitude=latitude, longitude=longitude) MAPPING = { @@ -811,5 +839,5 @@ def to_python(self, value): 'schema': SchemaField, 'array': ArrayField, 'object': ObjectField, - 'geopoint': GeoPoint, + 'geopoint': GeoPointField, } diff --git a/tests/integration_test_geo.py b/tests/integration_test_geo.py index bf5f575..c85ab53 100644 --- a/tests/integration_test_geo.py +++ b/tests/integration_test_geo.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import six -from syncano.models import Class, GeoPoint, Object +from syncano.models import Class, GeoLookup, GeoPoint, Object from tests.integration_test import InstanceMixin, IntegrationTest @@ -43,7 +43,7 @@ def test_filtering_on_geo_point_near(self): "latitude": 52.2297, "longitude": 21.0122, "distance": distance, - "unit": GeoPoint.KILOMETERS + "unit": GeoLookup.KILOMETERS } ) @@ -57,12 +57,19 @@ def test_filtering_on_geo_point_near_miles(self): "latitude": 52.2297, "longitude": 21.0122, "distance": 10, - "unit": GeoPoint.MILES + "unit": GeoLookup.MILES } ) result_list = self._prepare_result_list(objects) self.assertListEqual(result_list, self.list_warsaw) + def test_filtering_on_geo_point_near_with_another_syntax(self): + objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( + location__near=GeoLookup(GeoPoint(52.2297, 21.0122), distance=10, unit=GeoLookup.MILES) + ) + result_list = self._prepare_result_list(objects) + self.assertListEqual(result_list, self.list_warsaw) + def test_filtering_on_geo_point_exists(self): objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( location__exists=True diff --git a/tests/test_fields.py b/tests/test_fields.py index 1c021da..68ce868 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -63,7 +63,7 @@ class AllFieldsModel(models.Model): schema_field = models.SchemaField() array_field = models.ArrayField() object_field = models.ObjectField() - geo_field = models.GeoPoint() + geo_field = models.GeoPointField() class Meta: endpoints = { @@ -582,7 +582,7 @@ def test_validate(self): with self.assertRaises(SyncanoValueError): self.field.validate(12, self.instance) - self.field.validate(models.GeoPoint.GeoPointStruct(**{'latitude': 52.12, 'longitude': 12.02}), self.instance) + self.field.validate(models.GeoPoint(latitude=52.12, longitude=12.02), self.instance) def test_to_python(self): with self.assertRaises(SyncanoValueError): From 7ef7d8300fd3dbe51baf31c8669453ae9ccecbbb Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Wed, 27 Apr 2016 15:00:18 +0200 Subject: [PATCH 377/558] [LIB-653] correct bug --- syncano/models/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 839a670..368efe9 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -720,7 +720,7 @@ def __init__(self, geo_point, distance, unit=None): self.unit = unit or self.KILOMETERS def to_native(self): - distance_key = 'distance_'.format(self.unit) + distance_key = 'distance{}'.format(self.unit) return { 'latitude': self.geo_point.latitude, 'longitude': self.geo_point.longitude, From 893671c4c61a8d2e9a7b2921db3938040ab4492f Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 28 Apr 2016 13:00:50 +0200 Subject: [PATCH 378/558] [LIB-653] Correct geo handling --- syncano/models/base.py | 3 +- syncano/models/fields.py | 92 ++++++++++++++--------------------- syncano/models/geo.py | 40 +++++++++++++++ tests/integration_test_geo.py | 58 ++++++++++++++-------- tests/test_fields.py | 1 + 5 files changed, 116 insertions(+), 78 deletions(-) create mode 100644 syncano/models/geo.py diff --git a/syncano/models/base.py b/syncano/models/base.py index 7bc3d35..0dd9089 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -8,4 +8,5 @@ from .data_views import * # NOQA from .incentives import * # NOQA from .traces import * # NOQA -from .push_notification import * # NOQA \ No newline at end of file +from .push_notification import * # NOQA +from .geo import * # NOQA diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 368efe9..1973d0e 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -8,6 +8,7 @@ from syncano.exceptions import SyncanoFieldError, SyncanoValueError from syncano.utils import force_text +from .geo import Distance, GeoPoint from .manager import SchemaManager from .registry import registry @@ -686,48 +687,6 @@ def to_native(self, value): return value -class GeoPoint(object): - - def __init__(self, latitude, longitude): - self.latitude = latitude - self.longitude = longitude - - def __str__(self): - return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) - - def __repr__(self): - return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) - - def to_native(self): - geo_struct_dump = {'latitude': self.latitude, 'longitude': self.longitude} - return geo_struct_dump - - -class GeoLookup(object): - - KILOMETERS = '_in_kilometers' - MILES = '_in_miles' - - def __init__(self, geo_point, distance, unit=None): - if not isinstance(geo_point, GeoPoint): - raise SyncanoValueError('GeoPoint expected.') - - if unit is not None and unit not in [self.KILOMETERS, self.MILES]: - raise SyncanoValueError('Possible `unit` values: GeoLookup.MILES, GeoLookup.KILOMETERS') - - self.geo_point = geo_point - self.distance = distance - self.unit = unit or self.KILOMETERS - - def to_native(self): - distance_key = 'distance{}'.format(self.unit) - return { - 'latitude': self.geo_point.latitude, - 'longitude': self.geo_point.longitude, - distance_key: self.distance - } - - class GeoPointField(Field): def validate(self, value, model_instance): @@ -755,7 +714,11 @@ def to_native(self, value): if isinstance(value, dict): value = GeoPoint(latitude=value['latitude'], longitude=value['longitude']) - geo_struct = value.to_native() + if isinstance(value, tuple): + geo_struct = value[0].to_native() + else: + geo_struct = value.to_native() + geo_struct = json.dumps(geo_struct) return geo_struct @@ -776,27 +739,47 @@ def to_query(self, value, lookup_type): raise SyncanoValueError('Bool expected in {} lookup.'.format(lookup_type)) if isinstance(value, dict): - value = GeoLookup( - GeoPoint(latitude=value['latitude'], longitude=value['longitude']), - distance=value['distance'], - unit=value.get('unit') + value = ( + GeoPoint(latitude=value.pop('latitude'), longitude=value.pop('longitude')), + Distance(**value) ) - if not isinstance(value, GeoLookup): - raise SyncanoValueError('Expected GeoLookup.') + if len(value) != 2 or not isinstance(value[0], GeoPoint) or not isinstance(value[1], Distance): + raise SyncanoValueError('This lookup should be a tuple with GeoPoint and Distance: ' + '__near=(GeoPoint(52.12, 22.12), Distance(kilometers=100))') - return value.to_native() + query_dict = value[0].to_native() + query_dict.update(value[1].to_native()) + + return query_dict def to_python(self, value): if value is None: return + value = self._process_string_types(value) + + if isinstance(value, GeoPoint): + return value + + latitude, longitude = self._process_value(value) + + if not latitude or not longitude: + raise SyncanoValueError('Expected the `longitude` and `latitude` fields.') + + return GeoPoint(latitude=latitude, longitude=longitude) + + @classmethod + def _process_string_types(cls, value): if isinstance(value, six.string_types): try: - value = json.loads(value) + return json.loads(value) except (ValueError, TypeError): raise SyncanoValueError('Invalid value: can not be parsed.') + return value + @classmethod + def _process_value(cls, value): longitude = None latitude = None @@ -808,12 +791,9 @@ def to_python(self, value): latitude = value[0] longitude = value[1] except IndexError: - raise SyncanoValueError('Can not parse the geo struct.') - - if not longitude or not latitude: - raise SyncanoValueError('Expected the `longitude` and `latitude` fields.') + raise SyncanoValueError('Can not parse the geo point.') - return GeoPoint(latitude=latitude, longitude=longitude) + return latitude, longitude MAPPING = { diff --git a/syncano/models/geo.py b/syncano/models/geo.py new file mode 100644 index 0000000..6855078 --- /dev/null +++ b/syncano/models/geo.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from syncano.exceptions import SyncanoValueError + + +class GeoPoint(object): + + def __init__(self, latitude, longitude): + self.latitude = latitude + self.longitude = longitude + + def __str__(self): + return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) + + def __repr__(self): + return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) + + def to_native(self): + geo_struct_dump = {'latitude': self.latitude, 'longitude': self.longitude} + return geo_struct_dump + + +class Distance(object): + + KILOMETERS = '_in_kilometers' + MILES = '_in_miles' + + def __init__(self, kilometers=None, miles=None): + if kilometers is not None and miles is not None: + raise SyncanoValueError('`kilometers` and `miles` can not be set at the same time.') + + if kilometers is None and miles is None: + raise SyncanoValueError('`kilometers` or `miles` attribute should be specified.') + + self.distance = kilometers or miles + self.unit = self.KILOMETERS if kilometers is not None else self.MILES + + def to_native(self): + return { + 'distance{}'.format(self.unit): self.distance + } diff --git a/tests/integration_test_geo.py b/tests/integration_test_geo.py index c85ab53..685f828 100644 --- a/tests/integration_test_geo.py +++ b/tests/integration_test_geo.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import six -from syncano.models import Class, GeoLookup, GeoPoint, Object +from syncano.exceptions import SyncanoValueError +from syncano.models import Class, Distance, GeoPoint, Object from tests.integration_test import InstanceMixin, IntegrationTest @@ -10,19 +11,19 @@ class GeoPointApiTest(InstanceMixin, IntegrationTest): def setUpClass(cls): super(GeoPointApiTest, cls).setUpClass() - cls.city_model = Class.please.create(instance_name=cls.instance.name, name='city', schema=[ - {"name": "city", "type": "string"}, - {"name": "location", "type": "geopoint", "filter_index": True}, - ]) + cls.city_model = Class.please.create( + instance_name=cls.instance.name, + name='city', + schema=[ + {"name": "city", "type": "string"}, + {"name": "location", "type": "geopoint", "filter_index": True}, + ] + ) - cls.warsaw = Object.please.create(instance_name=cls.instance.name, - class_name='city', location=(52.2240698, 20.9942933), city='Warsaw') - cls.paris = Object.please.create(instance_name=cls.instance.name, - class_name='city', location=(52.4731384, 13.5425588), city='Berlin') - cls.berlin = Object.please.create(instance_name=cls.instance.name, - class_name='city', location=(48.8589101, 2.3125377), city='Paris') - cls.london = Object.please.create(instance_name=cls.instance.name, - class_name='city', city='London') + cls.warsaw = cls.city_model.objects.create(location=(52.2240698, 20.9942933), city='Warsaw') + cls.paris = cls.city_model.objects.create(location=(52.4731384, 13.5425588), city='Berlin') + cls.berlin = cls.city_model.objects.create(location=(48.8589101, 2.3125377), city='Paris') + cls.london = cls.city_model.objects.create(city='London') cls.list_london = ['London'] cls.list_warsaw = ['Warsaw'] @@ -42,8 +43,7 @@ def test_filtering_on_geo_point_near(self): location__near={ "latitude": 52.2297, "longitude": 21.0122, - "distance": distance, - "unit": GeoLookup.KILOMETERS + "kilometers": distance, } ) @@ -56,22 +56,27 @@ def test_filtering_on_geo_point_near_miles(self): location__near={ "latitude": 52.2297, "longitude": 21.0122, - "distance": 10, - "unit": GeoLookup.MILES + "miles": 10, } ) result_list = self._prepare_result_list(objects) self.assertListEqual(result_list, self.list_warsaw) def test_filtering_on_geo_point_near_with_another_syntax(self): - objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( - location__near=GeoLookup(GeoPoint(52.2297, 21.0122), distance=10, unit=GeoLookup.MILES) + objects = self.city_model.objects.filter( + location__near=(GeoPoint(52.2297, 21.0122), Distance(kilometers=10)) + ) + result_list = self._prepare_result_list(objects) + self.assertListEqual(result_list, self.list_warsaw) + + objects = self.city_model.objects.filter( + location__near=(GeoPoint(52.2297, 21.0122), Distance(miles=10)) ) result_list = self._prepare_result_list(objects) self.assertListEqual(result_list, self.list_warsaw) def test_filtering_on_geo_point_exists(self): - objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( + objects = self.city_model.objects.filter( location__exists=True ) @@ -79,7 +84,7 @@ def test_filtering_on_geo_point_exists(self): self.assertListEqual(result_list, self.list_warsaw_berlin_paris) - objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( + objects = self.city_model.objects.filter( location__exists=False ) @@ -87,5 +92,16 @@ def test_filtering_on_geo_point_exists(self): self.assertListEqual(result_list, self.list_london) + def test_distance_fail(self): + with self.assertRaises(SyncanoValueError): + self.city_model.objects.filter( + location__near=(GeoPoint(52.2297, 21.0122), Distance(miles=10, kilometers=20)) + ) + + with self.assertRaises(SyncanoValueError): + self.city_model.objects.filter( + location__near=(GeoPoint(52.2297, 21.0122), Distance()) + ) + def _prepare_result_list(self, objects): return [o.city for o in objects] diff --git a/tests/test_fields.py b/tests/test_fields.py index 68ce868..694a9bd 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -590,3 +590,4 @@ def test_to_python(self): self.field.to_python((52.12, 12.02)) self.field.to_python({'latitude': 52.12, 'longitude': 12.02}) + self.field.to_python(models.GeoPoint(52.12, 12.02)) From 1b4acd1c2cb8ddfa62f3db5ec78af82165c15a3d Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 28 Apr 2016 13:11:33 +0200 Subject: [PATCH 379/558] [LIB-653] correct typo and remove __str__ from GeoPoint --- syncano/models/fields.py | 2 +- syncano/models/geo.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 1973d0e..c0429c3 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -702,7 +702,7 @@ def validate(self, value, model_instance): raise SyncanoValueError('Expected an object') if not isinstance(value, GeoPoint): - raise SyncanoValueError('Expected an GeoPoint') + raise SyncanoValueError('Expected a GeoPoint') def to_native(self, value): if value is None: diff --git a/syncano/models/geo.py b/syncano/models/geo.py index 6855078..3b1e68d 100644 --- a/syncano/models/geo.py +++ b/syncano/models/geo.py @@ -8,9 +8,6 @@ def __init__(self, latitude, longitude): self.latitude = latitude self.longitude = longitude - def __str__(self): - return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) - def __repr__(self): return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) From cf7a1990e508b581ea6c6333dc775f25ff482694 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Thu, 28 Apr 2016 16:03:56 +0200 Subject: [PATCH 380/558] [WIP] Relation supp in LIB --- syncano/models/fields.py | 47 ++++++++++++++++++++++++++++++++ syncano/models/manager_mixins.py | 9 ++++++ 2 files changed, 56 insertions(+) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 12f10f8..e6bc254 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -622,6 +622,7 @@ class SchemaField(JSONField): 'datetime', 'file', 'reference', + 'relation', 'array', 'object', 'geopoint', @@ -745,12 +746,58 @@ def to_python(self, value): return GeoPointStruct(latitude, longitude) +class RelationValidatorMixin(object): + + def validate(self, value, model_instance): + super(RelationValidatorMixin, self).validate(value, model_instance) + self._validate(value) + + @classmethod + def _validate(cls, value): + value = cls._make_list(value) + all_ints = all([isinstance(x, int) for x in value]) + from archetypes import Model + all_objects = all([isinstance(obj, Model) for obj in value]) + object_types = [type(obj) for obj in value] + if len(set(object_types)) != 1: + raise SyncanoValueError("All objects should be the same type.") + + if (all_ints and all_objects) or (not all_ints and not all_objects): + raise SyncanoValueError("List elements should be objects or integers.") + + @classmethod + def _make_list(cls, value): + if not isinstance(value, (list, tuple)): + value = [value] + return value + + +class RelationField(RelationValidatorMixin, WritableField): + + def to_python(self, value): + if isinstance(value, dict) and 'type' in value and 'value' in value: + value = value['value'] + + if not isinstance(value, (list, tuple)): + return [value] + + return value + + def to_native(self, value): + if not isinstance(value, (list, tuple)): + value = [value] + + self._validate(value) + return value + + MAPPING = { 'string': StringField, 'text': StringField, 'file': FileField, 'ref': StringField, 'reference': ReferenceField, + 'relation': RelationField, 'integer': IntegerField, 'float': FloatField, 'boolean': BooleanField, diff --git a/syncano/models/manager_mixins.py b/syncano/models/manager_mixins.py index 7f0e0e7..f574cfd 100644 --- a/syncano/models/manager_mixins.py +++ b/syncano/models/manager_mixins.py @@ -110,3 +110,12 @@ def _check_field_type_for_increment(cls, model, field_name): return True return False + + +class RelationMixin(object): + + def add(self): + pass + + def remove(self): + pass From 6133c2598fffdfc555df3da944e36acf90365dd9 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 12:31:57 +0200 Subject: [PATCH 381/558] [LIB-678] add relation manager with add and remove methods; --- syncano/models/archetypes.py | 3 ++ syncano/models/fields.py | 30 ++------------- syncano/models/relations.py | 71 ++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 26 deletions(-) create mode 100644 syncano/models/relations.py diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index ce42147..984ebde 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -235,6 +235,9 @@ def to_python(self, data): value = data[field_name] setattr(self, field.name, value) + if isinstance(field, fields.RelationField): + setattr(self, "{}_set".format(field_name), field(instance=self, field_name=field_name)) + def to_native(self): """Converts the current instance to raw data which can be serialized to JSON and send to API. diff --git a/syncano/models/fields.py b/syncano/models/fields.py index e6bc254..4d7093b 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -11,6 +11,7 @@ from .manager import SchemaManager from .registry import registry +from .relations import RelationManager, RelationValidatorMixin class JSONToPythonMixin(object): @@ -746,34 +747,11 @@ def to_python(self, value): return GeoPointStruct(latitude, longitude) -class RelationValidatorMixin(object): - - def validate(self, value, model_instance): - super(RelationValidatorMixin, self).validate(value, model_instance) - self._validate(value) - - @classmethod - def _validate(cls, value): - value = cls._make_list(value) - all_ints = all([isinstance(x, int) for x in value]) - from archetypes import Model - all_objects = all([isinstance(obj, Model) for obj in value]) - object_types = [type(obj) for obj in value] - if len(set(object_types)) != 1: - raise SyncanoValueError("All objects should be the same type.") - - if (all_ints and all_objects) or (not all_ints and not all_objects): - raise SyncanoValueError("List elements should be objects or integers.") - - @classmethod - def _make_list(cls, value): - if not isinstance(value, (list, tuple)): - value = [value] - return value - - class RelationField(RelationValidatorMixin, WritableField): + def __call__(self, instance, field_name): + return RelationManager(instance=instance, field_name=field_name) + def to_python(self, value): if isinstance(value, dict) and 'type' in value and 'value' in value: value = value['value'] diff --git a/syncano/models/relations.py b/syncano/models/relations.py new file mode 100644 index 0000000..31b25be --- /dev/null +++ b/syncano/models/relations.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +from syncano.exceptions import SyncanoValueError + + +class RelationValidatorMixin(object): + + def validate(self, value, model_instance): + super(RelationValidatorMixin, self).validate(value, model_instance) + self._validate(value) + + @classmethod + def _validate(cls, value): + value = cls._make_list(value) + all_ints = all([isinstance(x, int) for x in value]) + from archetypes import Model + all_objects = all([isinstance(obj, Model) for obj in value]) + object_types = [type(obj) for obj in value] + if len(set(object_types)) != 1: + raise SyncanoValueError("All objects should be the same type.") + + if (all_ints and all_objects) or (not all_ints and not all_objects): + raise SyncanoValueError("List elements should be objects or integers.") + + if all_objects: + return True + return False + + @classmethod + def _make_list(cls, value): + if not isinstance(value, (list, tuple)): + value = [value] + return value + + +class RelationManager(RelationValidatorMixin): + + def __init__(self, instance, field_name): + super(RelationManager, self).__init__() + self.instance = instance + self.model = instance._meta + self.field_name = field_name + + def add(self, *args): + if self._validate(args): + value_ids = [obj.id for obj in args] + else: + value_ids = args + + connection = self.instance._meta.connection + + data = {self.field_name: {'_add': value_ids}} + meta = self.instance._meta + update_path = meta.get_endpoint(name='detail')['path'] + update_path = update_path.format(**self.instance.get_endpoint_data()) + response = connection.request('PATCH', update_path, data=data) + self.instance.to_python(response) + + def remove(self, *args): + if self._validate(args): + value_ids = [obj.id for obj in args] + else: + value_ids = args + + connection = self.instance._meta.connection + + data = {self.field_name: {'_remove': value_ids}} + meta = self.instance._meta + update_path = meta.get_endpoint(name='detail')['path'] + update_path = update_path.format(**self.instance.get_endpoint_data()) + response = connection.request('PATCH', update_path, data=data) + self.instance.to_python(response) From 604a75831e5f13efaabc67ff9d1aa388164d82d9 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 13:21:09 +0200 Subject: [PATCH 382/558] [LIB-678] add integration test for relations --- syncano/models/fields.py | 3 +- tests/integration_test_relations.py | 59 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 tests/integration_test_relations.py diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 4d7093b..4f44412 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -765,7 +765,8 @@ def to_native(self, value): if not isinstance(value, (list, tuple)): value = [value] - self._validate(value) + if self._validate(value): + value = [obj.id for obj in value] return value diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py new file mode 100644 index 0000000..9b6e3a2 --- /dev/null +++ b/tests/integration_test_relations.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from syncano.models import Class +from tests.integration_test import InstanceMixin, IntegrationTest + + +class ResponseTemplateApiTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(ResponseTemplateApiTest, cls).setUpClass() + + # prapare data + cls.author = Class.please.create(name="author", schema=[ + {"name": "name", "type": "string", "filter_index": True}, + {"name": "birthday", "type": "integer"}, + ]) + + cls.book = Class.please.create(name="book", schema=[ + {"name": "title", "type": "string", "filter_index": True}, + {"name": "authors", "type": "relation", "target": "author"}, + ]) + + cls.prus = cls.author.objects.create(name='Bolesław Prus', birthday=1847) + cls.lem = cls.author.objects.create(name='Stanisław Lem', birthday=1921) + cls.coehlo = cls.author.objects.create(name='Paulo Coehlo', birthday=1947) + + cls.lalka = cls.book.objects.create(authors=[cls.prus.id], title='Lalka') + cls.niezwyciezony = cls.book.objects.create(authors=[cls.lem.id], title='Niezwyciężony') + cls.brida = cls.book.objects.create(authors=[cls.coehlo.id], title='Brida') + + def test_integers_list(self): + authors_list_ids = [self.prus.id, self.coehlo.id] + book = self.book.object.create(authors=authors_list_ids, title='Strange title') + self.assertListEqual(book.authors, authors_list_ids) + + def test_object_list(self): + authors_list_ids = [self.prus, self.coehlo] + book = self.book.object.create(authors=authors_list_ids, title='Strange title') + self.assertListEqual(book.authors, authors_list_ids) + + def test_object_assign(self): + self.lalka.authors = [self.lem, self.coehlo] + self.lalka.save() + + self.assertListEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) + + def test_related_field_add(self): + self.lalka.authors_set.add(self.coehlo) + self.assertListEqual(self.lalka.authors, [self.prus.id, self.coehlo.id]) + + self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) + self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) + + def test_related_field_remove(self): + self.lalka.authors_set.remove(self.prus) + self.assertListEqual(self.lalka.authors, []) + + self.lalka.authors_set.remove(self.prus, self.lem, self.coehlo) + self.assertListEqual(self.lalka.authors, [self.prus.id, self.lem.id, self.coehlo.id]) From 720852a396ae5065d4680d7746f7859a5eaf35df Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 13:27:54 +0200 Subject: [PATCH 383/558] [LIB-678] add integration test for relations --- tests/integration_test_relations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 9b6e3a2..d54ab67 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -30,12 +30,12 @@ def setUpClass(cls): def test_integers_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] - book = self.book.object.create(authors=authors_list_ids, title='Strange title') + book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(book.authors, authors_list_ids) def test_object_list(self): authors_list_ids = [self.prus, self.coehlo] - book = self.book.object.create(authors=authors_list_ids, title='Strange title') + book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(book.authors, authors_list_ids) def test_object_assign(self): From 3d0bc216ac2621614897196bdc5b7780f1260424 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 14:40:11 +0200 Subject: [PATCH 384/558] [LIB-678] add integration test for relations --- syncano/models/relations.py | 2 +- tests/integration_test_relations.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/syncano/models/relations.py b/syncano/models/relations.py index 31b25be..03a4e33 100644 --- a/syncano/models/relations.py +++ b/syncano/models/relations.py @@ -12,7 +12,7 @@ def validate(self, value, model_instance): def _validate(cls, value): value = cls._make_list(value) all_ints = all([isinstance(x, int) for x in value]) - from archetypes import Model + from .archetypes import Model all_objects = all([isinstance(obj, Model) for obj in value]) object_types = [type(obj) for obj in value] if len(set(object_types)) != 1: diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index d54ab67..4c46a51 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -34,7 +34,7 @@ def test_integers_list(self): self.assertListEqual(book.authors, authors_list_ids) def test_object_list(self): - authors_list_ids = [self.prus, self.coehlo] + authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(book.authors, authors_list_ids) @@ -45,15 +45,15 @@ def test_object_assign(self): self.assertListEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) def test_related_field_add(self): - self.lalka.authors_set.add(self.coehlo) - self.assertListEqual(self.lalka.authors, [self.prus.id, self.coehlo.id]) + self.niezwyciezony.authors_set.add(self.coehlo) + self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) def test_related_field_remove(self): - self.lalka.authors_set.remove(self.prus) - self.assertListEqual(self.lalka.authors, []) + self.brida.authors_set.remove(self.coehlo) + self.assertListEqual(self.brida.authors, []) - self.lalka.authors_set.remove(self.prus, self.lem, self.coehlo) - self.assertListEqual(self.lalka.authors, [self.prus.id, self.lem.id, self.coehlo.id]) + self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) + self.assertListEqual(self.niezwyciezony.authors, []) From 6a02d4507213b0b3677a826126f90589e0381a7b Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 14:58:54 +0200 Subject: [PATCH 385/558] [LIB-678] correct test and to_naive and to_python methods --- syncano/models/fields.py | 6 ++++++ tests/integration_test_relations.py | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 4f44412..3bf787f 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -753,6 +753,9 @@ def __call__(self, instance, field_name): return RelationManager(instance=instance, field_name=field_name) def to_python(self, value): + if not value: + return None + if isinstance(value, dict) and 'type' in value and 'value' in value: value = value['value'] @@ -762,6 +765,9 @@ def to_python(self, value): return value def to_native(self, value): + if not value: + return None + if not isinstance(value, (list, tuple)): value = [value] diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 4c46a51..8815876 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -31,29 +31,29 @@ def setUpClass(cls): def test_integers_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertListEqual(book.authors, authors_list_ids) + self.assertItemsEqual(book.authors, authors_list_ids) def test_object_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertListEqual(book.authors, authors_list_ids) + self.assertItemsEqual(book.authors, authors_list_ids) def test_object_assign(self): self.lalka.authors = [self.lem, self.coehlo] self.lalka.save() - self.assertListEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) + self.assertItemsEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.coehlo) - self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) + self.assertItemsEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) - self.assertListEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) + self.assertItemsEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) def test_related_field_remove(self): self.brida.authors_set.remove(self.coehlo) - self.assertListEqual(self.brida.authors, []) + self.assertItemsEqual(self.brida.authors, None) self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) - self.assertListEqual(self.niezwyciezony.authors, []) + self.assertItemsEqual(self.niezwyciezony.authors, None) From 9719122d8694db881dfae008324af4c8201dff56 Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 15:09:07 +0200 Subject: [PATCH 386/558] [LIB-678] correct test and to_naive and to_python methods --- tests/integration_test_relations.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 8815876..f84bb8a 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -31,29 +31,29 @@ def setUpClass(cls): def test_integers_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertItemsEqual(book.authors, authors_list_ids) + self.assertCountEqual(book.authors, authors_list_ids) def test_object_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertItemsEqual(book.authors, authors_list_ids) + self.assertCountEqual(book.authors, authors_list_ids) def test_object_assign(self): self.lalka.authors = [self.lem, self.coehlo] self.lalka.save() - self.assertItemsEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) + self.assertCountEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.coehlo) - self.assertItemsEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) + self.assertCountEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) - self.assertItemsEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) + self.assertCountEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) def test_related_field_remove(self): self.brida.authors_set.remove(self.coehlo) - self.assertItemsEqual(self.brida.authors, None) + self.assertEqual(self.brida.authors, None) self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) - self.assertItemsEqual(self.niezwyciezony.authors, None) + self.assertEqual(self.niezwyciezony.authors, None) From b13d164d92c888e539799ceee77eb97aa512b26e Mon Sep 17 00:00:00 2001 From: Sebastian Opalczynski Date: Fri, 29 Apr 2016 15:21:21 +0200 Subject: [PATCH 387/558] [LIB-678] correct test and to_naive and to_python methods --- tests/integration_test_relations.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index f84bb8a..ec3d2e9 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -31,25 +31,25 @@ def setUpClass(cls): def test_integers_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertCountEqual(book.authors, authors_list_ids) + self.assertListEqual(sorted(book.authors), sorted(authors_list_ids)) def test_object_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') - self.assertCountEqual(book.authors, authors_list_ids) + self.assertListEqual(sorted(book.authors), sorted(authors_list_ids)) def test_object_assign(self): self.lalka.authors = [self.lem, self.coehlo] self.lalka.save() - self.assertCountEqual(self.lalka.authors, [self.lem.id, self.coehlo.id]) + self.assertListEqual(sorted(self.lalka.authors), sorted([self.lem.id, self.coehlo.id])) def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.coehlo) - self.assertCountEqual(self.niezwyciezony.authors, [self.lem.id, self.coehlo.id]) + self.assertListEqual(sorted(self.niezwyciezony.authors), sorted([self.lem.id, self.coehlo.id])) self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) - self.assertCountEqual(self.niezwyciezony.authors, [self.lem.id, self.prus.id, self.coehlo.id]) + self.assertListEqual(sorted(self.niezwyciezony.authors), sorted([self.lem.id, self.prus.id, self.coehlo.id])) def test_related_field_remove(self): self.brida.authors_set.remove(self.coehlo) From 023fe00d7bec8a2c3f8bc795d04d6e557e652067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 3 May 2016 11:07:59 +0200 Subject: [PATCH 388/558] [LIB-678] remove uneeded mixin and make code more DRY; --- syncano/models/manager_mixins.py | 9 --------- syncano/models/relations.py | 29 ++++++++++------------------- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/syncano/models/manager_mixins.py b/syncano/models/manager_mixins.py index f574cfd..7f0e0e7 100644 --- a/syncano/models/manager_mixins.py +++ b/syncano/models/manager_mixins.py @@ -110,12 +110,3 @@ def _check_field_type_for_increment(cls, model, field_name): return True return False - - -class RelationMixin(object): - - def add(self): - pass - - def remove(self): - pass diff --git a/syncano/models/relations.py b/syncano/models/relations.py index 03a4e33..2307d6e 100644 --- a/syncano/models/relations.py +++ b/syncano/models/relations.py @@ -41,30 +41,21 @@ def __init__(self, instance, field_name): self.field_name = field_name def add(self, *args): - if self._validate(args): - value_ids = [obj.id for obj in args] - else: - value_ids = args - - connection = self.instance._meta.connection - - data = {self.field_name: {'_add': value_ids}} - meta = self.instance._meta - update_path = meta.get_endpoint(name='detail')['path'] - update_path = update_path.format(**self.instance.get_endpoint_data()) - response = connection.request('PATCH', update_path, data=data) - self.instance.to_python(response) + self._add_or_remove(args) def remove(self, *args): - if self._validate(args): - value_ids = [obj.id for obj in args] - else: - value_ids = args + self._add_or_remove(args, operation='_remove') - connection = self.instance._meta.connection + def _add_or_remove(self, id_list, operation='_add'): + if self._validate(id_list): + value_ids = [obj.id for obj in id_list] + else: + value_ids = id_list - data = {self.field_name: {'_remove': value_ids}} meta = self.instance._meta + connection = meta.connection + + data = {self.field_name: {operation: value_ids}} update_path = meta.get_endpoint(name='detail')['path'] update_path = update_path.format(**self.instance.get_endpoint_data()) response = connection.request('PATCH', update_path, data=data) From 6a71c83debcd76fb48e69ae74308903e0334d89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 16:00:31 +0200 Subject: [PATCH 389/558] [LIB-678] start to implement filters; --- syncano/models/fields.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 3bf787f..2c0a659 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -748,6 +748,7 @@ def to_python(self, value): class RelationField(RelationValidatorMixin, WritableField): + query_allowed = True def __call__(self, instance, field_name): return RelationManager(instance=instance, field_name=field_name) @@ -764,6 +765,9 @@ def to_python(self, value): return value + def to_query(self, value, lookup_type): + pass + def to_native(self, value): if not value: return None From d8768d59852acec3e89be74a8484b1d716721a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:14:08 +0200 Subject: [PATCH 390/558] [LIB-678] add contains and is filtering on relation field --- syncano/models/fields.py | 27 ++++++++++++--- syncano/models/manager.py | 54 ++++++++++++++++++++++------- tests/integration_test_relations.py | 19 ++++++++++ 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 2980d8b..944bed1 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -138,7 +138,7 @@ def to_native(self, value): """ return value - def to_query(self, value, lookup_type): + def to_query(self, value, lookup_type, **kwargs): """ Returns field's value prepared for usage in HTTP request query. """ @@ -725,11 +725,11 @@ def to_native(self, value): return geo_struct - def to_query(self, value, lookup_type): + def to_query(self, value, lookup_type, **kwargs): """ Returns field's value prepared for usage in HTTP request query. """ - super(GeoPointField, self).to_query(value, lookup_type) + super(GeoPointField, self).to_query(value, lookup_type, **kwargs) if lookup_type not in ['near', 'exists']: raise SyncanoValueError('Lookup {} not supported for geopoint field'.format(lookup_type)) @@ -816,8 +816,25 @@ def to_python(self, value): return value - def to_query(self, value, lookup_type): - pass + def to_query(self, value, lookup_type, related_field_name=None, related_field_lookup=None, **kwargs): + + if not self.query_allowed: + raise self.ValidationError('Query on this field is not supported.') + + if lookup_type not in ['contains', 'is']: + raise SyncanoValueError('Lookup {} not supported for relation field.'.format(lookup_type)) + + query_dict = {} + + if lookup_type == 'contains': + if self._validate(value): + value = [obj.id for obj in value] + query_dict = value + + if lookup_type == 'is': + query_dict = {related_field_name: {"_{0}".format(related_field_lookup): value}} + + return query_dict def to_native(self, value): if not value: diff --git a/syncano/models/manager.py b/syncano/models/manager.py index cd72e0f..c3dd04e 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -882,7 +882,8 @@ class for :class:`~syncano.models.base.Object` model. LOOKUP_SEPARATOR = '__' ALLOWED_LOOKUPS = [ 'gt', 'gte', 'lt', 'lte', - 'eq', 'neq', 'exists', 'in', 'near' + 'eq', 'neq', 'exists', 'in', 'startswith', + 'near', 'is', 'contains', ] def __init__(self): @@ -955,28 +956,57 @@ def filter(self, **kwargs): lookup = 'eq' if self.LOOKUP_SEPARATOR in field_name: - field_name, lookup = field_name.split(self.LOOKUP_SEPARATOR, 1) - - if field_name not in model._meta.field_names: - allowed = ', '.join(model._meta.field_names) - raise SyncanoValueError('Invalid field name "{0}" allowed are {1}.'.format(field_name, allowed)) - - if lookup not in self.ALLOWED_LOOKUPS: - allowed = ', '.join(self.ALLOWED_LOOKUPS) - raise SyncanoValueError('Invalid lookup type "{0}" allowed are {1}.'.format(lookup, allowed)) + model_name, field_name, lookup = self._get_lookup_attributes(field_name) for field in model._meta.fields: if field.name == field_name: break - query.setdefault(field_name, {}) - query[field_name]['_{0}'.format(lookup)] = field.to_query(value, lookup) + self._validate_lookup(model, model_name, field_name, lookup, field) + + query_main_lookup, query_main_field = self._get_main_lookup(model_name, field_name, lookup) + + query.setdefault(query_main_field, {}) + query[query_main_field]['_{0}'.format(query_main_lookup)] = field.to_query( + value, + query_main_lookup, + related_field_name=field_name, + related_field_lookup=lookup, + ) self.query['query'] = json.dumps(query) self.method = 'GET' self.endpoint = 'list' return self + def _get_lookup_attributes(self, field_name): + try: + model_name, field_name, lookup = field_name.split(self.LOOKUP_SEPARATOR, 2) + except ValueError: + model_name = None + field_name, lookup = field_name.split(self.LOOKUP_SEPARATOR, 1) + + return model_name, field_name, lookup + + def _validate_lookup(self, model, model_name, field_name, lookup, field): + if not model_name and field_name not in model._meta.field_names: + allowed = ', '.join(model._meta.field_names) + raise SyncanoValueError('Invalid field name "{0}" allowed are {1}.'.format(field_name, allowed)) + + if lookup not in self.ALLOWED_LOOKUPS: + allowed = ', '.join(self.ALLOWED_LOOKUPS) + raise SyncanoValueError('Invalid lookup type "{0}" allowed are {1}.'.format(lookup, allowed)) + + if model_name and field.__class__.__name__ != 'RelationField': + raise SyncanoValueError('Lookup supported only for RelationField.') + + @classmethod + def _get_main_lookup(cls, model_name, field_name, lookup): + if model_name: + return 'is', model_name + else: + return lookup, field_name + def bulk_create(self, *objects): """ Creates many new objects. diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index ec3d2e9..13f9c24 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -57,3 +57,22 @@ def test_related_field_remove(self): self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) self.assertEqual(self.niezwyciezony.authors, None) + + def test_related_field_lookup_contains(self): + filtered_books = self.book.objects.list().filter(author__contains=[self.prus]) + + self.assertEqual(len(filtered_books), 1) + + for book in filtered_books: + self.assertEqual(book.title, self.lalka.title) + + def test_related_field_lookup_contains_fail(self): + filtered_books = self.book.objects.list().filter(author__contains=[self.prus, self.lem]) + self.assertEqual(len(filtered_books), 0) + + def test_related_field_lookup_is(self): + filtered_books = self.book.objects.list().filter(author__name__startswith='Stan') + + self.assertEqual(len(filtered_books), 1) + for book in filtered_books: + self.assertEqual(book.title, self.niezwyciezony.title) From 3994735bc63ca36dd23d43eb8855b89dc5b97dae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:16:00 +0200 Subject: [PATCH 391/558] [LIB-678] add filter index in test correct spell error - in names --- tests/integration_test_relations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 13f9c24..7b9672d 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -17,7 +17,7 @@ def setUpClass(cls): cls.book = Class.please.create(name="book", schema=[ {"name": "title", "type": "string", "filter_index": True}, - {"name": "authors", "type": "relation", "target": "author"}, + {"name": "authors", "type": "relation", "target": "author", "filter_index": True}, ]) cls.prus = cls.author.objects.create(name='Bolesław Prus', birthday=1847) @@ -59,7 +59,7 @@ def test_related_field_remove(self): self.assertEqual(self.niezwyciezony.authors, None) def test_related_field_lookup_contains(self): - filtered_books = self.book.objects.list().filter(author__contains=[self.prus]) + filtered_books = self.book.objects.list().filter(authors__contains=[self.prus]) self.assertEqual(len(filtered_books), 1) @@ -67,11 +67,11 @@ def test_related_field_lookup_contains(self): self.assertEqual(book.title, self.lalka.title) def test_related_field_lookup_contains_fail(self): - filtered_books = self.book.objects.list().filter(author__contains=[self.prus, self.lem]) + filtered_books = self.book.objects.list().filter(authors__contains=[self.prus, self.lem]) self.assertEqual(len(filtered_books), 0) def test_related_field_lookup_is(self): - filtered_books = self.book.objects.list().filter(author__name__startswith='Stan') + filtered_books = self.book.objects.list().filter(authors__name__startswith='Stan') self.assertEqual(len(filtered_books), 1) for book in filtered_books: From 25db2dec73b519e0d0a8b594cf7a0cd17c3de63c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:25:48 +0200 Subject: [PATCH 392/558] [LIB-678] correct model_name initialization --- syncano/models/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index c3dd04e..8ce1606 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -954,6 +954,7 @@ def filter(self, **kwargs): for field_name, value in six.iteritems(kwargs): lookup = 'eq' + model_name = None if self.LOOKUP_SEPARATOR in field_name: model_name, field_name, lookup = self._get_lookup_attributes(field_name) From fbfb410dc233c9fb584e75689b2fcde871abc8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:32:58 +0200 Subject: [PATCH 393/558] [LIB-678] add list() in tests asserts --- tests/integration_test_relations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 7b9672d..2ae0a85 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -61,18 +61,18 @@ def test_related_field_remove(self): def test_related_field_lookup_contains(self): filtered_books = self.book.objects.list().filter(authors__contains=[self.prus]) - self.assertEqual(len(filtered_books), 1) + self.assertEqual(len(list(filtered_books)), 1) for book in filtered_books: self.assertEqual(book.title, self.lalka.title) def test_related_field_lookup_contains_fail(self): filtered_books = self.book.objects.list().filter(authors__contains=[self.prus, self.lem]) - self.assertEqual(len(filtered_books), 0) + self.assertEqual(len(list(filtered_books)), 0) def test_related_field_lookup_is(self): filtered_books = self.book.objects.list().filter(authors__name__startswith='Stan') - self.assertEqual(len(filtered_books), 1) + self.assertEqual(len(list(filtered_books)), 1) for book in filtered_books: self.assertEqual(book.title, self.niezwyciezony.title) From 81a5e3e3a65303f61c6239fd55b5a51bd9054871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 4 May 2016 17:44:46 +0200 Subject: [PATCH 394/558] [LIB-678] clean up test a little --- tests/integration_test_relations.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 2ae0a85..3bfac92 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -33,11 +33,15 @@ def test_integers_list(self): book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(sorted(book.authors), sorted(authors_list_ids)) + book.delete() + def test_object_list(self): authors_list_ids = [self.prus.id, self.coehlo.id] book = self.book.objects.create(authors=authors_list_ids, title='Strange title') self.assertListEqual(sorted(book.authors), sorted(authors_list_ids)) + book.delete() + def test_object_assign(self): self.lalka.authors = [self.lem, self.coehlo] self.lalka.save() @@ -51,6 +55,9 @@ def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.prus.id, self.coehlo.id) self.assertListEqual(sorted(self.niezwyciezony.authors), sorted([self.lem.id, self.prus.id, self.coehlo.id])) + self.niezwyciezony.authors = [self.lem] + self.niezwyciezony.save() + def test_related_field_remove(self): self.brida.authors_set.remove(self.coehlo) self.assertEqual(self.brida.authors, None) @@ -58,6 +65,9 @@ def test_related_field_remove(self): self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) self.assertEqual(self.niezwyciezony.authors, None) + self.niezwyciezony.authors_set.add(self.lem) + self.brida.authors_set.add(self.coehlo) + def test_related_field_lookup_contains(self): filtered_books = self.book.objects.list().filter(authors__contains=[self.prus]) From 8175bd60e772b00a9f08ffe843cbfa6687a3335f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 5 May 2016 01:09:04 +0200 Subject: [PATCH 395/558] [LIB-678] correct tests --- tests/integration_test_relations.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 3bfac92..37348b6 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -47,6 +47,8 @@ def test_object_assign(self): self.lalka.save() self.assertListEqual(sorted(self.lalka.authors), sorted([self.lem.id, self.coehlo.id])) + self.lalka.authors = [self.prus] + self.lalka.save() def test_related_field_add(self): self.niezwyciezony.authors_set.add(self.coehlo) @@ -65,8 +67,10 @@ def test_related_field_remove(self): self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) self.assertEqual(self.niezwyciezony.authors, None) - self.niezwyciezony.authors_set.add(self.lem) - self.brida.authors_set.add(self.coehlo) + self.niezwyciezony.authors = [self.lem] + self.niezwyciezony.save() + self.brida.authors = [self.coehlo] + self.brida.save() def test_related_field_lookup_contains(self): filtered_books = self.book.objects.list().filter(authors__contains=[self.prus]) From 4cf264436800993cf02604a4fd2a75e0ea93436b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 10 May 2016 12:58:00 +0200 Subject: [PATCH 396/558] [LIB-678-fixes] relation - field handling fixes --- syncano/models/fields.py | 10 ++++++++-- syncano/models/relations.py | 23 ++++++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 944bed1..564ec19 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -811,6 +811,9 @@ def to_python(self, value): if isinstance(value, dict) and 'type' in value and 'value' in value: value = value['value'] + if isinstance(value, dict) and ('_add' in value or '_remove' in value): + return value + if not isinstance(value, (list, tuple)): return [value] @@ -827,7 +830,7 @@ def to_query(self, value, lookup_type, related_field_name=None, related_field_lo query_dict = {} if lookup_type == 'contains': - if self._validate(value): + if self._check_relation_value(value): value = [obj.id for obj in value] query_dict = value @@ -840,10 +843,13 @@ def to_native(self, value): if not value: return None + if isinstance(value, dict) and ('_add' in value or '_remove' in value): + return value + if not isinstance(value, (list, tuple)): value = [value] - if self._validate(value): + if self._check_relation_value(value): value = [obj.id for obj in value] return value diff --git a/syncano/models/relations.py b/syncano/models/relations.py index 2307d6e..1c23bd6 100644 --- a/syncano/models/relations.py +++ b/syncano/models/relations.py @@ -6,15 +6,24 @@ class RelationValidatorMixin(object): def validate(self, value, model_instance): super(RelationValidatorMixin, self).validate(value, model_instance) - self._validate(value) + self._check_relation_value(value) @classmethod - def _validate(cls, value): - value = cls._make_list(value) - all_ints = all([isinstance(x, int) for x in value]) + def _check_relation_value(cls, value): + if value is None: + return False + + if '_add' in value or '_remove' in value: + check_value = value.get('_add') or value.get('_remove') + else: + check_value = value + + check_value = cls._make_list(check_value) + + all_ints = all([isinstance(x, int) for x in check_value]) from .archetypes import Model - all_objects = all([isinstance(obj, Model) for obj in value]) - object_types = [type(obj) for obj in value] + all_objects = all([isinstance(obj, Model) for obj in check_value]) + object_types = [type(obj) for obj in check_value] if len(set(object_types)) != 1: raise SyncanoValueError("All objects should be the same type.") @@ -47,7 +56,7 @@ def remove(self, *args): self._add_or_remove(args, operation='_remove') def _add_or_remove(self, id_list, operation='_add'): - if self._validate(id_list): + if self._check_relation_value(id_list): value_ids = [obj.id for obj in id_list] else: value_ids = id_list From f550ba56392dacf591cf4a6bc23be3d9661aa3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 11 May 2016 14:28:46 +0200 Subject: [PATCH 397/558] [LIB-701] make schema field not required on class --- syncano/models/classes.py | 2 +- syncano/models/fields.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index 63ddc42..e710d82 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -47,7 +47,7 @@ class Class(Model): description = fields.StringField(read_only=False, required=False) objects_count = fields.Field(read_only=True, required=False) - schema = fields.SchemaField(read_only=False, required=True) + schema = fields.SchemaField(read_only=False) links = fields.LinksField() status = fields.Field() metadata = fields.JSONField(read_only=False, required=False) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 564ec19..cdbb7fb 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -600,6 +600,7 @@ def validate(self, value, model_instance): class SchemaField(JSONField): + required = False query_allowed = False not_indexable_types = ['text', 'file'] schema = { @@ -646,6 +647,9 @@ class SchemaField(JSONField): } def validate(self, value, model_instance): + if value is None: + return + if isinstance(value, SchemaManager): value = value.schema From eb6ac49df2ca9f6d6445eaaf759c0fff28cbd7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 11 May 2016 15:51:42 +0200 Subject: [PATCH 398/558] [LIB-701] change log leve when read only attribute is set --- syncano/models/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index cdbb7fb..f924267 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -98,8 +98,8 @@ def __get__(self, instance, owner): def __set__(self, instance, value): if self.read_only and value and instance._raw_data.get(self.name): - logger.warning('Field "{0}"" is read only, ' - 'your changes will not be saved.'.format(self.name)) + logger.debug('Field "{0}"" is read only, ' + 'your changes will not be saved.'.format(self.name)) instance._raw_data[self.name] = self.to_python(value) From 0a3e2df8881081519e3f8108240941ae5b750d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 11 May 2016 16:39:02 +0200 Subject: [PATCH 399/558] [LIB-701] remove choices from Script runtime name, add get_runtimes to Script manager; --- syncano/models/incentives.py | 13 +++++-------- syncano/models/manager.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index c48b648..a93f03f 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -38,17 +38,10 @@ class Script(Model): >>> s.run(variable_one=1, variable_two=2) """ - RUNTIME_CHOICES = ( - {'display_name': 'nodejs', 'value': 'nodejs'}, - {'display_name': 'python', 'value': 'python'}, - {'display_name': 'ruby', 'value': 'ruby'}, - {'display_name': 'golang', 'value': 'golang'}, - ) - label = fields.StringField(max_length=80) description = fields.StringField(required=False) source = fields.StringField() - runtime_name = fields.ChoiceField(choices=RUNTIME_CHOICES) + runtime_name = fields.StringField() config = fields.Field(required=False) links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) @@ -75,6 +68,10 @@ class Meta: 'methods': ['post'], 'path': '/snippets/scripts/{id}/run/', }, + 'runtimes': { + 'methods': ['get'], + 'path': '/snippets/scripts/runtimes/' + } } def run(self, **payload): diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 8ce1606..4cf3cf2 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -849,6 +849,37 @@ def run(self, *args, **kwargs): response = self.request() return registry.ScriptTrace(**response) + @clone + def get_runtimes(self): + """ + Method which get available runtimes from CORE; + :return: + """ + self.method = 'GET' + self.endpoint = 'runtimes' + self._serialize = False + response = self.request() + + class RuntimeChoices(object): + + def __init__(self, choices): + self._choices = {} + for choice in choices: + self._choices[choice.upper()] = choice + setattr(self, choice.upper(), choice) + + def __repr__(self): + repr = "" + for key, value in six.iteritems(self._choices): + repr += '{name}.{attr} = "{value}"\n'.format( + name=self.__class__.__name__, + attr=key, + value=value + ) + return repr + + return RuntimeChoices(choices=response.keys()) + class ScriptEndpointManager(Manager): """ From 5beb45d51ccc34966b9b3229b56be8fb02e41ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 12 May 2016 09:56:54 +0200 Subject: [PATCH 400/558] [LIB-701] small improevements --- syncano/models/manager.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 4cf3cf2..b6c9c95 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -862,21 +862,22 @@ def get_runtimes(self): class RuntimeChoices(object): + _choices = {} + def __init__(self, choices): - self._choices = {} for choice in choices: self._choices[choice.upper()] = choice setattr(self, choice.upper(), choice) def __repr__(self): - repr = "" + repr_str = "" for key, value in six.iteritems(self._choices): - repr += '{name}.{attr} = "{value}"\n'.format( + repr_str += '{name}.{attr} = "{value}"\n'.format( name=self.__class__.__name__, attr=key, value=value ) - return repr + return repr_str return RuntimeChoices(choices=response.keys()) From 8494dd043d62ed1b15e8c38fbe350f2d4564eb4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 12 May 2016 10:03:18 +0200 Subject: [PATCH 401/558] [LIB-701] change config field in Script to JSONField --- syncano/models/incentives.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index a93f03f..d640198 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -42,7 +42,7 @@ class Script(Model): description = fields.StringField(required=False) source = fields.StringField() runtime_name = fields.StringField() - config = fields.Field(required=False) + config = fields.JSONField(required=False) links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) From 15bed9a002e0f688fa612c8339ba3ec921702bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 12 May 2016 10:16:34 +0200 Subject: [PATCH 402/558] [LIB-701] remove not needed print --- syncano/models/push_notification.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index bdaa3e8..8dc7f43 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -31,7 +31,6 @@ def send_message(self, content): :param contet: Message content structure - object like; :return: """ - print(self.links.links_dict) send_message_path = self.links.send_message data = { 'content': content From ecddd5f5b0fcf614e48f168337381649623872e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 12 May 2016 10:30:17 +0200 Subject: [PATCH 403/558] [LIB-701] correct Runtime object setting --- syncano/models/manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index b6c9c95..4b0ba79 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -866,8 +866,9 @@ class RuntimeChoices(object): def __init__(self, choices): for choice in choices: - self._choices[choice.upper()] = choice - setattr(self, choice.upper(), choice) + runtime_name = choice.upper().replace('.', '_') + self._choices[runtime_name] = choice + setattr(self, runtime_name, choice) def __repr__(self): repr_str = "" From 0adb68e4e66fbd3372093c851d464f239610fa35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 12 May 2016 11:29:36 +0200 Subject: [PATCH 404/558] [LIB-701] small fixes after QA --- syncano/models/manager.py | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 4b0ba79..f1cfc58 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -828,6 +828,26 @@ def _get_endpoint_properties(self): return self.model._meta.resolve_endpoint(self.endpoint, defaults), defaults +class RuntimeChoices(object): + _choices = {} + + def __init__(self, choices): + for choice in choices: + runtime_name = choice.upper().replace('.', '_') + self._choices[runtime_name] = choice + setattr(self, runtime_name, choice) + + def __repr__(self): + repr_str = "" + for key, value in six.iteritems(self._choices): + repr_str += '{name}.{attr} = "{value}"\n'.format( + name=self.__class__.__name__, + attr=key, + value=value + ) + return repr_str + + class ScriptManager(Manager): """ Custom :class:`~syncano.models.manager.Manager` @@ -852,7 +872,7 @@ def run(self, *args, **kwargs): @clone def get_runtimes(self): """ - Method which get available runtimes from CORE; + Method which get available runtimes from Syncano API; :return: """ self.method = 'GET' @@ -860,26 +880,6 @@ def get_runtimes(self): self._serialize = False response = self.request() - class RuntimeChoices(object): - - _choices = {} - - def __init__(self, choices): - for choice in choices: - runtime_name = choice.upper().replace('.', '_') - self._choices[runtime_name] = choice - setattr(self, runtime_name, choice) - - def __repr__(self): - repr_str = "" - for key, value in six.iteritems(self._choices): - repr_str += '{name}.{attr} = "{value}"\n'.format( - name=self.__class__.__name__, - attr=key, - value=value - ) - return repr_str - return RuntimeChoices(choices=response.keys()) From cf99296731d9e61e8cc3fba9c161ce087c5e1ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 12 May 2016 15:11:00 +0200 Subject: [PATCH 405/558] [LIB-701] add RuntimeChoices constant --- syncano/models/incentives.py | 16 ++++++++++++++++ syncano/models/manager.py | 33 --------------------------------- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index d640198..0b6471f 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -11,6 +11,22 @@ from .mixins import RenameMixin +class RuntimeChoices(object): + """ + Store available Script runtimes; + """ + PYTHON = 'python' + PYTHON_V4_2 = 'python_library_v4.2' # python old library; + PYTHON_V5_0 = 'python_library_v5.0' # python >5.0 library not backward compatible; + NODEJS = 'nodejs' + NODEJS_V0_4 = 'nodejs_library_v0.4' # nodejs old library; + NODEJS_V1_0 = 'nodejs_library_v1.0' # nodejs >1.0 library, not backward compatible; + GOLANG = 'golang' + SWIFT = 'swift' + PHP = 'php' + RUBY = 'ruby' + + class Script(Model): """ OO wrapper around scripts `link `_. diff --git a/syncano/models/manager.py b/syncano/models/manager.py index f1cfc58..8ce1606 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -828,26 +828,6 @@ def _get_endpoint_properties(self): return self.model._meta.resolve_endpoint(self.endpoint, defaults), defaults -class RuntimeChoices(object): - _choices = {} - - def __init__(self, choices): - for choice in choices: - runtime_name = choice.upper().replace('.', '_') - self._choices[runtime_name] = choice - setattr(self, runtime_name, choice) - - def __repr__(self): - repr_str = "" - for key, value in six.iteritems(self._choices): - repr_str += '{name}.{attr} = "{value}"\n'.format( - name=self.__class__.__name__, - attr=key, - value=value - ) - return repr_str - - class ScriptManager(Manager): """ Custom :class:`~syncano.models.manager.Manager` @@ -869,19 +849,6 @@ def run(self, *args, **kwargs): response = self.request() return registry.ScriptTrace(**response) - @clone - def get_runtimes(self): - """ - Method which get available runtimes from Syncano API; - :return: - """ - self.method = 'GET' - self.endpoint = 'runtimes' - self._serialize = False - response = self.request() - - return RuntimeChoices(choices=response.keys()) - class ScriptEndpointManager(Manager): """ From 008cdbd3952bb513d0cb8890dc5b93b4a76bd3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 12 May 2016 15:22:29 +0200 Subject: [PATCH 406/558] [LIB-701] remove runtimes endpoint --- syncano/models/incentives.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 0b6471f..99da749 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -84,10 +84,6 @@ class Meta: 'methods': ['post'], 'path': '/snippets/scripts/{id}/run/', }, - 'runtimes': { - 'methods': ['get'], - 'path': '/snippets/scripts/runtimes/' - } } def run(self, **payload): From bc93492f6cd1d816e32d00832455e144ab4bb64d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 13 May 2016 10:48:07 +0200 Subject: [PATCH 407/558] [RELEASE v5.1.0] next release --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index d728cf6..21ec7f5 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.0.2' +__version__ = '5.1.0' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From caa0529416cd54a2cc31117604e6243b6a6e5ddc Mon Sep 17 00:00:00 2001 From: Mariusz Wisniewski Date: Tue, 17 May 2016 16:35:22 -0400 Subject: [PATCH 408/558] Update README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 84854b4..2744d96 100644 --- a/README.rst +++ b/README.rst @@ -15,8 +15,8 @@ You can also find library reference hosted on GitHub pages [here](http://syncano Backwards incompatible changes ------------------------------ -Version 4.0 is designed for new release of Syncano platform and -it's **not compatible** with any previous releases. +Version 4.x and 5.x is designed for new release of Syncano platform and +is **not compatible** with any previous releases. Code from `0.6.x` release is avalable on [stable/0.6.x](https://github.com/Syncano/syncano-python/tree/stable/0.6.x) branch and it can be installed directly from pip via: From 4f20f4769a81cb197d8cfd16ea4d477772e1bfbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 25 May 2016 11:16:24 +0200 Subject: [PATCH 409/558] [LIB-743] Add string field filtering --- syncano/models/fields.py | 18 +++++++++-- syncano/models/manager.py | 6 ++-- tests/integration_test_string_filtering.py | 35 ++++++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 tests/integration_test_string_filtering.py diff --git a/syncano/models/fields.py b/syncano/models/fields.py index f924267..38e6f18 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -44,6 +44,7 @@ class Field(object): allow_increment = False creation_counter = 0 + field_lookups = [] def __init__(self, name=None, **kwargs): self.name = name @@ -214,6 +215,16 @@ class EndpointField(WritableField): class StringField(WritableField): + field_lookups = [ + 'startswith', + 'endswith', + 'contains', + 'istartswith', + 'iendswith', + 'icontains', + 'ieq', + ] + def to_python(self, value): value = super(StringField, self).to_python(value) @@ -695,6 +706,8 @@ def to_native(self, value): class GeoPointField(Field): + field_lookups = ['near', 'exists'] + def validate(self, value, model_instance): super(GeoPointField, self).validate(value, model_instance) @@ -735,7 +748,7 @@ def to_query(self, value, lookup_type, **kwargs): """ super(GeoPointField, self).to_query(value, lookup_type, **kwargs) - if lookup_type not in ['near', 'exists']: + if lookup_type not in self.field_lookups: raise SyncanoValueError('Lookup {} not supported for geopoint field'.format(lookup_type)) if lookup_type in ['exists']: @@ -804,6 +817,7 @@ def _process_value(cls, value): class RelationField(RelationValidatorMixin, WritableField): query_allowed = True + field_lookups = ['contains', 'is'] def __call__(self, instance, field_name): return RelationManager(instance=instance, field_name=field_name) @@ -828,7 +842,7 @@ def to_query(self, value, lookup_type, related_field_name=None, related_field_lo if not self.query_allowed: raise self.ValidationError('Query on this field is not supported.') - if lookup_type not in ['contains', 'is']: + if lookup_type not in self.field_lookups: raise SyncanoValueError('Lookup {} not supported for relation field.'.format(lookup_type)) query_dict = {} diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 8ce1606..d529796 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -882,7 +882,7 @@ class for :class:`~syncano.models.base.Object` model. LOOKUP_SEPARATOR = '__' ALLOWED_LOOKUPS = [ 'gt', 'gte', 'lt', 'lte', - 'eq', 'neq', 'exists', 'in', 'startswith', + 'eq', 'neq', 'exists', 'in', 'near', 'is', 'contains', ] @@ -994,7 +994,9 @@ def _validate_lookup(self, model, model_name, field_name, lookup, field): allowed = ', '.join(model._meta.field_names) raise SyncanoValueError('Invalid field name "{0}" allowed are {1}.'.format(field_name, allowed)) - if lookup not in self.ALLOWED_LOOKUPS: + allowed_lookups = set(self.ALLOWED_LOOKUPS + field.field_lookups) + + if lookup not in allowed_lookups: allowed = ', '.join(self.ALLOWED_LOOKUPS) raise SyncanoValueError('Invalid lookup type "{0}" allowed are {1}.'.format(lookup, allowed)) diff --git a/tests/integration_test_string_filtering.py b/tests/integration_test_string_filtering.py new file mode 100644 index 0000000..ff7dcaa --- /dev/null +++ b/tests/integration_test_string_filtering.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from syncano.models import Object +from tests.integration_test import InstanceMixin, IntegrationTest + + +class StringFilteringTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(StringFilteringTest, cls).setUpClass() + cls.klass = cls.instance.classes.create(name='class_a', + schema=[{'name': 'title', 'type': 'string', 'filter_index': True}]) + cls.object = cls.klass.objects.create(title='Some great title') + + def _test_filter(self, filter): + filtered_obj = Object.please.list(class_name='class_a').filter( + **filter + ).first() + + self.assertTrue(filtered_obj.id) + + def test_starstwith(self): + self._test_filter({'title__startswith': 'Some'}) + self._test_filter({'title__istartswith': 'omes'}) + + def test_endswith(self): + self._test_filter({'title__endswith': 'tle'}) + self._test_filter({'title__iendswith': 'TLE'}) + + def test_contains(self): + self._test_filter({'title__contains': 'gre'}) + self._test_filter({'title__icontains': 'gRe'}) + + def test_eq(self): + self._test_filter({'title__ieq': 'some gREAt title'}) From 12a61a0d40b1fcd302233ee0e2c1e608c4a751cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 25 May 2016 11:38:36 +0200 Subject: [PATCH 410/558] [LIB-743] corrects after tests --- syncano/models/manager.py | 9 ++++++--- tests/integration_test_string_filtering.py | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index d529796..4281c7e 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -884,6 +884,10 @@ class for :class:`~syncano.models.base.Object` model. 'gt', 'gte', 'lt', 'lte', 'eq', 'neq', 'exists', 'in', 'near', 'is', 'contains', + 'startswith', 'endswith', + 'contains', 'istartswith', + 'iendswith', 'icontains', + 'ieq', 'near', ] def __init__(self): @@ -990,13 +994,12 @@ def _get_lookup_attributes(self, field_name): return model_name, field_name, lookup def _validate_lookup(self, model, model_name, field_name, lookup, field): + if not model_name and field_name not in model._meta.field_names: allowed = ', '.join(model._meta.field_names) raise SyncanoValueError('Invalid field name "{0}" allowed are {1}.'.format(field_name, allowed)) - allowed_lookups = set(self.ALLOWED_LOOKUPS + field.field_lookups) - - if lookup not in allowed_lookups: + if lookup not in self.ALLOWED_LOOKUPS: allowed = ', '.join(self.ALLOWED_LOOKUPS) raise SyncanoValueError('Invalid lookup type "{0}" allowed are {1}.'.format(lookup, allowed)) diff --git a/tests/integration_test_string_filtering.py b/tests/integration_test_string_filtering.py index ff7dcaa..70aba17 100644 --- a/tests/integration_test_string_filtering.py +++ b/tests/integration_test_string_filtering.py @@ -21,7 +21,7 @@ def _test_filter(self, filter): def test_starstwith(self): self._test_filter({'title__startswith': 'Some'}) - self._test_filter({'title__istartswith': 'omes'}) + self._test_filter({'title__istartswith': 'some'}) def test_endswith(self): self._test_filter({'title__endswith': 'tle'}) From 82494c070098e895186e367422a31da28e7aea22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 25 May 2016 14:42:39 +0200 Subject: [PATCH 411/558] [LIB-672] Add mixin fro handling the array fields operations; --- syncano/models/fields.py | 10 +- syncano/models/manager.py | 4 +- syncano/models/manager_mixins.py | 154 +++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 4 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index f924267..4e34e7c 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -572,10 +572,16 @@ def validate(self, value, model_instance): except (ValueError, TypeError): raise SyncanoValueError('Expected an array') - if not isinstance(value, list): + if isinstance(value, dict): + if len(value) != 1 or len(set(value.keys()).intersection(['_add', '_remove', '_addunique'])) != 1: + raise SyncanoValueError('Wrong value: one operation at the time.') + + elif not isinstance(value, list): raise SyncanoValueError('Expected an array') - for element in value: + value_to_check = value if isinstance(value, list) else value.values()[0] + + for element in value_to_check: if not isinstance(element, six.string_types + (bool, int, float)): raise SyncanoValueError( 'Currently supported types for array items are: string types, bool, float and int') diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 8ce1606..a55cc01 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -5,7 +5,7 @@ from syncano.connection import ConnectionMixin from syncano.exceptions import SyncanoRequestError, SyncanoValidationError, SyncanoValueError from syncano.models.bulk import ModelBulkCreate, ObjectBulkCreate -from syncano.models.manager_mixins import IncrementMixin, clone +from syncano.models.manager_mixins import ArrayOperationsMixin, IncrementMixin, clone from .registry import registry @@ -874,7 +874,7 @@ def run(self, *args, **kwargs): return registry.ScriptEndpointTrace(**response) -class ObjectManager(IncrementMixin, Manager): +class ObjectManager(IncrementMixin, ArrayOperationsMixin, Manager): """ Custom :class:`~syncano.models.manager.Manager` class for :class:`~syncano.models.base.Object` model. diff --git a/syncano/models/manager_mixins.py b/syncano/models/manager_mixins.py index 7f0e0e7..91defef 100644 --- a/syncano/models/manager_mixins.py +++ b/syncano/models/manager_mixins.py @@ -110,3 +110,157 @@ def _check_field_type_for_increment(cls, model, field_name): return True return False + + +class ArrayOperationsMixin(object): + + @clone + def add(self, field_name, value, **kwargs): + """ + A manager method that will add a values to the array field. + + Usage:: + + data_object = Object.please.add( + field_name='array', + value=[10], + class_name='arr_test', + id=155 + ) + + Consider example: + + data_object.array = [1] + + after running:: + + data_object = Object.please.add( + field_name='array', + value=[3], + id=data_object.id, + ) + + data_object.array will be equal: [1, 3] + + and after:: + + data_object = Object.please.add( + field_name='array', + value=[1], + id=data_object.id, + ) + + data_object.array will be equal: [1, 3, 1] + + :param field_name: the array field name to which elements will be added; + :param value: the list of values to add; + :param kwargs: class_name and id usually; + :return: the processed data object; + """ + self.properties.update(kwargs) + model = self.model.get_subclass_model(**self.properties) + + self.array_validate(field_name, value, model) + return self.array_process(field_name, value, operation_type='add') + + def remove(self, field_name, value, **kwargs): + """ + A manager method that will remove a values from the array field. + + Usage:: + + data_object = Object.please.remove( + field_name='array', + value=[10], + class_name='arr_test', + id=155 + ) + + :param field_name: the array field name from which elements will be removed; + :param value: the list of values to remove; + :param kwargs: class_name and id usually; + :return: the processed data object; + """ + self.properties.update(kwargs) + model = self.model.get_subclass_model(**self.properties) + + self.array_validate(field_name, value, model) + return self.array_process(field_name, value, operation_type='remove') + + def add_unique(self, field_name, value, **kwargs): + """ + A manager method that will add an unique values to the array field. + + Usage:: + + data_object = Object.please.add_unique( + field_name='array', + value=[10], + class_name='arr_test', + id=155 + ) + + The main distinction between add and add unique is that: add unique will not repeat elements. + Consider example:: + + data_object.array = [1] + + after running:: + + data_object = Object.please.add_unique( + field_name='array', + value=[1], + id=data_object.id, + ) + + data_object.array will be equal: [1] + + But if only add will be run the result will be as follow: + + data_object.array will be equal: [1, 1] + + :param field_name: field_name: the array field name to which elements will be added unique; + :param value: the list of values to add unique; + :param kwargs: class_name and id usually; + :return: the processed data object; + """ + self.properties.update(kwargs) + model = self.model.get_subclass_model(**self.properties) + + self.array_validate(field_name, value, model) + return self.array_process(field_name, value, operation_type='add_unique') + + @classmethod + def array_validate(cls, field_name, value, model): + + fields = {field.name: field for field in model._meta.fields} + if field_name not in fields: + raise SyncanoValueError('Object has not specified field.') + + from syncano.models import ArrayField + if not isinstance(fields[field_name], ArrayField): + raise SyncanoValueError('Field must be of array type') + + if not isinstance(value, list): + raise SyncanoValueError('List of values expected') + + def array_process(self, field_name, value, operation_type, **kwargs): + self.endpoint = 'detail' + self.method = self.get_allowed_method('PATCH', 'PUT', 'POST') + self.data = kwargs.copy() + + if operation_type == 'add': + array_data = {'_add': value} + elif operation_type == 'remove': + array_data = {'_remove': value} + elif operation_type == 'add_unique': + array_data = {'_addunique': value} + else: + raise SyncanoValueError('Operation not supported') + + self.data.update( + {field_name: array_data} + ) + + response = self.request() + return response From c8d516b3a81eba2d92e34df1a10a0a0cd6ee393c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 27 May 2016 16:56:06 +0200 Subject: [PATCH 412/558] [LIB-672] add test for manager methods: add, remove, addunique --- tests/integration_test.py | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/integration_test.py b/tests/integration_test.py index f766b25..fe2a361 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -231,6 +231,7 @@ def setUpClass(cls): {'type': 'float', 'name': 'cost'}, {'type': 'boolean', 'name': 'available'}, {'type': 'datetime', 'name': 'published_at'}, + {'type': 'array', 'name': 'array'}, {'type': 'file', 'name': 'cover'}, {'type': 'reference', 'name': 'author', 'order_index': True, 'filter_index': True, 'target': cls.author.name}, @@ -365,6 +366,60 @@ def test_increment_and_decrement_on_float(self): self.assertEqual(decremented_book.cost, 8.4) + def test_add_array(self): + book = self.model.please.create( + instance_name=self.instance.name, class_name=self.book.name, + name='test', description='test', quantity=10, cost=10.5, + published_at=datetime.now(), available=True, array=[10]) + + book = Object.please.add( + 'array', + [11], + class_name='book', + id=book.id + ) + + self.assertEqual(book.array, [10, 11]) + + def test_remove_array(self): + book = self.model.please.create( + instance_name=self.instance.name, class_name=self.book.name, + name='test', description='test', quantity=10, cost=10.5, + published_at=datetime.now(), available=True, array=[10]) + + book = Object.please.remove( + 'array', + [10], + class_name='book', + id=book.id + ) + + self.assertEqual(book.array, []) + + def test_addunique_array(self): + book = self.model.please.create( + instance_name=self.instance.name, class_name=self.book.name, + name='test', description='test', quantity=10, cost=10.5, + published_at=datetime.now(), available=True, array=[10]) + + book = Object.please.add_unique( + 'array', + [10], + class_name='book', + id=book.id + ) + + self.assertEqual(book.array, [10]) + + book = Object.please.add_unique( + 'array', + [11], + class_name='book', + id=book.id + ) + + self.assertEqual(book.array, [10, 11]) + class ScriptIntegrationTest(InstanceMixin, IntegrationTest): model = Script From b2fc933fef4d018936dd4882a9a45017d92881bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 27 May 2016 17:15:27 +0200 Subject: [PATCH 413/558] [LIB-672] add test for manager methods: add, remove, addunique --- tests/integration_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index fe2a361..115b3b3 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -375,7 +375,7 @@ def test_add_array(self): book = Object.please.add( 'array', [11], - class_name='book', + class_name=self.book.name, id=book.id ) @@ -390,7 +390,7 @@ def test_remove_array(self): book = Object.please.remove( 'array', [10], - class_name='book', + class_name=self.book.name, id=book.id ) @@ -405,7 +405,7 @@ def test_addunique_array(self): book = Object.please.add_unique( 'array', [10], - class_name='book', + class_name=self.book.name, id=book.id ) @@ -414,7 +414,7 @@ def test_addunique_array(self): book = Object.please.add_unique( 'array', [11], - class_name='book', + class_name=self.book.name, id=book.id ) From 618b25d423927c02e8eaa80643467db559458c8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 27 May 2016 17:44:35 +0200 Subject: [PATCH 414/558] [LIB-732] correct new object initialization - use registry.intance_name if provided --- syncano/models/classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/classes.py b/syncano/models/classes.py index e710d82..0027c04 100644 --- a/syncano/models/classes.py +++ b/syncano/models/classes.py @@ -156,7 +156,7 @@ def _set_up_object_class(cls, model): @classmethod def _get_instance_name(cls, kwargs): - return kwargs.get('instance_name') + return kwargs.get('instance_name') or registry.instance_name @classmethod def _get_class_name(cls, kwargs): From 457181948defa6f3411d8a346aa168876b636ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 27 May 2016 18:04:11 +0200 Subject: [PATCH 415/558] [LIB-732] add test; --- tests/integration_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration_test.py b/tests/integration_test.py index f766b25..75edbf9 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -267,7 +267,11 @@ def test_create(self): name='test', description='test', quantity=10, cost=10.5, published_at=datetime.now(), author=author, available=True) + book_direct = Object(class_name=self.book.name, quantity=15, cost=7.5) + book_direct.save() + book.delete() + book_direct.delete() author.delete() def test_update(self): From 357ee5e0836e2d2db6cd71a57659ad244f654537 Mon Sep 17 00:00:00 2001 From: Maciej Kucharz Date: Thu, 2 Jun 2016 13:36:09 +0200 Subject: [PATCH 416/558] Support for user auth(). (#202) * Support for user auth(). * Style fix. * Small integration test. * Removing this test for now. * I've changed a little bit concept, * Imports sorting. * Simple test. * s/!/. --- syncano/models/accounts.py | 21 ++++++++++++++++++++- tests/integration_test_accounts.py | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 082cccf..8623c49 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -1,4 +1,4 @@ - +from syncano.exceptions import SyncanoValueError from . import fields from .base import Model @@ -117,6 +117,10 @@ class Meta: 'methods': ['post'], 'path': '/users/{id}/reset_key/', }, + 'auth': { + 'methods': ['post'], + 'path': '/user/auth/', + }, 'list': { 'methods': ['get'], 'path': '/users/', @@ -133,6 +137,21 @@ def reset_key(self): connection = self._get_connection() return connection.request('POST', endpoint) + def auth(self, username=None, password=None): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('auth', properties) + connection = self._get_connection() + + if not (username and password): + raise SyncanoValueError('You need provide username and password.') + + data = { + 'username': username, + 'password': password + } + + return connection.request('POST', endpoint, data=data) + def _user_groups_method(self, group_id=None, method='GET'): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('groups', properties) diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py index 36d6e90..ac467db 100644 --- a/tests/integration_test_accounts.py +++ b/tests/integration_test_accounts.py @@ -67,3 +67,8 @@ def test_user_alt_login(self): user_key=self.USER_KEY, instance_name=self.INSTANCE_NAME) self.check_connection(con) + + def test_user_auth(self): + self.assertTrue( + self.connection.User().auth(username=self.USER_NAME, password=self.USER_PASSWORD) + ) From d938dcd0efb3c3050072c5b0141414df44b2a725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 12:23:13 +0200 Subject: [PATCH 417/558] [LIB-656] add cache_key support for data endpoints and script endpoints; --- syncano/models/data_views.py | 9 ++++-- syncano/models/incentives.py | 8 +++-- syncano/models/instances.py | 1 + tests/integration_test_cache.py | 56 +++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 tests/integration_test_cache.py diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 703e252..85f63a9 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -78,12 +78,17 @@ def clear_cache(self): connection = self._get_connection() return connection.request('POST', endpoint) - def get(self): + def get(self, cache_key=None): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('get', properties) connection = self._get_connection() + + params = {} + if cache_key is not None: + params = {'cache_key': cache_key} + while endpoint is not None: - response = connection.request('GET', endpoint) + response = connection.request('GET', endpoint, params=params) endpoint = response.get('next') for obj in response['objects']: yield obj diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 99da749..91cdb9a 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -254,7 +254,7 @@ class Meta: } } - def run(self, **payload): + def run(self, cache_key=None, **payload): """ Usage:: @@ -271,7 +271,11 @@ def run(self, **payload): endpoint = self._meta.resolve_endpoint('run', properties) connection = self._get_connection(**payload) - response = connection.request('POST', endpoint, **{'data': payload}) + params = {} + if cache_key is not None: + params = {'cache_key': cache_key} + + response = connection.request('POST', endpoint, **{'data': payload, 'params': params}) if isinstance(response, dict) and 'result' in response and 'stdout' in response['result']: response.update({'instance_name': self.instance_name, diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 66f49f6..3cc4697 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -51,6 +51,7 @@ class Instance(RenameMixin, Model): # snippets and data fields; scripts = fields.RelatedManagerField('Script') script_endpoints = fields.RelatedManagerField('ScriptEndpoint') + data_endpoints = fields.RelatedManagerField('EndpointData') templates = fields.RelatedManagerField('ResponseTemplate') triggers = fields.RelatedManagerField('Trigger') diff --git a/tests/integration_test_cache.py b/tests/integration_test_cache.py new file mode 100644 index 0000000..513a6a4 --- /dev/null +++ b/tests/integration_test_cache.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from syncano.models import RuntimeChoices +from tests.integration_test import InstanceMixin, IntegrationTest + + +class DataEndpointCacheTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(DataEndpointCacheTest, cls).setUpClass() + cls.klass = cls.instance.templates.create( + name='sample_klass', + schema=[ + {'name': 'test1', 'type': 'string'}, + {'name': 'test2', 'type': 'string'} + ]) + cls.data_object = cls.instance.templates.create( + class_name=cls.klass.name + ) + + cls.data_endpoint = cls.instance.data_endpoints.create( + name='test_data_endpoint', + description='test description', + class_name=cls.klass.name + ) + + def test_cache_request(self): + data_endpoint = list(self.data_endpoint.get(cache_key='12345')) + + self.assertTrue(data_endpoint) + + for data_object in data_endpoint: + self.assertTrue(data_object) + + +class ScriptEndpointCacheTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(ScriptEndpointCacheTest, cls).setUpClass() + + cls.script = cls.instance.scripts.create( + label='test_script', + description='test script desc', + source='print(12)', + runtime_name=RuntimeChoices.PYTHON_V5_0, + ) + + cls.script_endpoint = cls.instance.script_endpoints.create( + name='test_script_endpoint', + script=cls.script.id + ) + + def test_cache_request(self): + response = self.script_endpoint.run(cache_key='123456') + self.assertEqual(response.result['stdout'], 12) From 2565cb8774bb0f859529688c3d443fd23a88c545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 12:33:36 +0200 Subject: [PATCH 418/558] [LIB-656] add cache_key support for data endpoints and script endpoints; --- syncano/models/data_views.py | 6 +++++- syncano/models/incentives.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 85f63a9..cadb05f 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -83,12 +83,16 @@ def get(self, cache_key=None): endpoint = self._meta.resolve_endpoint('get', properties) connection = self._get_connection() + kwargs = {} params = {} if cache_key is not None: params = {'cache_key': cache_key} + if params: + kwargs = {'params': params} + while endpoint is not None: - response = connection.request('GET', endpoint, params=params) + response = connection.request('GET', endpoint, **kwargs) endpoint = response.get('next') for obj in response['objects']: yield obj diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 91cdb9a..3c6105b 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -275,7 +275,11 @@ def run(self, cache_key=None, **payload): if cache_key is not None: params = {'cache_key': cache_key} - response = connection.request('POST', endpoint, **{'data': payload, 'params': params}) + kwargs = {'data': payload} + if params: + kwargs.update({'params': params}) + + response = connection.request('POST', endpoint, **kwargs) if isinstance(response, dict) and 'result' in response and 'stdout' in response['result']: response.update({'instance_name': self.instance_name, From f600cf33e3420a71eebac49359059b2ae790f34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 12:51:38 +0200 Subject: [PATCH 419/558] [LIB-656] add cache_key support for data endpoints and script endpoints; --- tests/integration_test_cache.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration_test_cache.py b/tests/integration_test_cache.py index 513a6a4..13a701d 100644 --- a/tests/integration_test_cache.py +++ b/tests/integration_test_cache.py @@ -9,6 +9,7 @@ class DataEndpointCacheTest(InstanceMixin, IntegrationTest): def setUpClass(cls): super(DataEndpointCacheTest, cls).setUpClass() cls.klass = cls.instance.templates.create( + instance_name=cls.instance.name, name='sample_klass', schema=[ {'name': 'test1', 'type': 'string'}, @@ -53,4 +54,4 @@ def setUpClass(cls): def test_cache_request(self): response = self.script_endpoint.run(cache_key='123456') - self.assertEqual(response.result['stdout'], 12) + self.assertEqual(response.result['stdout'], '12') From 18ed55b3f06b39fe5b681903440fa9089c45594d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 13:03:08 +0200 Subject: [PATCH 420/558] [LIB-656] add cache_key support for data endpoints and script endpoints; --- tests/integration_test_cache.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_cache.py b/tests/integration_test_cache.py index 13a701d..46b93ad 100644 --- a/tests/integration_test_cache.py +++ b/tests/integration_test_cache.py @@ -15,8 +15,10 @@ def setUpClass(cls): {'name': 'test1', 'type': 'string'}, {'name': 'test2', 'type': 'string'} ]) - cls.data_object = cls.instance.templates.create( - class_name=cls.klass.name + cls.data_object = cls.klass.objects.create( + class_name=cls.klass.name, + test1='123', + test2='321', ) cls.data_endpoint = cls.instance.data_endpoints.create( From 41751ce2073a1c70fe304ebeb1f85a020de8232c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 13:12:24 +0200 Subject: [PATCH 421/558] [LIB-656] add cache_key support for data endpoints and script endpoints; --- tests/integration_test_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_cache.py b/tests/integration_test_cache.py index 46b93ad..3aff7ad 100644 --- a/tests/integration_test_cache.py +++ b/tests/integration_test_cache.py @@ -8,13 +8,13 @@ class DataEndpointCacheTest(InstanceMixin, IntegrationTest): @classmethod def setUpClass(cls): super(DataEndpointCacheTest, cls).setUpClass() - cls.klass = cls.instance.templates.create( - instance_name=cls.instance.name, + cls.klass = cls.instance.classes.create( name='sample_klass', schema=[ {'name': 'test1', 'type': 'string'}, {'name': 'test2', 'type': 'string'} ]) + cls.data_object = cls.klass.objects.create( class_name=cls.klass.name, test1='123', From 7f98de234d73a1111981868dbeab3411e4e9304f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 13:49:46 +0200 Subject: [PATCH 422/558] [LIB-656] add cache_key support for data endpoints and script endpoints; --- syncano/models/archetypes.py | 4 +++- tests/integration_test_cache.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 984ebde..7011375 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -198,6 +198,7 @@ def validate(self): """ for field in self._meta.fields: if not field.read_only: + # field_name = field.name if not field.mapping else field.mapping value = getattr(self, field.name) field.validate(value, self) @@ -225,10 +226,11 @@ def to_python(self, data): :type data: dict :param data: Raw data """ + for field in self._meta.fields: field_name = field.name - if field.mapping is not None and self.pk: + if field.mapping is not None and not self.is_new(): field_name = field.mapping if field_name in data: diff --git a/tests/integration_test_cache.py b/tests/integration_test_cache.py index 3aff7ad..1f907bf 100644 --- a/tests/integration_test_cache.py +++ b/tests/integration_test_cache.py @@ -24,7 +24,8 @@ def setUpClass(cls): cls.data_endpoint = cls.instance.data_endpoints.create( name='test_data_endpoint', description='test description', - class_name=cls.klass.name + class_name=cls.klass.name, + query={} ) def test_cache_request(self): From 8c13787024769fe1824e4d1e69d88e9f80e543c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 13:57:05 +0200 Subject: [PATCH 423/558] [LIB-656] query on data endpoints is not required; --- syncano/models/data_views.py | 2 +- tests/integration_test_cache.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index cadb05f..dfff917 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -28,7 +28,7 @@ class EndpointData(Model): name = fields.StringField(max_length=64, primary_key=True) description = fields.StringField(required=False) - query = fields.JSONField(read_only=False, required=True) + query = fields.JSONField(read_only=False) class_name = fields.StringField(label='class name', mapping='class') diff --git a/tests/integration_test_cache.py b/tests/integration_test_cache.py index 1f907bf..3aff7ad 100644 --- a/tests/integration_test_cache.py +++ b/tests/integration_test_cache.py @@ -24,8 +24,7 @@ def setUpClass(cls): cls.data_endpoint = cls.instance.data_endpoints.create( name='test_data_endpoint', description='test description', - class_name=cls.klass.name, - query={} + class_name=cls.klass.name ) def test_cache_request(self): From 35755dbdafa37d7232e66a17a78e76c80a76f9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 14:06:03 +0200 Subject: [PATCH 424/558] [LIB-656] query on data endpoints is not required; --- syncano/models/data_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index dfff917..f504179 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -28,7 +28,7 @@ class EndpointData(Model): name = fields.StringField(max_length=64, primary_key=True) description = fields.StringField(required=False) - query = fields.JSONField(read_only=False) + query = fields.JSONField(read_only=False, required=False) class_name = fields.StringField(label='class name', mapping='class') From c3029a1a538f6c3c017da4243e6aff65839c5f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 14:40:05 +0200 Subject: [PATCH 425/558] [LIB-175] add special error if user is not found; --- syncano/exceptions.py | 4 ++++ syncano/models/accounts.py | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/syncano/exceptions.py b/syncano/exceptions.py index bcd3f2a..1e2e7d8 100644 --- a/syncano/exceptions.py +++ b/syncano/exceptions.py @@ -75,3 +75,7 @@ class SyncanoDoesNotExist(SyncanoException): class RevisionMismatchException(SyncanoRequestError): """Revision do not match with expected one""" + + +class UserNotFound(SyncanoRequestError): + """Special error to handle user not found case.""" diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 8623c49..3cba2c2 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -1,4 +1,4 @@ -from syncano.exceptions import SyncanoValueError +from syncano.exceptions import SyncanoValueError, SyncanoRequestError, UserNotFound from . import fields from .base import Model @@ -214,7 +214,14 @@ def _group_users_method(self, user_id=None, method='GET'): if user_id is not None: endpoint += '{}/'.format(user_id) connection = self._get_connection() - return connection.request(method, endpoint) + try: + response = connection.request(method, endpoint) + except SyncanoRequestError as e: + if e.status_code == 404: + raise UserNotFound(e.status_code, 'User not found.') + raise + + return response def list_users(self): return self._group_users_method() From 9f2dbfb87f79f29c4a2149958f18eeccfa14c1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 14:43:22 +0200 Subject: [PATCH 426/558] [LIB-175] add tests --- syncano/models/accounts.py | 2 +- ..._user_profile.py => integration_test_user.py} | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) rename tests/{integration_test_user_profile.py => integration_test_user.py} (75%) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 3cba2c2..7737f48 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -1,4 +1,4 @@ -from syncano.exceptions import SyncanoValueError, SyncanoRequestError, UserNotFound +from syncano.exceptions import SyncanoRequestError, SyncanoValueError, UserNotFound from . import fields from .base import Model diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user.py similarity index 75% rename from tests/integration_test_user_profile.py rename to tests/integration_test_user.py index 37a8def..29cb94d 100644 --- a/tests/integration_test_user_profile.py +++ b/tests/integration_test_user.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from syncano.exceptions import UserNotFound from syncano.models import User from tests.integration_test import InstanceMixin, IntegrationTest @@ -39,3 +40,18 @@ def test_profile_change_schema(self): self.user.profile.save() user = User.please.get(id=self.user.id) self.assertEqual(user.profile.profile_pic, self.SAMPLE_PROFILE_PIC) + + +class UserTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(UserProfileTest, cls).setUpClass() + + cls.group = cls.instance.groups.create( + name='testgroup' + ) + + def test_if_custom_error_is_raised_on_user_group(self): + with self.assertRaises(UserNotFound): + self.group.user_details(user_id=221) From 162a356d32c581e0876de8b88140d8fad489eb36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 14:55:29 +0200 Subject: [PATCH 427/558] [LIB-175] correct tests --- tests/integration_test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_user.py b/tests/integration_test_user.py index 29cb94d..ecb12f8 100644 --- a/tests/integration_test_user.py +++ b/tests/integration_test_user.py @@ -46,7 +46,7 @@ class UserTest(InstanceMixin, IntegrationTest): @classmethod def setUpClass(cls): - super(UserProfileTest, cls).setUpClass() + super(UserTest, cls).setUpClass() cls.group = cls.instance.groups.create( name='testgroup' From f7349b477edbcdbc1944a4554ef19c3891f614dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 15:12:07 +0200 Subject: [PATCH 428/558] [LIB-175] correct tests --- tests/integration_test_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_user.py b/tests/integration_test_user.py index ecb12f8..457ee0e 100644 --- a/tests/integration_test_user.py +++ b/tests/integration_test_user.py @@ -49,7 +49,7 @@ def setUpClass(cls): super(UserTest, cls).setUpClass() cls.group = cls.instance.groups.create( - name='testgroup' + label='testgroup' ) def test_if_custom_error_is_raised_on_user_group(self): From 9a66243422dba913829490efe46807de4de5043b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 15:13:11 +0200 Subject: [PATCH 429/558] [LIB-656] remove obsolete comment --- syncano/models/archetypes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 7011375..bf97136 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -198,7 +198,6 @@ def validate(self): """ for field in self._meta.fields: if not field.read_only: - # field_name = field.name if not field.mapping else field.mapping value = getattr(self, field.name) field.validate(value, self) From 8f44705d73c857766ddd8fa7c1dd4b6fa3a278fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 7 Jun 2016 15:44:58 +0200 Subject: [PATCH 430/558] [LIB-667][WIP] saving a user saves now user_profile; --- syncano/models/fields.py | 6 +++++- tests/integration_test_user_profile.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index bb057e2..a8d8aaa 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -504,7 +504,8 @@ def validate(self, value, model_instance): super(ModelField, self).validate(value, model_instance) if not isinstance(value, (self.rel, dict)): - raise self.ValidationError('Value needs to be a {0} instance.'.format(self.rel.__name__)) + if 'UserProfile' not in value.__class__.__name__: + raise self.ValidationError('Value needs to be a {0} instance.'.format(self.rel.__name__)) if self.required and isinstance(value, self.rel): value.validate() @@ -534,6 +535,9 @@ def to_native(self, value): pk_value = getattr(value, pk_field.name) return pk_field.to_native(pk_value) + if 'UserProfile' in value.__class__.__name__: + return value.to_native() + return value diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user_profile.py index 37a8def..021394a 100644 --- a/tests/integration_test_user_profile.py +++ b/tests/integration_test_user_profile.py @@ -36,6 +36,6 @@ def test_profile_change_schema(self): self.user.reload() # force to refresh profile model; self.user.profile.profile_pic = self.SAMPLE_PROFILE_PIC - self.user.profile.save() + self.user.save() user = User.please.get(id=self.user.id) self.assertEqual(user.profile.profile_pic, self.SAMPLE_PROFILE_PIC) From eff5040e1d64c2bf7c0f9ebfeb23da94da384cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 8 Jun 2016 11:36:06 +0200 Subject: [PATCH 431/558] [LIB-667] add is_data_object_mixin on ModelField - DataObjects are dynamically created class - so it's hard to check isinstance or issubclass - but we know it that they are a valid objects; --- syncano/models/accounts.py | 3 ++- syncano/models/fields.py | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 8623c49..a668de7 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -100,7 +100,8 @@ class User(Model): password = fields.StringField(read_only=False, required=True) user_key = fields.StringField(read_only=True, required=False) - profile = fields.ModelField('Profile', read_only=False, default={}) + profile = fields.ModelField('Profile', read_only=False, default={}, + just_pk=False, is_data_object_mixin=True) links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index a8d8aaa..e88b2cd 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -481,6 +481,7 @@ class ModelField(Field): def __init__(self, rel, *args, **kwargs): self.rel = rel self.just_pk = kwargs.pop('just_pk', True) + self.is_data_object_mixin = kwargs.pop('is_data_object_mixin', False) super(ModelField, self).__init__(*args, **kwargs) def contribute_to_class(self, cls, name): @@ -504,7 +505,7 @@ def validate(self, value, model_instance): super(ModelField, self).validate(value, model_instance) if not isinstance(value, (self.rel, dict)): - if 'UserProfile' not in value.__class__.__name__: + if not self.is_data_object_mixin: raise self.ValidationError('Value needs to be a {0} instance.'.format(self.rel.__name__)) if self.required and isinstance(value, self.rel): @@ -527,7 +528,7 @@ def to_native(self, value): if value is None: return - if isinstance(value, self.rel): + if isinstance(value, self.rel) or self.is_data_object_mixin: if not self.just_pk: return value.to_native() @@ -535,9 +536,6 @@ def to_native(self, value): pk_value = getattr(value, pk_field.name) return pk_field.to_native(pk_value) - if 'UserProfile' in value.__class__.__name__: - return value.to_native() - return value From 230e7f9abb5a7ddedc6563c1fbd5de83a098748b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 8 Jun 2016 12:01:16 +0200 Subject: [PATCH 432/558] [LIB-667] rework to_native on ModelField --- syncano/models/fields.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index e88b2cd..739e1d6 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -528,7 +528,7 @@ def to_native(self, value): if value is None: return - if isinstance(value, self.rel) or self.is_data_object_mixin: + if isinstance(value, self.rel): if not self.just_pk: return value.to_native() @@ -536,6 +536,10 @@ def to_native(self, value): pk_value = getattr(value, pk_field.name) return pk_field.to_native(pk_value) + if self.is_data_object_mixin: + if not self.just_pk and hasattr(value, 'to_native'): + return value.to_native() + return value From 7e5ba1c33423d0d6dea7983595d8b0bf15010d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 8 Jun 2016 12:10:42 +0200 Subject: [PATCH 433/558] [LIB-667] add one more test - save directly on profile; add validation check for profile in ModelField --- syncano/models/fields.py | 3 +++ tests/integration_test_user_profile.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 739e1d6..7c49f66 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -511,6 +511,9 @@ def validate(self, value, model_instance): if self.required and isinstance(value, self.rel): value.validate() + if self.is_data_object_mixin and hasattr(value, 'validate'): + value.validate() + def to_python(self, value): if value is None: diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user_profile.py index 021394a..14147bf 100644 --- a/tests/integration_test_user_profile.py +++ b/tests/integration_test_user_profile.py @@ -13,6 +13,7 @@ def setUpClass(cls): password='jezioro', ) cls.SAMPLE_PROFILE_PIC = 'some_url_here' + cls.ANOTHER_SAMPLE_PROFILE_PIC = 'yet_another_url' def test_profile(self): self.assertTrue(self.user.profile) @@ -39,3 +40,9 @@ def test_profile_change_schema(self): self.user.save() user = User.please.get(id=self.user.id) self.assertEqual(user.profile.profile_pic, self.SAMPLE_PROFILE_PIC) + + # test save directly on profile + self.user.profile.profile_pic = self.ANOTHER_SAMPLE_PROFILE_PIC + self.user.profile.save() + user = User.please.get(id=self.user.id) + self.assertEqual(user.profile.profile_pic, self.SAMPLE_PROFILE_PIC) From c0f327d68fd2433795874cf71f905fd7179eda2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 8 Jun 2016 12:39:05 +0200 Subject: [PATCH 434/558] [LIB-667] correct test --- tests/integration_test_user_profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_user_profile.py b/tests/integration_test_user_profile.py index 14147bf..b1dc49b 100644 --- a/tests/integration_test_user_profile.py +++ b/tests/integration_test_user_profile.py @@ -45,4 +45,4 @@ def test_profile_change_schema(self): self.user.profile.profile_pic = self.ANOTHER_SAMPLE_PROFILE_PIC self.user.profile.save() user = User.please.get(id=self.user.id) - self.assertEqual(user.profile.profile_pic, self.SAMPLE_PROFILE_PIC) + self.assertEqual(user.profile.profile_pic, self.ANOTHER_SAMPLE_PROFILE_PIC) From 52827fc722a2c365277490dda448087ac1219db2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 8 Jun 2016 13:01:03 +0200 Subject: [PATCH 435/558] [LIB-667] correct ifo-logic --- syncano/models/fields.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 7c49f66..15126e1 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -505,13 +505,11 @@ def validate(self, value, model_instance): super(ModelField, self).validate(value, model_instance) if not isinstance(value, (self.rel, dict)): - if not self.is_data_object_mixin: + if not isinstance(value, (self.rel, dict)) and not self.is_data_object_mixin: raise self.ValidationError('Value needs to be a {0} instance.'.format(self.rel.__name__)) - if self.required and isinstance(value, self.rel): - value.validate() - - if self.is_data_object_mixin and hasattr(value, 'validate'): + if (self.required and isinstance(value, self.rel))or \ + (self.is_data_object_mixin and hasattr(value, 'validate')): value.validate() def to_python(self, value): @@ -539,9 +537,8 @@ def to_native(self, value): pk_value = getattr(value, pk_field.name) return pk_field.to_native(pk_value) - if self.is_data_object_mixin: - if not self.just_pk and hasattr(value, 'to_native'): - return value.to_native() + if self.is_data_object_mixin and not self.just_pk and hasattr(value, 'to_native'): + return value.to_native() return value From 77ea467ecefd01937fbd3c8f0e9ee75520afa7a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 9 Jun 2016 16:33:52 +0200 Subject: [PATCH 436/558] [LIB-642][WIP] add query paramter to DataEndpoints --- syncano/models/archetypes.py | 6 +++--- syncano/models/data_views.py | 14 +++++++++----- syncano/models/instances.py | 2 +- syncano/models/manager.py | 31 ++++++++++++++++++++++++++++++- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index bf97136..cdd532a 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -229,11 +229,11 @@ def to_python(self, data): for field in self._meta.fields: field_name = field.name - if field.mapping is not None and not self.is_new(): + if field.mapping is not None: field_name = field.mapping - if field_name in data: - value = data[field_name] + if field_name in data or field.name in data: + value = data.get(field_name, None) or data.get(field.name, None) setattr(self, field.name, value) if isinstance(field, fields.RelationField): diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index f504179..5f3e77a 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -1,11 +1,11 @@ - +import json from . import fields -from .base import Model +from .base import Model, Object from .instances import Instance -class EndpointData(Model): +class DataEndpoint(Model): """ :ivar name: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` @@ -78,13 +78,17 @@ def clear_cache(self): connection = self._get_connection() return connection.request('POST', endpoint) - def get(self, cache_key=None): + def get(self, cache_key=None, **kwargs): + connection = self._get_connection() properties = self.get_endpoint_data() + query = Object.please._build_query(query_data=kwargs, class_name=self.class_name) + endpoint = self._meta.resolve_endpoint('get', properties) - connection = self._get_connection() kwargs = {} params = {} + params.update({'query': json.dumps(query)}) + if cache_key is not None: params = {'cache_key': cache_key} diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 3cc4697..59c3360 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -51,7 +51,7 @@ class Instance(RenameMixin, Model): # snippets and data fields; scripts = fields.RelatedManagerField('Script') script_endpoints = fields.RelatedManagerField('ScriptEndpoint') - data_endpoints = fields.RelatedManagerField('EndpointData') + data_endpoints = fields.RelatedManagerField('DataEndpoint') templates = fields.RelatedManagerField('ResponseTemplate') triggers = fields.RelatedManagerField('Trigger') diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 591ee18..03a29f1 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -978,12 +978,41 @@ def filter(self, **kwargs): related_field_name=field_name, related_field_lookup=lookup, ) - + query = self._build_query(query_data=kwargs) self.query['query'] = json.dumps(query) self.method = 'GET' self.endpoint = 'list' return self + def _build_query(self, query_data, **kwargs): + query = {} + self.properties.update(**kwargs) + model = self.model.get_subclass_model(**self.properties) + + for field_name, value in six.iteritems(query_data): + lookup = 'eq' + model_name = None + + if self.LOOKUP_SEPARATOR in field_name: + model_name, field_name, lookup = self._get_lookup_attributes(field_name) + + for field in model._meta.fields: + if field.name == field_name: + break + + self._validate_lookup(model, model_name, field_name, lookup, field) + + query_main_lookup, query_main_field = self._get_main_lookup(model_name, field_name, lookup) + + query.setdefault(query_main_field, {}) + query[query_main_field]['_{0}'.format(query_main_lookup)] = field.to_query( + value, + query_main_lookup, + related_field_name=field_name, + related_field_lookup=lookup, + ) + return query + def _get_lookup_attributes(self, field_name): try: model_name, field_name, lookup = field_name.split(self.LOOKUP_SEPARATOR, 2) From 873296428dc87555ad59135c30448b74f8d0b275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 13 Jun 2016 10:25:56 +0200 Subject: [PATCH 437/558] [LIB-642] add tests, correct mapping in model archetypes; --- syncano/models/archetypes.py | 12 +++++-- tests/integration_test.py | 63 +++++++++++++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index cdd532a..507cbef 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -229,10 +229,18 @@ def to_python(self, data): for field in self._meta.fields: field_name = field.name - if field.mapping is not None: + # some explanation needed here: + # When data comes from Syncano Platform the 'class' field is there + # so to map correctly the 'class' value to the 'class_name' field + # the mapping is required. + # But. When DataEndpoint (and probably others models with mapping) is created from + # syncano LIB directly: DataEndpoint(class_name='some_class') + # the data dict has only 'class_name' key - not the 'class', + # later the transition between class_name and class is made in to_native on model; + if field.mapping is not None and field.mapping in data: field_name = field.mapping - if field_name in data or field.name in data: + if field_name: value = data.get(field_name, None) or data.get(field.name, None) setattr(self, field.name, value) diff --git a/tests/integration_test.py b/tests/integration_test.py index 86394c0..cc3fa6e 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -7,7 +7,7 @@ import syncano from syncano.exceptions import SyncanoRequestError, SyncanoValueError -from syncano.models import ApiKey, Class, Instance, Model, Object, Script, ScriptEndpoint, registry +from syncano.models import ApiKey, Class, DataEndpoint, Instance, Model, Object, Script, ScriptEndpoint, registry class IntegrationTest(unittest.TestCase): @@ -594,3 +594,64 @@ def test_api_key_flags(self): 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) + + +class DataEndpointIntegrationTest(InstanceMixin, IntegrationTest): + @classmethod + def setUpClass(cls): + super(DataEndpointIntegrationTest, cls).setUpClass() + cls.klass = cls.instance.classes.create( + name='sample_klass', + schema=[ + {'name': 'test1', 'type': 'string', 'filter_index': True}, + {'name': 'test2', 'type': 'string', 'filter_index': True}, + {'name': 'test3', 'type': 'integer', 'filter_index': True}, + ]) + + cls.data_object = cls.klass.objects.create( + class_name=cls.klass.name, + test1='atest', + test2='321', + test3=50 + ) + + cls.data_object = cls.klass.objects.create( + class_name=cls.klass.name, + test1='btest', + test2='432', + test3=45 + ) + + cls.data_object = cls.klass.objects.create( + class_name=cls.klass.name, + test1='ctest', + test2='543', + test3=35 + ) + + cls.data_endpoint = cls.instance.data_endpoints.create( + name='test_data_endpoint', + description='test description', + class_name=cls.klass.name, + query={'test3': {'_gt': 35}} + ) + + def test_mapping_class_name_lib_creation(self): + data_endpoint = DataEndpoint( + name='yet_another_data_endpoint', + class_name=self.klass.name, + ) + data_endpoint.save() + self.assertEqual(data_endpoint.class_name, 'sample_klass') + + def test_mapping_class_name_lib_read(self): + data_endpoint = self.instance.data_endpoints.get(name='test_data_endpoint') + self.assertEqual(data_endpoint.class_name, 'test_data_endpoint') + + def test_data_endpoint_filtering(self): + data_endpoint = self.instance.data_endpoints.get(name='test_data_endpoint') + objects = [object for object in data_endpoint.get()] + self.assertEqual(len(objects), 2) + + objects = [object for object in data_endpoint.get(test1__eq='atest')] + self.assertEqual(len(objects), 1) From b6881572787d3b9e9d18c043f803a9eaf53c0b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 13 Jun 2016 10:47:31 +0200 Subject: [PATCH 438/558] [LIB-642] correct mapping handling; --- syncano/models/archetypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 507cbef..39967eb 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -237,10 +237,10 @@ def to_python(self, data): # syncano LIB directly: DataEndpoint(class_name='some_class') # the data dict has only 'class_name' key - not the 'class', # later the transition between class_name and class is made in to_native on model; - if field.mapping is not None and field.mapping in data: + if field.mapping is not None and field.mapping in data and self.is_new(): field_name = field.mapping - if field_name: + if field_name in data: value = data.get(field_name, None) or data.get(field.name, None) setattr(self, field.name, value) From b21bcdd26c1eef2d947f43a5995ab84c17ad784a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 13 Jun 2016 11:06:51 +0200 Subject: [PATCH 439/558] [LIB-642] remove not needed code - moved this to build_query on manager; small test fixes; --- syncano/models/archetypes.py | 2 +- syncano/models/manager.py | 24 ------------------------ tests/integration_test.py | 2 +- 3 files changed, 2 insertions(+), 26 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 39967eb..f47f8ae 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -241,7 +241,7 @@ def to_python(self, data): field_name = field.mapping if field_name in data: - value = data.get(field_name, None) or data.get(field.name, None) + value = data[field_name] setattr(self, field.name, value) if isinstance(field, fields.RelationField): diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 03a29f1..07d6d5c 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -953,31 +953,7 @@ def filter(self, **kwargs): objects = Object.please.list('instance-name', 'class-name').filter(henryk__gte='hello') """ - query = {} - model = self.model.get_subclass_model(**self.properties) - - for field_name, value in six.iteritems(kwargs): - lookup = 'eq' - model_name = None - - if self.LOOKUP_SEPARATOR in field_name: - model_name, field_name, lookup = self._get_lookup_attributes(field_name) - - for field in model._meta.fields: - if field.name == field_name: - break - self._validate_lookup(model, model_name, field_name, lookup, field) - - query_main_lookup, query_main_field = self._get_main_lookup(model_name, field_name, lookup) - - query.setdefault(query_main_field, {}) - query[query_main_field]['_{0}'.format(query_main_lookup)] = field.to_query( - value, - query_main_lookup, - related_field_name=field_name, - related_field_lookup=lookup, - ) query = self._build_query(query_data=kwargs) self.query['query'] = json.dumps(query) self.method = 'GET' diff --git a/tests/integration_test.py b/tests/integration_test.py index cc3fa6e..c933e0e 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -646,7 +646,7 @@ def test_mapping_class_name_lib_creation(self): def test_mapping_class_name_lib_read(self): data_endpoint = self.instance.data_endpoints.get(name='test_data_endpoint') - self.assertEqual(data_endpoint.class_name, 'test_data_endpoint') + self.assertEqual(data_endpoint.class_name, 'sample_klass') def test_data_endpoint_filtering(self): data_endpoint = self.instance.data_endpoints.get(name='test_data_endpoint') From 0dca01958d5a27f5cfdde10f2a2b3ae181cf79c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 16 Jun 2016 11:33:59 +0200 Subject: [PATCH 440/558] [LIB-769] add proper user and group endpoints handling; --- syncano/models/accounts.py | 35 ++++++++++++++++++---- tests/integration_test_user.py | 54 +++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index aa07d50..16639d2 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -156,10 +156,24 @@ def auth(self, username=None, password=None): def _user_groups_method(self, group_id=None, method='GET'): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('groups', properties) - if group_id is not None: + + if group_id is not None and method != 'POST': endpoint += '{}/'.format(group_id) connection = self._get_connection() - return connection.request(method, endpoint) + + data = {} + if method == 'POST': + data = {'group': group_id} + + response = connection.request(method, endpoint, data=data) + + if method == 'DELETE': # no response here; + return + + if 'objects' in response: + return [Group(**group_response['group']) for group_response in response['objects']] + + return Group(**response['group']) def add_to_group(self, group_id): return self._user_groups_method(group_id, method='POST') @@ -212,17 +226,28 @@ class Meta: def _group_users_method(self, user_id=None, method='GET'): properties = self.get_endpoint_data() endpoint = self._meta.resolve_endpoint('users', properties) - if user_id is not None: + if user_id is not None and method != 'POST': endpoint += '{}/'.format(user_id) connection = self._get_connection() + + data = {} + if method == 'POST': + data = {'user': user_id} + try: - response = connection.request(method, endpoint) + response = connection.request(method, endpoint, data=data) except SyncanoRequestError as e: if e.status_code == 404: raise UserNotFound(e.status_code, 'User not found.') raise - return response + if method == 'DELETE': + return + + if 'objects' in response: + return [User(**user_response['user']) for user_response in response['objects']] + + return User(**response['user']) def list_users(self): return self._group_users_method() diff --git a/tests/integration_test_user.py b/tests/integration_test_user.py index 87fc9d5..98cf3cd 100644 --- a/tests/integration_test_user.py +++ b/tests/integration_test_user.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from syncano.exceptions import UserNotFound -from syncano.models import User +from syncano.models import Group, User from tests.integration_test import InstanceMixin, IntegrationTest @@ -62,3 +62,55 @@ def setUpClass(cls): def test_if_custom_error_is_raised_on_user_group(self): with self.assertRaises(UserNotFound): self.group.user_details(user_id=221) + + def test_user_group_membership(self): + user = User.please.create( + username='testa', + password='1234' + ) + + group_test = Group.please.create(label='new_group_a') + + groups = user.list_groups() + self.assertListEqual(groups, []) + + group = user.add_to_group(group_id=group_test.id) + self.assertEqual(group.id, group_test.id) + self.assertEqual(group.label, group_test.label) + + groups = user.list_groups() + self.assertEqual(len(groups), 1) + self.assertEqual(groups[0].id, group_test.id) + + group = user.group_details(group_id=group_test.id) + self.assertEqual(group.id, group_test.id) + self.assertEqual(group.label, group_test.label) + + response = user.remove_from_group(group_id=group_test.id) + self.assertIsNone(response) + + def test_group_user_membership(self): + user_test = User.please.create( + username='testa', + password='1234' + ) + + group = Group.please.create(label='new_group_a') + + users = group.list_users() + self.assertListEqual(users, []) + + user = group.add_user(user_id=user_test.id) + self.assertEqual(user.id, user_test.id) + self.assertEqual(user.label, user_test.label) + + users = group.list_users() + self.assertEqual(len(users), 1) + self.assertEqual(users[0].id, user_test.id) + + user = group.user_details(user_id=user_test.id) + self.assertEqual(user.id, user_test.id) + self.assertEqual(user.label, user_test.label) + + response = group.delete_user(user_id=user_test.id) + self.assertIsNone(response) From 4300e1901a388f27fd95a7713b47662d3ea42765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 16 Jun 2016 11:57:32 +0200 Subject: [PATCH 441/558] [LIB-769] Fix tests --- tests/integration_test_user.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration_test_user.py b/tests/integration_test_user.py index 98cf3cd..e314e28 100644 --- a/tests/integration_test_user.py +++ b/tests/integration_test_user.py @@ -91,18 +91,18 @@ def test_user_group_membership(self): def test_group_user_membership(self): user_test = User.please.create( - username='testa', + username='testb', password='1234' ) - group = Group.please.create(label='new_group_a') + group = Group.please.create(label='new_group_b') users = group.list_users() self.assertListEqual(users, []) user = group.add_user(user_id=user_test.id) self.assertEqual(user.id, user_test.id) - self.assertEqual(user.label, user_test.label) + self.assertEqual(user.username, user_test.username) users = group.list_users() self.assertEqual(len(users), 1) @@ -110,7 +110,7 @@ def test_group_user_membership(self): user = group.user_details(user_id=user_test.id) self.assertEqual(user.id, user_test.id) - self.assertEqual(user.label, user_test.label) + self.assertEqual(user.username, user_test.username) response = group.delete_user(user_id=user_test.id) self.assertIsNone(response) From 327ef61aab843bf8dd5b531d3a70318b29626269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 16 Jun 2016 14:24:17 +0200 Subject: [PATCH 442/558] [LIB-758] add Backup and PartialBackup models; with full CRUD; --- syncano/models/backups.py | 95 +++++++++++++++++++++++++++++++ syncano/models/base.py | 1 + tests/integration_test_backups.py | 68 ++++++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 syncano/models/backups.py create mode 100644 tests/integration_test_backups.py diff --git a/syncano/models/backups.py b/syncano/models/backups.py new file mode 100644 index 0000000..f997793 --- /dev/null +++ b/syncano/models/backups.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +from . import fields +from .base import Model +from .instances import Instance + + +class Backup(Model): + """ + OO wrapper around backups `link `_. + + :ivar label: :class:`~syncano.models.fields.StringField` + :ivar description: :class:`~syncano.models.fields.StringField` + :ivar instance: :class:`~syncano.models.fields.StringField` + :ivar size: :class:`~syncano.models.fields.IntegerField` + :ivar status: :class:`~syncano.models.fields.StringField` + :ivar status_info: :class:`~syncano.models.fields.StringField` + :ivar author: :class:`~syncano.models.fields.ModelField` + :ivar details: :class:`~syncano.models.fields.JSONField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + """ + + label = fields.StringField(read_only=True) + description = fields.StringField(read_only=True) + + instance = fields.StringField(read_only=True) + size = fields.IntegerField(read_only=True) + status = fields.StringField(read_only=True) + status_info = fields.StringField(read_only=True) + author = fields.ModelField('Admin') + details = fields.JSONField(read_only=True) + + updated_at = fields.DateTimeField(read_only=True, required=False) + created_at = fields.DateTimeField(read_only=True, required=False) + links = fields.LinksField() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['get', 'delete'], + 'path': '/backups/full/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/backups/full/', + } + } + + +class PartialBackup(Model): + """ + OO wrapper around backups `link `_. + + :ivar label: :class:`~syncano.models.fields.StringField` + :ivar description: :class:`~syncano.models.fields.StringField` + :ivar instance: :class:`~syncano.models.fields.StringField` + :ivar size: :class:`~syncano.models.fields.IntegerField` + :ivar status: :class:`~syncano.models.fields.StringField` + :ivar status_info: :class:`~syncano.models.fields.StringField` + :ivar author: :class:`~syncano.models.fields.ModelField` + :ivar details: :class:`~syncano.models.fields.JSONField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + """ + + label = fields.StringField(read_only=True) + description = fields.StringField(read_only=True) + + instance = fields.StringField(read_only=True) + size = fields.IntegerField(read_only=True) + status = fields.StringField(read_only=True) + status_info = fields.StringField(read_only=True) + author = fields.ModelField('Admin') + details = fields.JSONField(read_only=True) + query_args = fields.JSONField(required=True) + + updated_at = fields.DateTimeField(read_only=True, required=False) + created_at = fields.DateTimeField(read_only=True, required=False) + links = fields.LinksField() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['get', 'delete'], + 'path': '/backups/partial/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/backups/partial/', + } + } diff --git a/syncano/models/base.py b/syncano/models/base.py index 0dd9089..31eb2dc 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -10,3 +10,4 @@ from .traces import * # NOQA from .push_notification import * # NOQA from .geo import * # NOQA +from .backups import * # NOQA diff --git a/tests/integration_test_backups.py b/tests/integration_test_backups.py new file mode 100644 index 0000000..6f7b2f0 --- /dev/null +++ b/tests/integration_test_backups.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +from syncano.models import Backup, PartialBackup +from tests.integration_test import InstanceMixin, IntegrationTest + + +class BaseBackupTestCase(InstanceMixin, IntegrationTest): + + BACKUP_MODEL = { + 'full': Backup, + 'partial': PartialBackup, + } + + def _get_backup_model(self, backup_type): + return self.BACKUP_MODEL.get(backup_type) + + def _test_backup_create(self, backup_type='full', query_args=None): + backup_model = self._get_backup_model(backup_type) + if query_args is not None: + new_backup = backup_model(query_args=query_args) + else: + new_backup = backup_model() + backup_test = new_backup.save() + + backup = Backup.please.get(id=backup_test.id) + self.assertTrue(backup) + self.assertEqual(backup.id, backup_test.id) + self.assertEqual(backup.author.email, self.API_EMAIL) + + return backup.id + + def _test_backup_detail(self, backup_id, backup_type='full'): + backup_model = self._get_backup_model(backup_type) + backup = backup_model.please.get(id=backup_id) + + self.assertEqual(backup.id, self.backup.id) + self.assertEqual(backup.author.email, self.API_EMAIL) + + def _test_backup_list(self, backup_type='full'): + backup_model = self._get_backup_model(backup_type) + backups = [backup for backup in backup_model.please.list()] + self.assertTrue(len(backups)) # at least one backup here; + + def _test_backup_delete(self, backup_id, backup_type='full'): + backup_model = self._get_backup_model(backup_type) + backup = backup_model.please.get(id=backup_id) + backup.delete() + backups = [backup_object for backup_object in backup_model.please.list()] + self.assertEqual(len(backups), 0) + + +class FullBackupTestCase(BaseBackupTestCase): + + def test_backup(self): + # we provide one test for all functionality to avoid creating too many backups; + backup_id = self._test_backup_create() + self._test_backup_list() + self._test_backup_detail(backup_id=backup_id) + self._test_backup_delete(backup_id=backup_id) + + +class PartialBackupTestCase(BaseBackupTestCase): + + def test_backup(self): + # we provide one test for all functionality to avoid creating too many backups; + backup_id = self._test_backup_create(backup_type='partial', query_args={'class': ['user_profile']}) + self._test_backup_list(backup_type='partial') + self._test_backup_detail(backup_id=backup_id, backup_type='partial') + self._test_backup_delete(backup_id=backup_id, backup_type='partial') From 3be4dc09d0b49c48765bee09780c6743dfc905b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 16 Jun 2016 15:15:46 +0200 Subject: [PATCH 443/558] [LIB-758] test fix; add prefix to instance name; --- tests/integration_test.py | 2 +- tests/integration_test_backups.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index c933e0e..0741ff6 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -43,7 +43,7 @@ def setUpClass(cls): super(InstanceMixin, cls).setUpClass() cls.instance = cls.connection.Instance.please.create( - name='i%s' % cls.generate_hash()[:10], + name='test_python_lib_i%s' % cls.generate_hash()[:10], description='IntegrationTest %s' % datetime.now(), ) diff --git a/tests/integration_test_backups.py b/tests/integration_test_backups.py index 6f7b2f0..bbd2143 100644 --- a/tests/integration_test_backups.py +++ b/tests/integration_test_backups.py @@ -32,7 +32,7 @@ def _test_backup_detail(self, backup_id, backup_type='full'): backup_model = self._get_backup_model(backup_type) backup = backup_model.please.get(id=backup_id) - self.assertEqual(backup.id, self.backup.id) + self.assertEqual(backup.id, backup_id) self.assertEqual(backup.author.email, self.API_EMAIL) def _test_backup_list(self, backup_type='full'): From ddd279204a570e6f52db2404e148c92e499912c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 16 Jun 2016 15:25:50 +0200 Subject: [PATCH 444/558] [LIB-758] correct prefix to instance name; --- tests/integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 0741ff6..1503bdd 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -43,7 +43,7 @@ def setUpClass(cls): super(InstanceMixin, cls).setUpClass() cls.instance = cls.connection.Instance.please.create( - name='test_python_lib_i%s' % cls.generate_hash()[:10], + name='testpythonlibi%s' % cls.generate_hash()[:10], description='IntegrationTest %s' % datetime.now(), ) From 9b177a3cd01f0aab020bf1469db0e991b11514b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 16 Jun 2016 15:42:03 +0200 Subject: [PATCH 445/558] [LIB-758] correct create test; --- tests/integration_test_backups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_backups.py b/tests/integration_test_backups.py index bbd2143..6f09836 100644 --- a/tests/integration_test_backups.py +++ b/tests/integration_test_backups.py @@ -21,7 +21,7 @@ def _test_backup_create(self, backup_type='full', query_args=None): new_backup = backup_model() backup_test = new_backup.save() - backup = Backup.please.get(id=backup_test.id) + backup = backup_model.please.get(id=backup_test.id) self.assertTrue(backup) self.assertEqual(backup.id, backup_test.id) self.assertEqual(backup.author.email, self.API_EMAIL) From f6f95de4ba9f40c27eb300073a7a2a4ac1c384ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 16 Jun 2016 15:52:46 +0200 Subject: [PATCH 446/558] [LIB-758] small fixes; --- syncano/models/backups.py | 1 + tests/integration_test.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/syncano/models/backups.py b/syncano/models/backups.py index f997793..78f53f4 100644 --- a/syncano/models/backups.py +++ b/syncano/models/backups.py @@ -61,6 +61,7 @@ class PartialBackup(Model): :ivar status_info: :class:`~syncano.models.fields.StringField` :ivar author: :class:`~syncano.models.fields.ModelField` :ivar details: :class:`~syncano.models.fields.JSONField` + :ivar query_args: :class:`~syncano.models.fields.JSONField` :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` :ivar created_at: :class:`~syncano.models.fields.DateTimeField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` diff --git a/tests/integration_test.py b/tests/integration_test.py index 1503bdd..ac015d0 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -43,7 +43,7 @@ def setUpClass(cls): super(InstanceMixin, cls).setUpClass() cls.instance = cls.connection.Instance.please.create( - name='testpythonlibi%s' % cls.generate_hash()[:10], + name='testpythonlib%s' % cls.generate_hash()[:10], description='IntegrationTest %s' % datetime.now(), ) From 884b516d692621eea130644419f4108f17e8fba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 16 Jun 2016 16:03:51 +0200 Subject: [PATCH 447/558] [LIB-758] remove partial backup for this PR; partial will be forgoten; --- syncano/models/backups.py | 47 --------------------------- tests/integration_test_backups.py | 53 ++++++++----------------------- 2 files changed, 13 insertions(+), 87 deletions(-) diff --git a/syncano/models/backups.py b/syncano/models/backups.py index 78f53f4..0d06016 100644 --- a/syncano/models/backups.py +++ b/syncano/models/backups.py @@ -47,50 +47,3 @@ class Meta: 'path': '/backups/full/', } } - - -class PartialBackup(Model): - """ - OO wrapper around backups `link `_. - - :ivar label: :class:`~syncano.models.fields.StringField` - :ivar description: :class:`~syncano.models.fields.StringField` - :ivar instance: :class:`~syncano.models.fields.StringField` - :ivar size: :class:`~syncano.models.fields.IntegerField` - :ivar status: :class:`~syncano.models.fields.StringField` - :ivar status_info: :class:`~syncano.models.fields.StringField` - :ivar author: :class:`~syncano.models.fields.ModelField` - :ivar details: :class:`~syncano.models.fields.JSONField` - :ivar query_args: :class:`~syncano.models.fields.JSONField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - """ - - label = fields.StringField(read_only=True) - description = fields.StringField(read_only=True) - - instance = fields.StringField(read_only=True) - size = fields.IntegerField(read_only=True) - status = fields.StringField(read_only=True) - status_info = fields.StringField(read_only=True) - author = fields.ModelField('Admin') - details = fields.JSONField(read_only=True) - query_args = fields.JSONField(required=True) - - updated_at = fields.DateTimeField(read_only=True, required=False) - created_at = fields.DateTimeField(read_only=True, required=False) - links = fields.LinksField() - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['get', 'delete'], - 'path': '/backups/partial/{id}/', - }, - 'list': { - 'methods': ['get', 'post'], - 'path': '/backups/partial/', - } - } diff --git a/tests/integration_test_backups.py b/tests/integration_test_backups.py index 6f09836..357db55 100644 --- a/tests/integration_test_backups.py +++ b/tests/integration_test_backups.py @@ -1,68 +1,41 @@ # -*- coding: utf-8 -*- -from syncano.models import Backup, PartialBackup +from syncano.models import Backup from tests.integration_test import InstanceMixin, IntegrationTest -class BaseBackupTestCase(InstanceMixin, IntegrationTest): +class FullBackupTestCase(InstanceMixin, IntegrationTest): - BACKUP_MODEL = { - 'full': Backup, - 'partial': PartialBackup, - } - - def _get_backup_model(self, backup_type): - return self.BACKUP_MODEL.get(backup_type) - - def _test_backup_create(self, backup_type='full', query_args=None): - backup_model = self._get_backup_model(backup_type) - if query_args is not None: - new_backup = backup_model(query_args=query_args) - else: - new_backup = backup_model() + def _test_backup_create(self): + new_backup = Backup() backup_test = new_backup.save() - backup = backup_model.please.get(id=backup_test.id) + backup = Backup.please.get(id=backup_test.id) self.assertTrue(backup) self.assertEqual(backup.id, backup_test.id) self.assertEqual(backup.author.email, self.API_EMAIL) return backup.id - def _test_backup_detail(self, backup_id, backup_type='full'): - backup_model = self._get_backup_model(backup_type) - backup = backup_model.please.get(id=backup_id) + def _test_backup_detail(self, backup_id): + backup = Backup.please.get(id=backup_id) self.assertEqual(backup.id, backup_id) self.assertEqual(backup.author.email, self.API_EMAIL) - def _test_backup_list(self, backup_type='full'): - backup_model = self._get_backup_model(backup_type) - backups = [backup for backup in backup_model.please.list()] + def _test_backup_list(self): + + backups = [backup for backup in Backup.please.list()] self.assertTrue(len(backups)) # at least one backup here; - def _test_backup_delete(self, backup_id, backup_type='full'): - backup_model = self._get_backup_model(backup_type) - backup = backup_model.please.get(id=backup_id) + def _test_backup_delete(self, backup_id): + backup = Backup.please.get(id=backup_id) backup.delete() - backups = [backup_object for backup_object in backup_model.please.list()] + backups = [backup_object for backup_object in Backup.please.list()] self.assertEqual(len(backups), 0) - -class FullBackupTestCase(BaseBackupTestCase): - def test_backup(self): # we provide one test for all functionality to avoid creating too many backups; backup_id = self._test_backup_create() self._test_backup_list() self._test_backup_detail(backup_id=backup_id) self._test_backup_delete(backup_id=backup_id) - - -class PartialBackupTestCase(BaseBackupTestCase): - - def test_backup(self): - # we provide one test for all functionality to avoid creating too many backups; - backup_id = self._test_backup_create(backup_type='partial', query_args={'class': ['user_profile']}) - self._test_backup_list(backup_type='partial') - self._test_backup_detail(backup_id=backup_id, backup_type='partial') - self._test_backup_delete(backup_id=backup_id, backup_type='partial') From 478e325f172f4673b55cd9f23062a755c9df4ace Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Tue, 28 Jun 2016 10:13:20 +0200 Subject: [PATCH 448/558] [LIB-783] - move logic checking available endpoint request methods to Meta class, apply changes to ScriptEndpoint model --- syncano/models/archetypes.py | 8 ++++++++ syncano/models/incentives.py | 11 +++++------ syncano/models/options.py | 4 ++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index f47f8ae..4909fe7 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -275,3 +275,11 @@ def get_endpoint_data(self): if field.has_endpoint_data: properties[field.name] = getattr(self, field.name) return properties + + def make_endpoint_request(self, connection, endpoint_name, http_method, **kwargs): + if self._meta.is_http_method_available_for_endpoint(http_method, endpoint_name): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint(endpoint_name, properties) + return connection.request(http_method, endpoint, **kwargs) + else: + raise SyncanoValidationError('HTTP method {0} not allowed for endpoint name "{1}".'.format(http_method, endpoint_name)) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 3c6105b..05a9e74 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -267,8 +267,7 @@ def run(self, cache_key=None, **payload): if self.is_new(): raise SyncanoValidationError('Method allowed only on existing model.') - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('run', properties) + endpoint_name = 'run' connection = self._get_connection(**payload) params = {} @@ -279,7 +278,7 @@ def run(self, cache_key=None, **payload): if params: kwargs.update({'params': params}) - response = connection.request('POST', endpoint, **kwargs) + response = self.make_endpoint_request(connection, endpoint_name, 'POST', **kwargs) if isinstance(response, dict) and 'result' in response and 'stdout' in response['result']: response.update({'instance_name': self.instance_name, @@ -295,11 +294,11 @@ def reset_link(self): >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') >>> se.reset_link() """ - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('reset', properties) + endpoint_name = 'reset' connection = self._get_connection() - response = connection.request('POST', endpoint) + response = self.make_endpoint_request(connection, endpoint_name, 'POST') + self.public_link = response['public_link'] diff --git a/syncano/models/options.py b/syncano/models/options.py index f008be3..e260120 100644 --- a/syncano/models/options.py +++ b/syncano/models/options.py @@ -142,6 +142,10 @@ def resolve_endpoint(self, name, properties): return endpoint['path'].format(**properties) + def is_http_method_available_for_endpoint(self, http_method_name, endpoint_name): + available_methods = self.get_endpoint_methods(endpoint_name) + return http_method_name.lower() in available_methods + def get_endpoint_query_params(self, name, params): properties = self.get_endpoint_properties(name) return {k: v for k, v in six.iteritems(params) if k not in properties} From d84dbab1908f65f62ade669b248a4506ee582580 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Tue, 28 Jun 2016 10:55:00 +0200 Subject: [PATCH 449/558] [LIB-783] - move logic checking available endpoint request methods to Meta class --- syncano/models/archetypes.py | 8 -------- syncano/models/incentives.py | 13 ++++++++----- syncano/models/options.py | 16 ++++++++++------ 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index 4909fe7..f47f8ae 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -275,11 +275,3 @@ def get_endpoint_data(self): if field.has_endpoint_data: properties[field.name] = getattr(self, field.name) return properties - - def make_endpoint_request(self, connection, endpoint_name, http_method, **kwargs): - if self._meta.is_http_method_available_for_endpoint(http_method, endpoint_name): - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint(endpoint_name, properties) - return connection.request(http_method, endpoint, **kwargs) - else: - raise SyncanoValidationError('HTTP method {0} not allowed for endpoint name "{1}".'.format(http_method, endpoint_name)) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 05a9e74..baf901b 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -267,7 +267,9 @@ def run(self, cache_key=None, **payload): if self.is_new(): raise SyncanoValidationError('Method allowed only on existing model.') - endpoint_name = 'run' + properties = self.get_endpoint_data() + endpoint_http_method = 'POST' + endpoint = self._meta.resolve_endpoint('run', properties, endpoint_http_method) connection = self._get_connection(**payload) params = {} @@ -278,7 +280,7 @@ def run(self, cache_key=None, **payload): if params: kwargs.update({'params': params}) - response = self.make_endpoint_request(connection, endpoint_name, 'POST', **kwargs) + response = connection.request(endpoint_http_method, endpoint, **kwargs) if isinstance(response, dict) and 'result' in response and 'stdout' in response['result']: response.update({'instance_name': self.instance_name, @@ -294,11 +296,12 @@ def reset_link(self): >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') >>> se.reset_link() """ - endpoint_name = 'reset' + properties = self.get_endpoint_data() + endpoint_http_method = 'POST' + endpoint = self._meta.resolve_endpoint('reset', properties, endpoint_http_method) connection = self._get_connection() - response = self.make_endpoint_request(connection, endpoint_name, 'POST') - + response = connection.request(endpoint_http_method, endpoint) self.public_link = response['public_link'] diff --git a/syncano/models/options.py b/syncano/models/options.py index e260120..dc900c9 100644 --- a/syncano/models/options.py +++ b/syncano/models/options.py @@ -3,7 +3,7 @@ import six from syncano.connection import ConnectionMixin -from syncano.exceptions import SyncanoValueError +from syncano.exceptions import SyncanoValidationError, SyncanoValueError from syncano.models.registry import registry from syncano.utils import camelcase_to_underscore @@ -133,12 +133,16 @@ def get_endpoint_methods(self, name): endpoint = self.get_endpoint(name) return endpoint['methods'] - def resolve_endpoint(self, name, properties): - endpoint = self.get_endpoint(name) + def resolve_endpoint(self, endpoint_name, properties, http_method=None): + if http_method and not self.is_http_method_available_for_endpoint(http_method, endpoint_name): + raise SyncanoValidationError( + 'HTTP method {0} not allowed for endpoint name "{1}".'.format(http_method, endpoint_name) + ) + endpoint = self.get_endpoint(endpoint_name) - for name in endpoint['properties']: - if name not in properties: - raise SyncanoValueError('Request property "{0}" is required.'.format(name)) + for endpoint_name in endpoint['properties']: + if endpoint_name not in properties: + raise SyncanoValueError('Request property "{0}" is required.'.format(endpoint_name)) return endpoint['path'].format(**properties) From 0303de29d7ec334a3ac9ac983e36bd740942ea31 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Tue, 28 Jun 2016 12:14:34 +0200 Subject: [PATCH 450/558] [LIB-783] - check available http methods for endpoint calls: apply changes to all models --- syncano/models/accounts.py | 14 ++++++++------ syncano/models/archetypes.py | 12 +++++++----- syncano/models/channels.py | 7 ++++--- syncano/models/data_views.py | 15 +++++++++------ syncano/models/incentives.py | 22 ++++++++++++---------- syncano/models/options.py | 6 +++--- 6 files changed, 43 insertions(+), 33 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 16639d2..1929aaf 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -134,13 +134,15 @@ class Meta: def reset_key(self): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('reset_key', properties) + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('reset_key', properties, http_method) connection = self._get_connection() - return connection.request('POST', endpoint) + return connection.request(http_method, endpoint) def auth(self, username=None, password=None): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('auth', properties) + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('auth', properties, http_method) connection = self._get_connection() if not (username and password): @@ -151,11 +153,11 @@ def auth(self, username=None, password=None): 'password': password } - return connection.request('POST', endpoint, data=data) + return connection.request(http_method, endpoint, data=data) def _user_groups_method(self, group_id=None, method='GET'): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('groups', properties) + endpoint = self._meta.resolve_endpoint('groups', properties, method) if group_id is not None and method != 'POST': endpoint += '{}/'.format(group_id) @@ -225,7 +227,7 @@ class Meta: def _group_users_method(self, user_id=None, method='GET'): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('users', properties) + endpoint = self._meta.resolve_endpoint('users', properties, method) if user_id is not None and method != 'POST': endpoint += '{}/'.format(user_id) connection = self._get_connection() diff --git a/syncano/models/archetypes.py b/syncano/models/archetypes.py index f47f8ae..236da8b 100644 --- a/syncano/models/archetypes.py +++ b/syncano/models/archetypes.py @@ -134,7 +134,7 @@ def save(self, **kwargs): if 'put' in methods: method = 'PUT' - endpoint = self._meta.resolve_endpoint(endpoint_name, properties) + endpoint = self._meta.resolve_endpoint(endpoint_name, properties, method) if 'expected_revision' in kwargs: data.update({'expected_revision': kwargs['expected_revision']}) request = {'data': data} @@ -171,9 +171,10 @@ def delete(self, **kwargs): raise SyncanoValidationError('Method allowed only on existing model.') properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('detail', properties) + http_method = 'DELETE' + endpoint = self._meta.resolve_endpoint('detail', properties, http_method) connection = self._get_connection(**kwargs) - connection.request('DELETE', endpoint) + connection.request(http_method, endpoint) if self.__class__.__name__ == 'Instance': # avoid circular import; registry.clear_used_instance() self._raw_data = {} @@ -185,9 +186,10 @@ def reload(self, **kwargs): raise SyncanoValidationError('Method allowed only on existing model.') properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('detail', properties) + http_method = 'GET' + endpoint = self._meta.resolve_endpoint('detail', properties, http_method) connection = self._get_connection(**kwargs) - response = connection.request('GET', endpoint) + response = connection.request(http_method, endpoint) self.to_python(response) def validate(self): diff --git a/syncano/models/channels.py b/syncano/models/channels.py index 4a68e21..0db4a3d 100644 --- a/syncano/models/channels.py +++ b/syncano/models/channels.py @@ -138,7 +138,7 @@ class Meta: def poll(self, room=None, last_id=None, callback=None, error=None, timeout=None): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('poll', properties) + endpoint = self._meta.resolve_endpoint('poll', properties, 'GET') connection = self._get_connection() thread = PollThread(connection, endpoint, callback, error, timeout=timeout, @@ -148,10 +148,11 @@ def poll(self, room=None, last_id=None, callback=None, error=None, timeout=None) def publish(self, payload, room=None): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('publish', properties) + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('publish', properties, http_method) connection = self._get_connection() request = {'data': Message(payload=payload, room=room).to_native()} - response = connection.request('POST', endpoint, **request) + response = connection.request(http_method, endpoint, **request) return Message(**response) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 5f3e77a..15df2d8 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -66,24 +66,27 @@ class Meta: def rename(self, new_name): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('rename', properties) + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('rename', properties, http_method) connection = self._get_connection() - return connection.request('POST', + return connection.request(http_method, endpoint, data={'new_name': new_name}) def clear_cache(self): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('clear_cache', properties) + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('clear_cache', properties, http_method) connection = self._get_connection() - return connection.request('POST', endpoint) + return connection.request(http_method, endpoint) def get(self, cache_key=None, **kwargs): connection = self._get_connection() properties = self.get_endpoint_data() query = Object.please._build_query(query_data=kwargs, class_name=self.class_name) - endpoint = self._meta.resolve_endpoint('get', properties) + http_method = 'GET' + endpoint = self._meta.resolve_endpoint('get', properties, http_method) kwargs = {} params = {} @@ -96,7 +99,7 @@ def get(self, cache_key=None, **kwargs): kwargs = {'params': params} while endpoint is not None: - response = connection.request('GET', endpoint, **kwargs) + response = connection.request(http_method, endpoint, **kwargs) endpoint = response.get('next') for obj in response['objects']: yield obj diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index baf901b..12a0d4d 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -100,14 +100,15 @@ def run(self, **payload): raise SyncanoValidationError('Method allowed only on existing model.') properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('run', properties) + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('run', properties, http_method) connection = self._get_connection(**payload) request = { 'data': { 'payload': json.dumps(payload) } } - response = connection.request('POST', endpoint, **request) + response = connection.request(http_method, endpoint, **request) response.update({'instance_name': self.instance_name, 'script_id': self.id}) return ScriptTrace(**response) @@ -268,8 +269,8 @@ def run(self, cache_key=None, **payload): raise SyncanoValidationError('Method allowed only on existing model.') properties = self.get_endpoint_data() - endpoint_http_method = 'POST' - endpoint = self._meta.resolve_endpoint('run', properties, endpoint_http_method) + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('run', properties, http_method) connection = self._get_connection(**payload) params = {} @@ -280,7 +281,7 @@ def run(self, cache_key=None, **payload): if params: kwargs.update({'params': params}) - response = connection.request(endpoint_http_method, endpoint, **kwargs) + response = connection.request(http_method, endpoint, **kwargs) if isinstance(response, dict) and 'result' in response and 'stdout' in response['result']: response.update({'instance_name': self.instance_name, @@ -297,11 +298,11 @@ def reset_link(self): >>> se.reset_link() """ properties = self.get_endpoint_data() - endpoint_http_method = 'POST' - endpoint = self._meta.resolve_endpoint('reset', properties, endpoint_http_method) + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('reset', properties, http_method) connection = self._get_connection() - response = connection.request(endpoint_http_method, endpoint) + response = connection.request(http_method, endpoint) self.public_link = response['public_link'] @@ -347,10 +348,11 @@ class Meta: def render(self, context=None): context = context or {} properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('render', properties) + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('render', properties, http_method) connection = self._get_connection() - return connection.request('POST', endpoint, data={'context': context}) + return connection.request(http_method, endpoint, data={'context': context}) def rename(self, new_name): rename_path = self.links.rename diff --git a/syncano/models/options.py b/syncano/models/options.py index dc900c9..605194e 100644 --- a/syncano/models/options.py +++ b/syncano/models/options.py @@ -134,9 +134,9 @@ def get_endpoint_methods(self, name): return endpoint['methods'] def resolve_endpoint(self, endpoint_name, properties, http_method=None): - if http_method and not self.is_http_method_available_for_endpoint(http_method, endpoint_name): + if http_method and not self.is_http_method_available(http_method, endpoint_name): raise SyncanoValidationError( - 'HTTP method {0} not allowed for endpoint name "{1}".'.format(http_method, endpoint_name) + 'HTTP method {0} not allowed for endpoint "{1}".'.format(http_method, endpoint_name) ) endpoint = self.get_endpoint(endpoint_name) @@ -146,7 +146,7 @@ def resolve_endpoint(self, endpoint_name, properties, http_method=None): return endpoint['path'].format(**properties) - def is_http_method_available_for_endpoint(self, http_method_name, endpoint_name): + def is_http_method_available(self, http_method_name, endpoint_name): available_methods = self.get_endpoint_methods(endpoint_name) return http_method_name.lower() in available_methods From 88d118cabf3269ed9122523d78642c5f4d487e6d Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Tue, 28 Jun 2016 12:39:10 +0200 Subject: [PATCH 451/558] [LIB-783] - tests --- syncano/models/accounts.py | 8 ++++---- syncano/models/channels.py | 2 +- syncano/models/push_notification.py | 4 ++-- syncano/models/traces.py | 8 ++++---- tests/test_options.py | 12 +++++++++++- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 1929aaf..7e2555a 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -37,7 +37,7 @@ class Meta: 'path': '/admins/{id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/admins/', } } @@ -76,7 +76,7 @@ class Meta: 'path': '/objects/{id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/objects/', } } @@ -123,7 +123,7 @@ class Meta: 'path': '/user/auth/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/users/', }, 'groups': { @@ -216,7 +216,7 @@ class Meta: 'path': '/groups/{id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/groups/', }, 'users': { diff --git a/syncano/models/channels.py b/syncano/models/channels.py index 0db4a3d..64382c7 100644 --- a/syncano/models/channels.py +++ b/syncano/models/channels.py @@ -190,7 +190,7 @@ class Meta: 'path': '/history/{pk}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/history/', }, } diff --git a/syncano/models/push_notification.py b/syncano/models/push_notification.py index 8dc7f43..b8a7c86 100644 --- a/syncano/models/push_notification.py +++ b/syncano/models/push_notification.py @@ -197,7 +197,7 @@ class Meta: 'path': '/push_notifications/gcm/messages/{id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/push_notifications/gcm/messages/', } } @@ -244,7 +244,7 @@ class Meta: 'path': '/push_notifications/apns/messages/{id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/push_notifications/apns/messages/', } } diff --git a/syncano/models/traces.py b/syncano/models/traces.py index 04677eb..e99003a 100644 --- a/syncano/models/traces.py +++ b/syncano/models/traces.py @@ -36,7 +36,7 @@ class Meta: 'path': '/traces/{id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/traces/', } } @@ -71,7 +71,7 @@ class Meta: 'path': '/traces/{id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/traces/', } } @@ -109,7 +109,7 @@ class Meta: 'path': '/traces/{id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/traces/', } } @@ -144,7 +144,7 @@ class Meta: 'path': '/traces/{id}/', }, 'list': { - 'methods': ['get'], + 'methods': ['get', 'post'], 'path': '/traces/', } } diff --git a/tests/test_options.py b/tests/test_options.py index 3a7831d..5bef5dd 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,6 +1,6 @@ import unittest -from syncano.exceptions import SyncanoValueError +from syncano.exceptions import SyncanoValidationError, SyncanoValueError from syncano.models import Field, Instance from syncano.models.options import Options @@ -149,3 +149,13 @@ def test_get_path_properties(self): path = '/{a}/{b}-{c}/dummy-{d}/' properties = self.options.get_path_properties(path) self.assertEqual(properties, ['a', 'b']) + + def test_resolve_endpoint_with_missing_http_method(self): + properties = {'instance_name': 'test'} + with self.assertRaises(SyncanoValidationError): + self.options.resolve_endpoint('list', properties, 'DELETE') + + def test_resolve_endpoint_with_specified_http_method(self): + properties = {'instance_name': 'test', 'a': 'a', 'b': 'b'} + path = self.options.resolve_endpoint('dummy', properties, 'GET') + self.assertEqual(path, '/v1.1/instances/test/v1.1/dummy/a/b/') From 35de4a602bdf1c10f1667d0df10ece905f4c7669 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Tue, 28 Jun 2016 12:51:54 +0200 Subject: [PATCH 452/558] [LIB-783] - fix user integration tests --- syncano/models/accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/accounts.py b/syncano/models/accounts.py index 7e2555a..1e79eca 100644 --- a/syncano/models/accounts.py +++ b/syncano/models/accounts.py @@ -127,7 +127,7 @@ class Meta: 'path': '/users/', }, 'groups': { - 'methods': ['get', 'post'], + 'methods': ['get', 'post', 'delete'], 'path': '/users/{id}/groups/', } } From 1a80368bd25099ab81b8d992d3df546f655f65b4 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Tue, 28 Jun 2016 17:32:03 +0200 Subject: [PATCH 453/558] [LIB-757] - add possibility to restore backups --- syncano/models/backups.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/syncano/models/backups.py b/syncano/models/backups.py index 0d06016..822be92 100644 --- a/syncano/models/backups.py +++ b/syncano/models/backups.py @@ -45,5 +45,21 @@ class Meta: 'list': { 'methods': ['post', 'get'], 'path': '/backups/full/', + }, + 'restore': { + 'methods': ['post'], + 'path': '/restores/', } } + + def restore(self): + properties = self.get_endpoint_data() + endpoint = self._meta.resolve_endpoint('restore', properties) + kwargs = { + 'data': { + 'backup': self.id + } + } + connection = self._get_connection() + connection.request('POST', endpoint, **kwargs) + From cf361bab3564abdac63e847447f6384cedba6efb Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Tue, 28 Jun 2016 17:32:37 +0200 Subject: [PATCH 454/558] [LIB-757] - remove obsolete blank lines --- syncano/models/backups.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncano/models/backups.py b/syncano/models/backups.py index 822be92..f4536e1 100644 --- a/syncano/models/backups.py +++ b/syncano/models/backups.py @@ -62,4 +62,3 @@ def restore(self): } connection = self._get_connection() connection.request('POST', endpoint, **kwargs) - From 3a1d0b622f6bcb010d9846d98625d9aeb26bc118 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Tue, 28 Jun 2016 18:17:03 +0200 Subject: [PATCH 455/558] [LIB-757] - test --- tests/integration_test_backups.py | 34 ++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/integration_test_backups.py b/tests/integration_test_backups.py index 357db55..6661816 100644 --- a/tests/integration_test_backups.py +++ b/tests/integration_test_backups.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- -from syncano.models import Backup +import time + +from syncano.models import Backup, Class, Instance from tests.integration_test import InstanceMixin, IntegrationTest @@ -27,6 +29,35 @@ def _test_backup_list(self): backups = [backup for backup in Backup.please.list()] self.assertTrue(len(backups)) # at least one backup here; + def _test_backup_restore(self, backup_id): + backup = Backup.please.get(id=backup_id) + instance_name = backup.instance + instance = Instance.please.get(name=instance_name) + classes_count = len(list(instance.classes)) + + test_class = Class(name='testclass') + test_class.save() + instance.reload() + classes_count_after_update = len(list(instance.classes)) + + self.assertTrue( + classes_count_after_update - classes_count == 1, + 'There should be only 1 more instance class after new class creation.' + ) + + # wait for backup to be restored + backup.restore() + time.sleep(10) + + instance.reload() + classes_count_after_restore = len(list(instance.classes)) + + self.assertEqual( + classes_count, + classes_count_after_restore, + 'Classes count after restore should be equal to original classes count.' + ) + def _test_backup_delete(self, backup_id): backup = Backup.please.get(id=backup_id) backup.delete() @@ -38,4 +69,5 @@ def test_backup(self): backup_id = self._test_backup_create() self._test_backup_list() self._test_backup_detail(backup_id=backup_id) + self._test_backup_restore(backup_id=backup_id) self._test_backup_delete(backup_id=backup_id) From 5d8a1cec2d2274b15c1f25f325c28a05eea6b1ce Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Wed, 29 Jun 2016 07:53:50 +0200 Subject: [PATCH 456/558] [LIB-757] - improve test --- tests/integration_test_backups.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_backups.py b/tests/integration_test_backups.py index 6661816..8fb60a7 100644 --- a/tests/integration_test_backups.py +++ b/tests/integration_test_backups.py @@ -45,9 +45,12 @@ def _test_backup_restore(self, backup_id): 'There should be only 1 more instance class after new class creation.' ) - # wait for backup to be restored + # wait for backup to be truly saved and restored + while backup.status != 'success': + time.sleep(1) + backup.reload() backup.restore() - time.sleep(10) + time.sleep(15) instance.reload() classes_count_after_restore = len(list(instance.classes)) From 4a62a9a45b366e85bc47b44ee7d3e4e90710e2ef Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Wed, 29 Jun 2016 08:23:05 +0200 Subject: [PATCH 457/558] [LIB-757] - make backup restore synchronous --- syncano/models/backups.py | 9 ++++++++- tests/integration_test_backups.py | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/syncano/models/backups.py b/syncano/models/backups.py index f4536e1..5d2c50c 100644 --- a/syncano/models/backups.py +++ b/syncano/models/backups.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import time + from . import fields from .base import Model from .instances import Instance @@ -61,4 +63,9 @@ def restore(self): } } connection = self._get_connection() - connection.request('POST', endpoint, **kwargs) + response = connection.request('POST', endpoint, **kwargs) + restore_endpoint = response['links']['self'] + restore_response = connection.request('GET', restore_endpoint) + while restore_response['status'] == 'running': + time.sleep(1) + restore_response = connection.request('GET', restore_endpoint) diff --git a/tests/integration_test_backups.py b/tests/integration_test_backups.py index 8fb60a7..c6de7f0 100644 --- a/tests/integration_test_backups.py +++ b/tests/integration_test_backups.py @@ -45,12 +45,12 @@ def _test_backup_restore(self, backup_id): 'There should be only 1 more instance class after new class creation.' ) - # wait for backup to be truly saved and restored + # wait for backup to be saved while backup.status != 'success': time.sleep(1) backup.reload() + backup.restore() - time.sleep(15) instance.reload() classes_count_after_restore = len(list(instance.classes)) From b7606bcd96cbb32fd0ca32f488e243b13c87f8f5 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Thu, 30 Jun 2016 15:36:12 +0200 Subject: [PATCH 458/558] [LIB-783] - Pass method type to resolve_endpoint as named parameter. --- syncano/models/channels.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/channels.py b/syncano/models/channels.py index 64382c7..65bdbd3 100644 --- a/syncano/models/channels.py +++ b/syncano/models/channels.py @@ -138,7 +138,7 @@ class Meta: def poll(self, room=None, last_id=None, callback=None, error=None, timeout=None): properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('poll', properties, 'GET') + endpoint = self._meta.resolve_endpoint('poll', properties, http_method='GET') connection = self._get_connection() thread = PollThread(connection, endpoint, callback, error, timeout=timeout, From d84be9de5ade6a8a54c2e46e1f76be4fe3f8756c Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Thu, 30 Jun 2016 16:35:05 +0200 Subject: [PATCH 459/558] [LIB-757] - Remove unused imports. --- syncano/models/backups.py | 45 ++++++++++++++++++------------- tests/integration_test_backups.py | 36 +++++++------------------ 2 files changed, 35 insertions(+), 46 deletions(-) diff --git a/syncano/models/backups.py b/syncano/models/backups.py index 5d2c50c..bf77353 100644 --- a/syncano/models/backups.py +++ b/syncano/models/backups.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -import time - from . import fields from .base import Model from .instances import Instance @@ -48,24 +46,33 @@ class Meta: 'methods': ['post', 'get'], 'path': '/backups/full/', }, - 'restore': { - 'methods': ['post'], - 'path': '/restores/', - } } - def restore(self): - properties = self.get_endpoint_data() - endpoint = self._meta.resolve_endpoint('restore', properties) - kwargs = { - 'data': { - 'backup': self.id + def schedule_restore(self): + restore = Restore(backup=self.id).save() + return restore + + +class Restore(Model): + + author = fields.ModelField('Admin') + status = fields.StringField(read_only=True) + status_info = fields.StringField(read_only=True) + updated_at = fields.DateTimeField(read_only=True, required=False) + created_at = fields.DateTimeField(read_only=True, required=False) + links = fields.LinksField() + backup = fields.StringField() + archive = fields.StringField(read_only=True) + + class Meta: + parent = Instance + endpoints = { + 'list': { + 'methods': ['get', 'post'], + 'path': '/restores/', + }, + 'detail': { + 'methods': ['get'], + 'path': '/restores/{id}/', } } - connection = self._get_connection() - response = connection.request('POST', endpoint, **kwargs) - restore_endpoint = response['links']['self'] - restore_response = connection.request('GET', restore_endpoint) - while restore_response['status'] == 'running': - time.sleep(1) - restore_response = connection.request('GET', restore_endpoint) diff --git a/tests/integration_test_backups.py b/tests/integration_test_backups.py index c6de7f0..b78f870 100644 --- a/tests/integration_test_backups.py +++ b/tests/integration_test_backups.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import time -from syncano.models import Backup, Class, Instance +from syncano.models import Backup from tests.integration_test import InstanceMixin, IntegrationTest @@ -29,37 +29,19 @@ def _test_backup_list(self): backups = [backup for backup in Backup.please.list()] self.assertTrue(len(backups)) # at least one backup here; - def _test_backup_restore(self, backup_id): + def _test_backup_schedule_restore(self, backup_id): backup = Backup.please.get(id=backup_id) - instance_name = backup.instance - instance = Instance.please.get(name=instance_name) - classes_count = len(list(instance.classes)) - - test_class = Class(name='testclass') - test_class.save() - instance.reload() - classes_count_after_update = len(list(instance.classes)) - - self.assertTrue( - classes_count_after_update - classes_count == 1, - 'There should be only 1 more instance class after new class creation.' - ) # wait for backup to be saved - while backup.status != 'success': + seconds_waited = 0 + while backup.status in ['scheduled', 'running']: + seconds_waited += 1 + self.assertTrue(seconds_waited < 20, 'Waiting for backup to be saved takes too long.') time.sleep(1) backup.reload() - backup.restore() - - instance.reload() - classes_count_after_restore = len(list(instance.classes)) - - self.assertEqual( - classes_count, - classes_count_after_restore, - 'Classes count after restore should be equal to original classes count.' - ) + restore = backup.schedule_restore() + self.assertIn(restore.status, ['success', 'scheduled']) def _test_backup_delete(self, backup_id): backup = Backup.please.get(id=backup_id) @@ -72,5 +54,5 @@ def test_backup(self): backup_id = self._test_backup_create() self._test_backup_list() self._test_backup_detail(backup_id=backup_id) - self._test_backup_restore(backup_id=backup_id) + self._test_backup_schedule_restore(backup_id=backup_id) self._test_backup_delete(backup_id=backup_id) From 80ac51781334282cce0bfe49db1d88d60f2401bb Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Wed, 6 Jul 2016 19:03:01 +0200 Subject: [PATCH 460/558] [LIB-800] - Add possibility to define global config --- syncano/models/instances.py | 19 +++++++++++++++++ tests/integration_test_snippet_config.py | 26 ++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/integration_test_snippet_config.py diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 59c3360..093a043 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -74,9 +74,28 @@ class Meta: 'list': { 'methods': ['post', 'get'], 'path': '/v1.1/instances/', + }, + 'config': { + 'methods': ['put', 'get'], + 'path': '/v1.1/instances/{name}/snippets/config/', } } + def get_config(self): + properties = self.get_endpoint_data() + http_method = 'GET' + endpoint = self._meta.resolve_endpoint('config', properties, http_method) + connection = self._get_connection() + return connection.request(http_method, endpoint)['config'] + + def set_config(self, config): + properties = self.get_endpoint_data() + http_method = 'PUT' + endpoint = self._meta.resolve_endpoint('config', properties, http_method) + data = {'config': config} + connection = self._get_connection() + connection.request(http_method, endpoint, data=data) + class ApiKey(Model): """ diff --git a/tests/integration_test_snippet_config.py b/tests/integration_test_snippet_config.py new file mode 100644 index 0000000..8fb883f --- /dev/null +++ b/tests/integration_test_snippet_config.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from syncano.exceptions import SyncanoRequestError +from tests.integration_test import InstanceMixin, IntegrationTest + + +class SnippetConfigTest(InstanceMixin, IntegrationTest): + + def test_update_config(self): + config = { + 'num': 123, + 'foo': 'bar', + 'arr': [1, 2, 3, 4], + 'another': { + 'num': 123, + 'foo': 'bar', + 'arr': [1, 2, 3, 4] + } + } + self.instance.set_config(config) + saved_config = self.instance.get_config() + self.assertDictContainsSubset(config, saved_config, 'Retrieved config should be equal to saved config.') + + def test_update_invalid_config(self): + with self.assertRaises(SyncanoRequestError): + self.instance.set_config('invalid config') \ No newline at end of file From 293a5473541efb0b71e4dd793916386e22a5c9f6 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Wed, 6 Jul 2016 19:07:28 +0200 Subject: [PATCH 461/558] Resolve isort issues. --- tests/integration_test_snippet_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_snippet_config.py b/tests/integration_test_snippet_config.py index 8fb883f..67b1b0e 100644 --- a/tests/integration_test_snippet_config.py +++ b/tests/integration_test_snippet_config.py @@ -23,4 +23,4 @@ def test_update_config(self): def test_update_invalid_config(self): with self.assertRaises(SyncanoRequestError): - self.instance.set_config('invalid config') \ No newline at end of file + self.instance.set_config('invalid config') From eda493e23c8a26913ea47435e56d46105311d3a0 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Thu, 7 Jul 2016 12:01:41 +0200 Subject: [PATCH 462/558] [LIB-800] - Add config validation before sending request to API. --- syncano/models/instances.py | 12 ++++++++++++ tests/integration_test_snippet_config.py | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/syncano/models/instances.py b/syncano/models/instances.py index 093a043..fbe93e4 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -1,4 +1,7 @@ +import json +import six +from syncano.exceptions import SyncanoValueError from . import fields from .base import Model @@ -89,6 +92,15 @@ def get_config(self): return connection.request(http_method, endpoint)['config'] def set_config(self, config): + if isinstance(config, six.string_types): + try: + config = json.loads(config) + except (ValueError, TypeError): + raise SyncanoValueError('Config string is not a parsable JSON.') + + if not isinstance(config, dict): + raise SyncanoValueError('Retrieved Config is not a valid dict object.') + properties = self.get_endpoint_data() http_method = 'PUT' endpoint = self._meta.resolve_endpoint('config', properties, http_method) diff --git a/tests/integration_test_snippet_config.py b/tests/integration_test_snippet_config.py index 67b1b0e..70521e8 100644 --- a/tests/integration_test_snippet_config.py +++ b/tests/integration_test_snippet_config.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from syncano.exceptions import SyncanoRequestError +from syncano.exceptions import SyncanoValueError from tests.integration_test import InstanceMixin, IntegrationTest @@ -22,5 +22,7 @@ def test_update_config(self): self.assertDictContainsSubset(config, saved_config, 'Retrieved config should be equal to saved config.') def test_update_invalid_config(self): - with self.assertRaises(SyncanoRequestError): + with self.assertRaises(SyncanoValueError): self.instance.set_config('invalid config') + with self.assertRaises(SyncanoValueError): + self.instance.set_config([1, 2, 3]) From f4c2aec0502f539044103cc1b50e708f2eeb0efd Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Thu, 7 Jul 2016 13:45:35 +0200 Subject: [PATCH 463/558] [LIB-802] - Add possibility to use template on DataEndpoint - on get() --- syncano/models/data_views.py | 32 ++++++++-- tests/integration_test_data_endpoint.py | 83 +++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 tests/integration_test_data_endpoint.py diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index 15df2d8..ae0384d 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -1,5 +1,9 @@ import json +import six +from syncano.exceptions import SyncanoValueError +from syncano.models.incentives import ResponseTemplate + from . import fields from .base import Model, Object from .instances import Instance @@ -80,7 +84,7 @@ def clear_cache(self): connection = self._get_connection() return connection.request(http_method, endpoint) - def get(self, cache_key=None, **kwargs): + def get(self, cache_key=None, response_template=None, **kwargs): connection = self._get_connection() properties = self.get_endpoint_data() query = Object.please._build_query(query_data=kwargs, class_name=self.class_name) @@ -98,8 +102,28 @@ def get(self, cache_key=None, **kwargs): if params: kwargs = {'params': params} + if response_template: + template_name = self._get_response_template_name(response_template) + kwargs['headers'] = { + 'X-TEMPLATE-RESPONSE': template_name + } + while endpoint is not None: response = connection.request(http_method, endpoint, **kwargs) - endpoint = response.get('next') - for obj in response['objects']: - yield obj + if isinstance(response, six.string_types): + endpoint = None + yield response + else: + endpoint = response.get('next') + for obj in response['objects']: + yield obj + + def _get_response_template_name(self, response_template): + name = response_template + if isinstance(response_template, ResponseTemplate): + name = response_template.name + if not isinstance(name, six.string_types): + raise SyncanoValueError( + 'Invalid response_template. Must be template\'s name or ResponseTemplate object.' + ) + return name diff --git a/tests/integration_test_data_endpoint.py b/tests/integration_test_data_endpoint.py new file mode 100644 index 0000000..0fc040a --- /dev/null +++ b/tests/integration_test_data_endpoint.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +import re + +from syncano.models import Class, DataEndpoint, Object, ResponseTemplate +from tests.integration_test import InstanceMixin, IntegrationTest + + +class DataEndpointTest(InstanceMixin, IntegrationTest): + + schema = [ + { + 'name': 'title', + 'type': 'string', + 'order_index': True, + 'filter_index': True + + } + ] + + template_content = ''' + {% if action == 'list' %} + {% set objects = response.objects %} + {% elif action == 'retrieve' %} + {% set objects = [response] %} + {% else %} + {% set objects = [] %} + {% endif %} + {% if objects %} + + + + {% for key in objects[0] if key not in fields_to_skip %} + {{ key }} + {% endfor %} + + {% for object in objects %} + + {% for key, value in object.iteritems() if key not in fields_to_skip %} + {{ value }} + {% endfor %} + + {% endfor %} +
    + {% endif %} + ''' + + template_context = { + "tr_header_classes": "", + "th_header_classes": "", + "tr_row_classes": "", + "table_classes": "", + "td_row_classes": "", + "fields_to_skip": [ + "id", + "channel", + "channel_room", + "group", + "links", + "group_permissions", + "owner_permissions", + "other_permissions", + "owner", + "revision", + "updated_at", + "created_at" + ] + } + + def test_template_response(self): + Class(name='test_class', schema=self.schema).save() + Object(class_name='test_class', title='test_title').save() + template = ResponseTemplate( + name='test_template', + content=self.template_content, + content_type='text/html', + context=self.template_context + ).save() + data_endpoint = DataEndpoint(name='test_endpoint', class_name='test_class').save() + + response = list(data_endpoint.get(response_template=template)) + self.assertEqual(len(response), 1, 'Data endpoint should return 1 element if queried with response_template.') + data = re.sub('[\s+]', '', response[0]) + self.assertEqual(data, '
    title
    test_title
    ') From 8624d23398ee7e62c9c11754523bf8d4a843cd27 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Thu, 7 Jul 2016 14:21:22 +0200 Subject: [PATCH 464/558] [LIB-800] -Improve tests. --- tests/integration_test_snippet_config.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/integration_test_snippet_config.py b/tests/integration_test_snippet_config.py index 70521e8..de0eba7 100644 --- a/tests/integration_test_snippet_config.py +++ b/tests/integration_test_snippet_config.py @@ -26,3 +26,18 @@ def test_update_invalid_config(self): self.instance.set_config('invalid config') with self.assertRaises(SyncanoValueError): self.instance.set_config([1, 2, 3]) + + def test_update_existing_config(self): + config = { + 'foo': 'bar' + } + self.instance.set_config(config) + saved_config = self.instance.get_config() + self.assertIn('foo', saved_config, 'Retrieved config should contain saved key.') + new_config = { + 'new_foo': 'new_bar' + } + self.instance.set_config(new_config) + saved_config = self.instance.get_config() + self.assertDictContainsSubset(new_config, saved_config, 'Retrieved config should be equal to saved config.') + self.assertNotIn('foo', saved_config, 'Retrieved config should not contain old keys.') From e2d09b6cd00ff3028d43a650eb8348ee3d3a1a39 Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Sat, 9 Jul 2016 15:19:04 +0200 Subject: [PATCH 465/558] [LIB-802] - Remove obsolete blank lines in the code. --- tests/integration_test_data_endpoint.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration_test_data_endpoint.py b/tests/integration_test_data_endpoint.py index 0fc040a..4cccd9f 100644 --- a/tests/integration_test_data_endpoint.py +++ b/tests/integration_test_data_endpoint.py @@ -13,7 +13,6 @@ class DataEndpointTest(InstanceMixin, IntegrationTest): 'type': 'string', 'order_index': True, 'filter_index': True - } ] From 039f38685ebe7b67d8be4e5f40be11f3641adbd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 19 Jul 2016 13:02:54 +0200 Subject: [PATCH 466/558] [LIB-BC] add backward compatibility import EndpointData --- syncano/models/base.py | 1 + tests/integration_test.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/syncano/models/base.py b/syncano/models/base.py index 31eb2dc..67689e2 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -11,3 +11,4 @@ from .push_notification import * # NOQA from .geo import * # NOQA from .backups import * # NOQA +from .data_views import DataEndpoint as EndpointData # NOQA \ No newline at end of file diff --git a/tests/integration_test.py b/tests/integration_test.py index ac015d0..3c395db 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -655,3 +655,9 @@ def test_data_endpoint_filtering(self): objects = [object for object in data_endpoint.get(test1__eq='atest')] self.assertEqual(len(objects), 1) + + def test_backward_compatibility_name(self): + from syncano.models import EndpointData + + data_endpoint = EndpointData.objects.get(name='test_data_endpoint') + self.assertEqual(data_endpoint.class_name, 'sample_klass') From 06a148101bd8f223b53e200e824edf8575c1e7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 19 Jul 2016 13:12:59 +0200 Subject: [PATCH 467/558] [LIB-BC] add backward compatibility import EndpointData --- tests/integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 3c395db..622722c 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -659,5 +659,5 @@ def test_data_endpoint_filtering(self): def test_backward_compatibility_name(self): from syncano.models import EndpointData - data_endpoint = EndpointData.objects.get(name='test_data_endpoint') + data_endpoint = EndpointData.please.get(name='test_data_endpoint') self.assertEqual(data_endpoint.class_name, 'sample_klass') From 9c45225f0a799471b44611ceb2b2f98feabe4e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 19 Jul 2016 14:19:45 +0200 Subject: [PATCH 468/558] [RELEASE v5.2.0] Bump the version --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 21ec7f5..8882af2 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.1.0' +__version__ = '5.2.0' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From b60be8e0756d01d713294eb60a837da37bf5b3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Jul 2016 12:49:33 +0200 Subject: [PATCH 469/558] [LIB-813] add support for hosting management; --- syncano/models/base.py | 1 + syncano/models/hosting.py | 52 ++++++++++++++++++++++++++++++ syncano/models/instances.py | 1 + tests/integration_tests_hosting.py | 25 ++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 syncano/models/hosting.py create mode 100644 tests/integration_tests_hosting.py diff --git a/syncano/models/base.py b/syncano/models/base.py index 67689e2..38c24b5 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -11,4 +11,5 @@ from .push_notification import * # NOQA from .geo import * # NOQA from .backups import * # NOQA +from .hosting import * # NOQA from .data_views import DataEndpoint as EndpointData # NOQA \ No newline at end of file diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py new file mode 100644 index 0000000..d849bc4 --- /dev/null +++ b/syncano/models/hosting.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +import requests + +from . import fields +from .base import Instance, Model, logger + + +class Hosting(Model): + """ + OO wrapper around hosting. + """ + + label = fields.StringField(max_length=64, primary_key=True) + description = fields.StringField(read_only=False, required=False) + domains = fields.JSONField(default=[]) + + links = fields.LinksField() + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'get', 'put', 'patch'], + 'path': '/hosting/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/hosting/', + } + } + + def upload_file(self, path, file): + files_path = self.links.files + data = {'path': path} + connection = self._get_connection() + params = connection.build_params(params={}) + headers = params['headers'] + headers.pop('content-type') + response = requests.post(connection.host + files_path, headers=headers, + data=data, files=[('file', file)]) + if response.status_code != 201: + logger.error(response.text) + return + return response + + def list_files(self): + files_path = self.links.files + connection = self._get_connection() + response = connection.request('GET', files_path) + return [f['path'] for f in response['objects']] diff --git a/syncano/models/instances.py b/syncano/models/instances.py index fbe93e4..f4f128a 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -61,6 +61,7 @@ class Instance(RenameMixin, Model): schedules = fields.RelatedManagerField('Schedule') classes = fields.RelatedManagerField('Class') invitations = fields.RelatedManagerField('InstanceInvitation') + hostings = fields.RelatedManagerField('Hosting') # push notifications fields; gcm_devices = fields.RelatedManagerField('GCMDevice') diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py new file mode 100644 index 0000000..0e169e7 --- /dev/null +++ b/tests/integration_tests_hosting.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +import StringIO + +from tests.integration_test import InstanceMixin, IntegrationTest + + +class HostingIntegrationTests(InstanceMixin, IntegrationTest): + + def setUp(self): + self.hosting = self.instance.hostings.please.create( + label='test12', + description='desc', + domains=['test.test.io'] + ) + + def test_create_file(self): + a_hosting_file = StringIO.StringIO() + a_hosting_file.write('h1 {color: #541231;}') + a_hosting_file.seek(0) + + self.hosting.upload_file(path='styles/main.css', file=a_hosting_file) + + files_list = self.hosting.list_files() + + self.assertIn('styles/mains.css', files_list) From ba5b877608b64ce8f80b03668ddb1528f3eea826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Jul 2016 12:57:14 +0200 Subject: [PATCH 470/558] [LIB-813] add support for hosting management; --- tests/integration_tests_hosting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 0e169e7..8a49ff5 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -7,7 +7,7 @@ class HostingIntegrationTests(InstanceMixin, IntegrationTest): def setUp(self): - self.hosting = self.instance.hostings.please.create( + self.hosting = self.instance.hostings.create( label='test12', description='desc', domains=['test.test.io'] From 274076454c470e152285f21768904d58eb06eaf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Jul 2016 13:00:32 +0200 Subject: [PATCH 471/558] [LIB-813] correct StringIO imports; --- tests/integration_tests_hosting.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 8a49ff5..670eb22 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -1,5 +1,10 @@ # -*- coding: utf-8 -*- -import StringIO +try: + # python2 + from StringIO import StringIO +except ImportError: + # python3 + from io import StringIO from tests.integration_test import InstanceMixin, IntegrationTest @@ -14,7 +19,7 @@ def setUp(self): ) def test_create_file(self): - a_hosting_file = StringIO.StringIO() + a_hosting_file = StringIO() a_hosting_file.write('h1 {color: #541231;}') a_hosting_file.seek(0) From baea8b5050385e7be609ee0e5ad169ab2c27ea9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Jul 2016 13:38:38 +0200 Subject: [PATCH 472/558] [LIB-813] add ListField for handling domains --- syncano/models/fields.py | 10 ++++++++++ syncano/models/hosting.py | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 15126e1..f6b96eb 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -716,6 +716,16 @@ def to_native(self, value): return value +class ListField(WritableField): + + def validate(self, value, model_instance): + if value is None: + return + + if not isinstance(value, list): + raise self.ValidationError('List expected.') + + class GeoPointField(Field): field_lookups = ['near', 'exists'] diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index d849bc4..7f894a2 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -12,8 +12,9 @@ class Hosting(Model): label = fields.StringField(max_length=64, primary_key=True) description = fields.StringField(read_only=False, required=False) - domains = fields.JSONField(default=[]) + domains = fields.ListField(default=[]) + id = fields.IntegerField(read_only=True) links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) From e02cd460df88d990b10778bde75a2346d2ed546c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Jul 2016 13:49:01 +0200 Subject: [PATCH 473/558] [LIB-813] correct tests; --- tests/integration_tests_hosting.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 670eb22..764333f 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- +import uuid + +from tests.integration_test import InstanceMixin, IntegrationTest + try: # python2 from StringIO import StringIO @@ -6,8 +10,6 @@ # python3 from io import StringIO -from tests.integration_test import InstanceMixin, IntegrationTest - class HostingIntegrationTests(InstanceMixin, IntegrationTest): @@ -15,7 +17,7 @@ def setUp(self): self.hosting = self.instance.hostings.create( label='test12', description='desc', - domains=['test.test.io'] + domains=['test.test{}.io'.format(uuid.uuid4().hex[:5])] ) def test_create_file(self): From 9b3d292f1288b10db99da207c11d6e1df6c1a5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Jul 2016 15:10:50 +0200 Subject: [PATCH 474/558] [LIB-813] use session instead of requests; correct test check --- syncano/models/hosting.py | 4 +--- tests/integration_tests_hosting.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index 7f894a2..2c510b8 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -import requests - from . import fields from .base import Instance, Model, logger @@ -39,7 +37,7 @@ def upload_file(self, path, file): params = connection.build_params(params={}) headers = params['headers'] headers.pop('content-type') - response = requests.post(connection.host + files_path, headers=headers, + response = connection.session.post(connection.host + files_path, headers=headers, data=data, files=[('file', file)]) if response.status_code != 201: logger.error(response.text) diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 764333f..cf414f2 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -29,4 +29,4 @@ def test_create_file(self): files_list = self.hosting.list_files() - self.assertIn('styles/mains.css', files_list) + self.assertIn('styles/main.css', files_list) From 6fc4c8fab6b63061f9c739a02ca8aac0adccd350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 21 Jul 2016 15:21:02 +0200 Subject: [PATCH 475/558] [LIB-813] correct flake8 issues; --- syncano/models/hosting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index 2c510b8..eb7b689 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -38,7 +38,7 @@ def upload_file(self, path, file): headers = params['headers'] headers.pop('content-type') response = connection.session.post(connection.host + files_path, headers=headers, - data=data, files=[('file', file)]) + data=data, files=[('file', file)]) if response.status_code != 201: logger.error(response.text) return From 1906e1dfcc077c55d2fc4e20718f89b86885dc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 22 Jul 2016 11:04:33 +0200 Subject: [PATCH 476/558] [LIB-813] add set default for hosting; --- syncano/models/hosting.py | 11 +++++++++++ tests/integration_tests_hosting.py | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index eb7b689..3c5c382 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -49,3 +49,14 @@ def list_files(self): connection = self._get_connection() response = connection.request('GET', files_path) return [f['path'] for f in response['objects']] + + def set_default(self): + default_path = self.links.default + connection = self._get_connection() + + response = connection.make_request('POST', default_path) + + if response.status_code == 200: + return True + + return False diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index cf414f2..1a003b3 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import uuid +from syncano.models import Hosting from tests.integration_test import InstanceMixin, IntegrationTest try: @@ -30,3 +31,9 @@ def test_create_file(self): files_list = self.hosting.list_files() self.assertIn('styles/main.css', files_list) + + def test_set_default(self): + self.hosting.set_default() + + hosting = Hosting.please.get(id=self.hosting.id) + self.assertIn('default', hosting.domains) From ba67381cb9d07ab8d8c75a579f82e3c35d0e4d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 22 Jul 2016 11:33:26 +0200 Subject: [PATCH 477/558] [LIB-816] add set default for hosting; --- syncano/models/hosting.py | 7 ++----- tests/integration_tests_hosting.py | 5 +---- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index 3c5c382..1e291c0 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -55,8 +55,5 @@ def set_default(self): connection = self._get_connection() response = connection.make_request('POST', default_path) - - if response.status_code == 200: - return True - - return False + self.to_python(response) + return self diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 1a003b3..8056ae1 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import uuid -from syncano.models import Hosting from tests.integration_test import InstanceMixin, IntegrationTest try: @@ -33,7 +32,5 @@ def test_create_file(self): self.assertIn('styles/main.css', files_list) def test_set_default(self): - self.hosting.set_default() - - hosting = Hosting.please.get(id=self.hosting.id) + hosting = self.hosting.set_default() self.assertIn('default', hosting.domains) From bf5d8c14638064209927fce317051dd02b67521e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 25 Jul 2016 13:26:03 +0200 Subject: [PATCH 478/558] [LIB-816] correct links name in set_default' --- syncano/models/hosting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index 1e291c0..39c32e7 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -51,7 +51,7 @@ def list_files(self): return [f['path'] for f in response['objects']] def set_default(self): - default_path = self.links.default + default_path = self.links.set_default connection = self._get_connection() response = connection.make_request('POST', default_path) From 9e78abef203189cc40669fc7462a95d3a814d25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 26 Jul 2016 13:19:05 +0200 Subject: [PATCH 479/558] [v5.3.0] bump the version --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 8882af2..93b0df2 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.2.0' +__version__ = '5.3.0' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From 90c4333e8030b20f13030770a184098f86039b04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 9 Aug 2016 15:57:13 +0200 Subject: [PATCH 480/558] [LIB-821] add retry-after handling; --- syncano/connection.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/syncano/connection.py b/syncano/connection.py index 4efff69..b5ad82c 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -3,6 +3,8 @@ import requests import six +import time + import syncano from syncano.exceptions import RevisionMismatchException, SyncanoRequestError, SyncanoValueError @@ -267,6 +269,12 @@ def make_request(self, method_name, path, **kwargs): url = self.build_url(path) response = method(url, **params) + + while response.status_code == 429: # throttling; + retry_after = response.headers.get('retry-after', 1) + time.sleep(float(retry_after)) + response = method(url, **params) + content = self.get_response_content(url, response) if files: From 3801a26fd806bd3e0a2947857da6991ab64b60d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 9 Aug 2016 16:00:02 +0200 Subject: [PATCH 481/558] [LIB-821] correct isort issues; --- syncano/connection.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index b5ad82c..c4c7706 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -1,10 +1,9 @@ import json +import time from copy import deepcopy import requests import six -import time - import syncano from syncano.exceptions import RevisionMismatchException, SyncanoRequestError, SyncanoValueError From 12cda74d7fe6260e84b87270fcccfe32f3d31b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 10 Aug 2016 18:01:15 +0200 Subject: [PATCH 482/558] [LIB-837][WIP] working on custom sockets in LIB --- syncano/models/base.py | 3 +- syncano/models/custom_sockets.py | 220 +++++++++++++++++++++++++++++++ syncano/models/fields.py | 2 +- 3 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 syncano/models/custom_sockets.py diff --git a/syncano/models/base.py b/syncano/models/base.py index 38c24b5..4b73a1d 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -12,4 +12,5 @@ from .geo import * # NOQA from .backups import * # NOQA from .hosting import * # NOQA -from .data_views import DataEndpoint as EndpointData # NOQA \ No newline at end of file +from .data_views import DataEndpoint as EndpointData # NOQA +from .custom_sockets import * # NOQA \ No newline at end of file diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py new file mode 100644 index 0000000..305d716 --- /dev/null +++ b/syncano/models/custom_sockets.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +from .base import Instance, Model +from . import fields + +from syncano.exceptions import SyncanoValueError + + +class CallTypeE(object): + SCRIPT = 'script' + + +class DependencyTypeE(object): + SCRIPT = 'script' + + +class Call(object): + + def __init__(self, name, methods, call_type=None): + call_type = call_type or CallTypeE.SCRIPT + self.type = call_type + self.name = name + self.methods = methods + + def to_dict(self): + return { + 'type': self.type, + 'name': self.name, + 'methods': self.methods + } + + +class Endpoint(object): + + def __init__(self, name): + self.name = name + self.calls = [] + + def add_call(self, call): + self.calls.append(call) + + def to_endpoint_data(self): + return { + self.name: { + 'calls': [call.to_dict() for call in self.calls] + } + } + + +class BaseDependency(object): + + fields = [] + dependency_type = None + field_mapping = {} + + def __init__(self, dependency_object): + self.dependency_object = dependency_object + + def to_dependency_data(self): + if self.dependency_type is None: + raise SyncanoValueError('dependency_type not set.') + dependency_data = {'type': self.dependency_type} + dependency_data.update({field_name: getattr( + self.dependency_object, + self.field_mapping.get(field_name, field_name) + ) for field_name in self.fields}) + return dependency_data + + +class ScriptDependency(BaseDependency): + dependency_type = DependencyTypeE.SCRIPT + fields = [ + 'runtime_name', + 'name', + 'source' + ] + + field_mapping = {'name': 'label'} + + +class EndpointMetadataMixin(object): + + def __init__(self, *args, **kwargs): + self._endpoints = [] + super(EndpointMetadataMixin, self).__init__(*args, **kwargs) + + def add_endpoint(self, endpoint): + self._endpoints.append(endpoint) + + @property + def endpoints_data(self): + endpoints = {} + for endpoint in self._endpoints: + endpoints.update(endpoint.to_endpoint_data()) + return endpoints + + +class DependencyMetadataMixin(object): + + def __init__(self, *args, **kwargs): + self._dependencies = [] + super(DependencyMetadataMixin, self).__init__(*args, **kwargs) + + def add_dependency(self, depedency): + self._dependencies.append(depedency) + + @property + def dependencies_data(self): + return [dependency.to_dependency_data() for dependency in self._dependencies] + + +class CustomSocket(EndpointMetadataMixin, DependencyMetadataMixin, Model): + """ + OO wrapper around instance custom sockets. + + :ivar name: :class:`~syncano.models.fields.StringField` + :ivar endpoints: :class:`~syncano.models.fields.JSONField` + :ivar dependencies: :class:`~syncano.models.fields.JSONField` + :ivar metadata: :class:`~syncano.models.fields.JSONField` + :ivar links: :class:`~syncano.models.fields.LinksField` + """ + + name = fields.StringField(max_length=64) + endpoints = fields.JSONField() + dependencies = fields.JSONField() + metadata = fields.JSONField(read_only=True, required=False) + links = fields.LinksField() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['get', 'put', 'patch', 'delete'], + 'path': '/sockets/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/sockets/', + } + } + + def get_endpoints(self): + endpoints_path = self.links.endpoints + connection = self._get_connection() + response = connection.request('GET', endpoints_path) + endpoints = [] + for endpoint in response['objects']: + endpoints.append(SocketEndpoint(**endpoint)) + return endpoints + + def run(self, method, endpoint_name, data={}): + endpoint = self._find_endpoint(endpoint_name) + return endpoint.run(method, data=data) + + def _find_endpoint(self, endpoint_name): + endpoints = self.get_endpoints() + for endpoint in endpoints: + print(endpoint.name, endpoint_name) + if endpoint_name == endpoint.name: + return endpoint + raise SyncanoValueError('Endpoint {} not found.'.format(endpoint_name)) + + def publish(self): + created_socket = self.__class__.please.create( + name=self.name, + endpoints=self.endpoints_data, + dependencies=self.dependencies_data + ) + raw_data = created_socket._raw_data + raw_data['links'] = raw_data['links'].links_dict + self.to_python(raw_data) + return self + + +class SocketEndpoint(Model): + """ + OO wrapper around endpoints defined in CustomSocket instance. + + :ivar name: :class:`~syncano.models.fields.StringField` + :ivar calls: :class:`~syncano.models.fields.JSONField` + :ivar links: :class:`~syncano.models.fields.LinksField` + """ + name = fields.StringField(max_length=64, primary_key=True) + calls = fields.JSONField() + links = fields.LinksField() + + class Meta: + parent = CustomSocket + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/endpoints/{name}/' + }, + 'list': { + 'methods': ['get'], + 'path': '/endpoints/' + } + } + + def run(self, method='GET', data={}): + endpoint_path = self.links.endpoint + connection = self._get_connection() + if not self._validate_method(method): + raise SyncanoValueError('Method: {} not specified in calls for this custom socket.'.format(method)) + + if method == ['GET', 'DELETE']: + response = connection.request(method, endpoint_path) + elif method in ['POST', 'PUT', 'PATCH']: + response = connection.request(method, endpoint_path, data=data) + else: + raise SyncanoValueError('Method: {} not supported.'.format(method)) + return response + + def _validate_method(self, method): + + methods = [] + for call in self.calls: + methods.extend(call['methods']) + if '*' in methods or method in methods: + return True + return False diff --git a/syncano/models/fields.py b/syncano/models/fields.py index f6b96eb..feb70a8 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -473,7 +473,7 @@ def to_python(self, value): return LinksWrapper(value, self.IGNORED_LINKS) def to_native(self, value): - return value + return value.to_native() class ModelField(Field): From 460006f22610a843b63ffcc982da804ac3c427bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 11 Aug 2016 15:30:30 +0200 Subject: [PATCH 483/558] [LIB-837] Add missing docs about custom_sockets; move utils to another file, add possibility to recheck and update custom socket; add possibility to remove endpoints and dependencies; --- docs/source/custom_sockets.rst | 214 +++++++++++++++++ docs/source/index.rst | 1 + .../refs/syncano.models.custom_sockets.rst | 7 + .../syncano.models.custom_sockets_utils.rst | 7 + docs/source/refs/syncano.models.geo.rst | 7 + docs/source/refs/syncano.models.hosting.rst | 7 + syncano/models/base.py | 3 +- syncano/models/custom_sockets.py | 147 +++--------- syncano/models/custom_sockets_utils.py | 222 ++++++++++++++++++ tests/integration_test_custom_socket.py | 6 + 10 files changed, 509 insertions(+), 112 deletions(-) create mode 100644 docs/source/custom_sockets.rst create mode 100644 docs/source/refs/syncano.models.custom_sockets.rst create mode 100644 docs/source/refs/syncano.models.custom_sockets_utils.rst create mode 100644 docs/source/refs/syncano.models.geo.rst create mode 100644 docs/source/refs/syncano.models.hosting.rst create mode 100644 syncano/models/custom_sockets_utils.py create mode 100644 tests/integration_test_custom_socket.py diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst new file mode 100644 index 0000000..976063e --- /dev/null +++ b/docs/source/custom_sockets.rst @@ -0,0 +1,214 @@ +.. _custom-sockets: + +========================= +Custom Sockets in Syncano +========================= + +``Syncano`` provides possibility of creating the custom sockets. It means that there's a possibility +to define a very specific endpoints in syncano application and use them as normal api calls. +Currently custom sockets allow only one dependency - script. This mean that on the backend side +each time the api is called - the script is executed and result from this script is returned as a result of the +api call. + +Creating a custom socket +------------------------ + +There are two methods of creating the custom socket. First: use the helpers objects defined in Python Libray. +Second: use the raw format - this is described below. + +To create a custom socket follow the steps:: + + import syncano + from syncano.models import CustomSocket, Endpoint, ScriptCall, ScriptDependency, RuntimeChoices + from syncano.connection import Connection + + custom_socket = CustomSocket(name='my_custom_socket') # this will create an object in place (do api call) + + # define endpoints + my_endpoint = Endpoint(name='my_endpoint') # again - no api call here + my_endpoint.add_call(ScriptCall(name='custom_script'), methods=['GET']) + my_endpoint.add_call(ScriptCall(name='another_custom_script'), methods=['POST']) + + # explanation for the above lines: + # The endpoint will be seen under `my_endpoint` name: + # On this syncano api endpoint the above endpoint will be called (after custom socket creation) + # :///instances//endpoints/sockets/my_endpoint/ + # On this syncano api endpoint the details of the defined endpoint will be returned + # :///instances//sockets/my_custom_socket/endpoints/my_endpoint/ + # For the above endpoint - the two calls are defined, one uses GET method - the custom_script will be executed + # there, second uses the POST method and then the another_custom_script will be called; + # Currently only script are available for calls; + + # After the creation of the endpoint, add them to custom_socket: + custom_socket.add_endpoint(my_endpoint) + + # define dependency now; + # using a new script - defining new source code; + custom_socket.add_dependency( + ScriptDependency( + Script( + label='custom_script', + runtime_name=RuntimeChoices.PYTHON_V5_0, + source='print("custom_script")' + ) + ) + ) + # using a existing script: + another_custom_script = Script.please.get(id=2) + custom_socket.add_dependency( + ScriptDependency( + another_custom_script + ) + ) + + # now it is time to publish custom_socket; + custom_socket.publish() # this will do an api call and will create script; + +Some time is needed to setup the environment for this custom socket. +There is possibility to check the custom socket status:: + + print(custom_socket.status) + # and + print(custom_socket.status_info) + + # to reload object (read it again from syncano api) use: + custom_socket.reload() + + + +Updating the custom socket +-------------------------- + +To update custom socket, use:: + + custom_socket = CustomSocket.please.get(name='my_custom_socket') + + custom_socket.remove_endpoint(endpoint_name='my_endpoint') + custom_socket.remove_dependency(dependency_name='custom_script') + + # or add new: + + custom_socket.add_endpoint(new_endpoint) # see above code for endpoint examples; + custom_socket.add_dependency(new_dependency) # see above code for dependency examples; + + custom_socket.update() + + +Running the custom socket +------------------------- + +To run custom socket use:: + + # this will run the my_endpoint - and call the custom_script (method is GET); + result = custom_socket.run(method='GET', endpoint_name='my_endpoint') + + +Read all endpoints +------------------ + +To get the all defined endpoints in custom socket run:: + + endpoints = custom_socket.get_endpoints() + + for endpoint in endpoints: + print(endpoint.name) + print(endpoint.calls) + +To run particular endpoint:: + + endpoint.run(method='GET') + # or: + endpoint.run(method='POST', data={'name': 'test_name'}) + +The data will be passed to the api call in the request body. + +Custom sockets endpoints +------------------------ + +Each custom socket is created from at least one endpoint. The endpoint is characterized by name and +defined calls. Calls is characterized by name and methods. The name is a identification for dependency, eg. +if it's equal to 'my_script' - the Script with label 'my_script' will be used (if exist and the source match), +or new one will be created. +There's a special wildcard method: `methods=['*']` - this mean that any request with +any method will be executed in this endpoint. + +To add endpoint to the custom_socket use:: + + my_endpoint = Endpoint(name='my_endpoint') # again - no api call here + my_endpoint.add_call(ScriptCall(name='custom_script'), methods=['GET']) + my_endpoint.add_call(ScriptCall(name='another_custom_script'), methods=['POST']) + + custom_socket.add_endpoint(my_endpoint) + +Custom socket dependency +------------------------ + +Each custom socket has dependency - this is a meta information for endpoint: which resource +should be used to return the api call results. The dependencies are bind to the endpoints call objects. +Currently supported dependency in only script. + +**Using new script** + +:: + + custom_socket.add_dependency( + ScriptDependency( + Script( + label='custom_script', + runtime_name=RuntimeChoices.PYTHON_V5_0, + source='print("custom_script")' + ) + ) + ) + + +**Using defined script** + +:: + + another_custom_script = Script.please.get(id=2) + custom_socket.add_dependency( + ScriptDependency( + another_custom_script + ) + ) + + +Custom socket recheck +--------------------- + +The creation of the socket can fail - this happen, eg. when endpoint name is already taken by another +custom socket. To check the statuses use:: + + print(custom_socket.status) + print(custom_socket.status_info) + +There is a possibility to re-check socket - this mean that if conditions are met - the socket will be +`created` again and available to use - if not the error will be returned in status field. + +Custom socket - raw format +-------------------------- + +There is a possibility to create a custom socket from the raw JSON format:: + + CustomSocket.please.create( + name='my_custom_socket_3', + endpoints={ + "my_endpoint_3": { + "calls": + [ + {"type": "script", "name": "my_script_3", "methods": ["POST"]} + ] + } + }, + dependencies=[ + { + "type": "script", + "runtime_name": "python_library_v5.0", + "name": "my_script_3", + "source": "print(3)" + } + ] + ) + +The disadvantage of this method is that - the JSON internal structure must be known by developer. diff --git a/docs/source/index.rst b/docs/source/index.rst index e54ea4a..1aaa32c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,6 +19,7 @@ Contents: getting_started interacting + custom_sockets refs/syncano diff --git a/docs/source/refs/syncano.models.custom_sockets.rst b/docs/source/refs/syncano.models.custom_sockets.rst new file mode 100644 index 0000000..3cecb71 --- /dev/null +++ b/docs/source/refs/syncano.models.custom_sockets.rst @@ -0,0 +1,7 @@ +syncano.models.custom_sockets +============================= + +.. automodule:: syncano.models.custom_sockets + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.custom_sockets_utils.rst b/docs/source/refs/syncano.models.custom_sockets_utils.rst new file mode 100644 index 0000000..dec7aba --- /dev/null +++ b/docs/source/refs/syncano.models.custom_sockets_utils.rst @@ -0,0 +1,7 @@ +syncano.models.custom_sockets_utils +=================================== + +.. automodule:: syncano.models.custom_sockets_utils + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.geo.rst b/docs/source/refs/syncano.models.geo.rst new file mode 100644 index 0000000..d9eee0a --- /dev/null +++ b/docs/source/refs/syncano.models.geo.rst @@ -0,0 +1,7 @@ +syncano.models.geo +================== + +.. automodule:: syncano.models.geo + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/refs/syncano.models.hosting.rst b/docs/source/refs/syncano.models.hosting.rst new file mode 100644 index 0000000..48a9639 --- /dev/null +++ b/docs/source/refs/syncano.models.hosting.rst @@ -0,0 +1,7 @@ +syncano.models.hosting +====================== + +.. automodule:: syncano.models.hosting + :members: + :undoc-members: + :show-inheritance: diff --git a/syncano/models/base.py b/syncano/models/base.py index 4b73a1d..d8c4db9 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -13,4 +13,5 @@ from .backups import * # NOQA from .hosting import * # NOQA from .data_views import DataEndpoint as EndpointData # NOQA -from .custom_sockets import * # NOQA \ No newline at end of file +from .custom_sockets import * # NOQA +from .custom_sockets_utils import Endpoint, ScriptCall, ScriptDependency # NOQA diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index 305d716..9c58f52 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -1,116 +1,15 @@ # -*- coding: utf-8 -*- -from .base import Instance, Model -from . import fields - from syncano.exceptions import SyncanoValueError +from syncano.models.custom_sockets_utils import DependencyMetadataMixin, EndpointMetadataMixin - -class CallTypeE(object): - SCRIPT = 'script' - - -class DependencyTypeE(object): - SCRIPT = 'script' - - -class Call(object): - - def __init__(self, name, methods, call_type=None): - call_type = call_type or CallTypeE.SCRIPT - self.type = call_type - self.name = name - self.methods = methods - - def to_dict(self): - return { - 'type': self.type, - 'name': self.name, - 'methods': self.methods - } - - -class Endpoint(object): - - def __init__(self, name): - self.name = name - self.calls = [] - - def add_call(self, call): - self.calls.append(call) - - def to_endpoint_data(self): - return { - self.name: { - 'calls': [call.to_dict() for call in self.calls] - } - } - - -class BaseDependency(object): - - fields = [] - dependency_type = None - field_mapping = {} - - def __init__(self, dependency_object): - self.dependency_object = dependency_object - - def to_dependency_data(self): - if self.dependency_type is None: - raise SyncanoValueError('dependency_type not set.') - dependency_data = {'type': self.dependency_type} - dependency_data.update({field_name: getattr( - self.dependency_object, - self.field_mapping.get(field_name, field_name) - ) for field_name in self.fields}) - return dependency_data - - -class ScriptDependency(BaseDependency): - dependency_type = DependencyTypeE.SCRIPT - fields = [ - 'runtime_name', - 'name', - 'source' - ] - - field_mapping = {'name': 'label'} - - -class EndpointMetadataMixin(object): - - def __init__(self, *args, **kwargs): - self._endpoints = [] - super(EndpointMetadataMixin, self).__init__(*args, **kwargs) - - def add_endpoint(self, endpoint): - self._endpoints.append(endpoint) - - @property - def endpoints_data(self): - endpoints = {} - for endpoint in self._endpoints: - endpoints.update(endpoint.to_endpoint_data()) - return endpoints - - -class DependencyMetadataMixin(object): - - def __init__(self, *args, **kwargs): - self._dependencies = [] - super(DependencyMetadataMixin, self).__init__(*args, **kwargs) - - def add_dependency(self, depedency): - self._dependencies.append(depedency) - - @property - def dependencies_data(self): - return [dependency.to_dependency_data() for dependency in self._dependencies] +from . import fields +from .base import Instance, Model class CustomSocket(EndpointMetadataMixin, DependencyMetadataMixin, Model): """ OO wrapper around instance custom sockets. + Look at the custom socket documentation for more details. :ivar name: :class:`~syncano.models.fields.StringField` :ivar endpoints: :class:`~syncano.models.fields.JSONField` @@ -119,10 +18,12 @@ class CustomSocket(EndpointMetadataMixin, DependencyMetadataMixin, Model): :ivar links: :class:`~syncano.models.fields.LinksField` """ - name = fields.StringField(max_length=64) + name = fields.StringField(max_length=64, primary_key=True) endpoints = fields.JSONField() dependencies = fields.JSONField() metadata = fields.JSONField(read_only=True, required=False) + status = fields.StringField(read_only=True, required=False) + status_info = fields.StringField(read_only=True, required=False) links = fields.LinksField() class Meta: @@ -154,26 +55,50 @@ def run(self, method, endpoint_name, data={}): def _find_endpoint(self, endpoint_name): endpoints = self.get_endpoints() for endpoint in endpoints: - print(endpoint.name, endpoint_name) if endpoint_name == endpoint.name: return endpoint raise SyncanoValueError('Endpoint {} not found.'.format(endpoint_name)) def publish(self): + if not self.is_new(): + raise SyncanoValueError('Can not publish already defined custom socket.') + created_socket = self.__class__.please.create( name=self.name, endpoints=self.endpoints_data, dependencies=self.dependencies_data ) - raw_data = created_socket._raw_data - raw_data['links'] = raw_data['links'].links_dict - self.to_python(raw_data) + + created_socket._raw_data['links'] = created_socket._raw_data['links'].links_dict + self.to_python(created_socket._raw_data) + return self + + def update(self): + if self.is_new(): + raise SyncanoValueError('Publish socket first.') + + update_socket = self.__class__.please.update( + name=self.name, + endpoints=self.endpoints_data, + dependencies=self.dependencies_data + ) + + update_socket._raw_data['links'] = update_socket._raw_data['links'].links_dict + self.to_python(update_socket._raw_data) + return self + + def recheck(self): + recheck_path = self.links.recheck + connection = self._get_connection() + rechecked_socket = connection.request('POST', recheck_path) + self.to_python(rechecked_socket) return self class SocketEndpoint(Model): """ OO wrapper around endpoints defined in CustomSocket instance. + Look at the custom socket documentation for more details. :ivar name: :class:`~syncano.models.fields.StringField` :ivar calls: :class:`~syncano.models.fields.JSONField` @@ -202,7 +127,7 @@ def run(self, method='GET', data={}): if not self._validate_method(method): raise SyncanoValueError('Method: {} not specified in calls for this custom socket.'.format(method)) - if method == ['GET', 'DELETE']: + if method in ['GET', 'DELETE']: response = connection.request(method, endpoint_path) elif method in ['POST', 'PUT', 'PATCH']: response = connection.request(method, endpoint_path, data=data) diff --git a/syncano/models/custom_sockets_utils.py b/syncano/models/custom_sockets_utils.py new file mode 100644 index 0000000..34c3256 --- /dev/null +++ b/syncano/models/custom_sockets_utils.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- +import six +from syncano.exceptions import SyncanoValueError + +from .incentives import Script + + +class CallTypeE(object): + """ + The type of the call object used in the custom socket; + """ + SCRIPT = 'script' + + +class DependencyTypeE(object): + """ + The type of the dependency object used in the custom socket; + """ + SCRIPT = 'script' + + +class BaseCall(object): + """ + Base class for call object. + """ + + call_type = None + + def __init__(self, name, methods): + self.name = name + self.methods = methods + + def to_dict(self): + if self.call_type is None: + raise SyncanoValueError('call_type not set.') + return { + 'type': self.call_type, + 'name': self.name, + 'methods': self.methods + } + + +class ScriptCall(BaseCall): + """ + Script call object. + + The JSON format is as follows (to_dict in the base class):: + + { + 'type': 'script', + 'name': ', + 'methods': [], + } + + methods can be as follows: + * ['GET'] + * ['*'] - which will do a call on every request method; + """ + call_type = CallTypeE.SCRIPT + + +class Endpoint(object): + """ + The object which stores metadata about endpoints in custom socket; + + The JSON format is as follows:: + + { + ': { + 'calls': [ + + ] + } + } + + """ + def __init__(self, name): + self.name = name + self.calls = [] + + def add_call(self, call): + self.calls.append(call) + + def to_endpoint_data(self): + return { + self.name: { + 'calls': [call.to_dict() for call in self.calls] + } + } + + +class BaseDependency(object): + """ + Base dependency object; + + On the base of the fields attribute - the JSON format of the dependency is returned. + The fields are taken from the dependency object - which can be Script (supported now). + """ + + fields = [] + dependency_type = None + field_mapping = {} + + def __init__(self, dependency_object): + self.dependency_object = dependency_object + + def to_dependency_data(self): + if self.dependency_type is None: + raise SyncanoValueError('dependency_type not set.') + dependency_data = {'type': self.dependency_type} + dependency_data.update({field_name: getattr( + self.dependency_object, + self.field_mapping.get(field_name, field_name) + ) for field_name in self.fields}) + return dependency_data + + +class ScriptDependency(BaseDependency): + """ + Script dependency object; + + The JSON format is as follows:: + { + 'type': 'script', + 'runtime_name': '', + } + """ + dependency_type = DependencyTypeE.SCRIPT + fields = [ + 'runtime_name', + 'name', + 'source' + ] + + field_mapping = {'name': 'label'} + id_name = 'label' + + +class EndpointMetadataMixin(object): + """ + A mixin which allows to collect Endpoints objects and transform them to the appropriate JSON format. + """ + + def __init__(self, *args, **kwargs): + self._endpoints = [] + super(EndpointMetadataMixin, self).__init__(*args, **kwargs) + if self.endpoints: + self.update_endpoints() + + def update_endpoints(self): + for raw_endpoint_name, raw_endpoint in six.iteritems(self.endpoints): + endpoint = Endpoint( + name=raw_endpoint_name, + ) + for call in raw_endpoint['calls']: + call_class = self._get_call_class(call['type']) + call_instance = call_class(name=call['name'], methods=call['methods']) + endpoint.add_call(call_instance) + + self.add_endpoint(endpoint) + + @classmethod + def _get_call_class(cls, call_type): + if call_type == CallTypeE.SCRIPT: + return ScriptCall + + def add_endpoint(self, endpoint): + self._endpoints.append(endpoint) + + def remove_endpoint(self, endpoint_name): + for index, endpoint in enumerate(self._endpoints): + if endpoint.name == endpoint_name: + self._endpoints.pop(index) + break + + @property + def endpoints_data(self): + endpoints = {} + for endpoint in self._endpoints: + endpoints.update(endpoint.to_endpoint_data()) + return endpoints + + +class DependencyMetadataMixin(object): + """ + A mixin which allows to collect Dependencies objects and transform them to the appropriate JSON format. + """ + + def __init__(self, *args, **kwargs): + self._dependencies = [] + super(DependencyMetadataMixin, self).__init__(*args, **kwargs) + if self.dependencies: + self.update_dependencies() + + def update_dependencies(self): + for raw_depedency in self.dependencies: + depedency_class, object_class = self._get_depedency_klass(raw_depedency['type']) + + self.add_dependency(depedency_class( + object_class(**{ + depedency_class.field_mapping.get(field_name, field_name): raw_depedency.get(field_name) + for field_name in depedency_class.fields + }) + )) + + @classmethod + def _get_depedency_klass(cls, depedency_type): + if depedency_type == DependencyTypeE.SCRIPT: + return ScriptDependency, Script + + def add_dependency(self, depedency): + self._dependencies.append(depedency) + + def remove_dependency(self, dependency_name): + for index, dependency in enumerate(self._dependencies): + if dependency_name == getattr(dependency.dependency_object, dependency.id_name, None): + self._dependencies.pop(index) + break + + @property + def dependencies_data(self): + return [dependency.to_dependency_data() for dependency in self._dependencies] diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py new file mode 100644 index 0000000..5218d9d --- /dev/null +++ b/tests/integration_test_custom_socket.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from tests.integration_test import InstanceMixin, IntegrationTest + + +class CustomSocketTest(InstanceMixin, IntegrationTest): + pass From 5eca90660f6f169bfd811fc5ca8e976d143b316a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 11 Aug 2016 15:34:11 +0200 Subject: [PATCH 484/558] [LIB-837] add missing fields in doc string; --- syncano/models/custom_sockets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index 9c58f52..59a93b5 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -15,6 +15,8 @@ class CustomSocket(EndpointMetadataMixin, DependencyMetadataMixin, Model): :ivar endpoints: :class:`~syncano.models.fields.JSONField` :ivar dependencies: :class:`~syncano.models.fields.JSONField` :ivar metadata: :class:`~syncano.models.fields.JSONField` + :ivar status: :class:`~syncano.models.fields.StringField` + :ivar status_info: :class:`~syncano.models.fields.StringField` :ivar links: :class:`~syncano.models.fields.LinksField` """ From cb5f0ab06204c77aa7bc6021abb6bf6a4221f12e Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Fri, 12 Aug 2016 13:44:03 +0200 Subject: [PATCH 485/558] [readme_changes] Some readme changes Removed info about 0.6.x version as it may be confusing for newer users. Fixed urls to work for rst --- README.rst | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 2744d96..396f7df 100644 --- a/README.rst +++ b/README.rst @@ -4,21 +4,10 @@ Syncano Python QuickStart Guide ----------------------- -You can find quick start on installing and using Syncano's Python library in our [documentation](http://docs.syncano.com/docs/python). +You can find quick start on installing and using Syncano's Python library in our `documentation `_. -For more detailed information on how to use Syncano and its features - our [Developer Manual](http://docs.syncano.com/docs/getting-started-with-syncano) should be very helpful. +For more detailed information on how to use Syncano and its features - our `Developer Manual `_ should be very helpful. In case you need help working with the library - email us at libraries@syncano.com - we will be happy to help! -You can also find library reference hosted on GitHub pages [here](http://syncano.github.io/syncano-python/). - -Backwards incompatible changes ------------------------------- - -Version 4.x and 5.x is designed for new release of Syncano platform and -is **not compatible** with any previous releases. - -Code from `0.6.x` release is avalable on [stable/0.6.x](https://github.com/Syncano/syncano-python/tree/stable/0.6.x) branch -and it can be installed directly from pip via: - -``pip install syncano==0.6.2 --pre`` +You can also find library reference hosted on GitHub pages `here `_. From c0f75cbe59acc493a796e685bb678fbd3f846f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 12 Aug 2016 14:58:08 +0200 Subject: [PATCH 486/558] [LIB-837] correct documentation; add possibility to read all endpoints; correct ScriptDependency to handle ScriptEndpoint; make Script a model field in ScriptEndpoint; overall fixes; --- docs/source/custom_sockets.rst | 115 ++++++++++------- setup.py | 4 +- syncano/models/custom_sockets.py | 14 +- syncano/models/custom_sockets_utils.py | 82 ++++++++---- syncano/models/fields.py | 4 + syncano/models/incentives.py | 6 +- syncano/models/instances.py | 4 + tests/integration_test_custom_socket.py | 162 +++++++++++++++++++++++- 8 files changed, 310 insertions(+), 81 deletions(-) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index 976063e..41efde8 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -4,78 +4,80 @@ Custom Sockets in Syncano ========================= -``Syncano`` provides possibility of creating the custom sockets. It means that there's a possibility -to define a very specific endpoints in syncano application and use them as normal api calls. +``Syncano`` provides possibility of creating custom sockets. It means that there's a possibility +to define a very specific endpoints in syncano application and use them as normal API calls. Currently custom sockets allow only one dependency - script. This mean that on the backend side -each time the api is called - the script is executed and result from this script is returned as a result of the -api call. +each time the API is called - the script is executed and result from this script is returned as a result of the +API call. Creating a custom socket ------------------------ -There are two methods of creating the custom socket. First: use the helpers objects defined in Python Libray. -Second: use the raw format - this is described below. - -To create a custom socket follow the steps:: +To create a custom socket follow these steps:: import syncano from syncano.models import CustomSocket, Endpoint, ScriptCall, ScriptDependency, RuntimeChoices from syncano.connection import Connection - custom_socket = CustomSocket(name='my_custom_socket') # this will create an object in place (do api call) + # 1. Initialize the custom socket. + custom_socket = CustomSocket(name='my_custom_socket') # this will create an object in place (do API call) - # define endpoints - my_endpoint = Endpoint(name='my_endpoint') # again - no api call here + # 2. Define endpoints. + my_endpoint = Endpoint(name='my_endpoint') # again - no API call here my_endpoint.add_call(ScriptCall(name='custom_script'), methods=['GET']) my_endpoint.add_call(ScriptCall(name='another_custom_script'), methods=['POST']) # explanation for the above lines: # The endpoint will be seen under `my_endpoint` name: - # On this syncano api endpoint the above endpoint will be called (after custom socket creation) + # On this syncano API endpoint the above endpoint will be called (after custom socket creation) # :///instances//endpoints/sockets/my_endpoint/ - # On this syncano api endpoint the details of the defined endpoint will be returned + # On this syncano API endpoint the details of the defined endpoint will be returned # :///instances//sockets/my_custom_socket/endpoints/my_endpoint/ # For the above endpoint - the two calls are defined, one uses GET method - the custom_script will be executed # there, second uses the POST method and then the another_custom_script will be called; # Currently only script are available for calls; - # After the creation of the endpoint, add them to custom_socket: + # 3. After the creation of the endpoint, add them to custom_socket. custom_socket.add_endpoint(my_endpoint) - # define dependency now; - # using a new script - defining new source code; + # 4. Define dependency now. + # 4.1 using a new script - defining new source code. custom_socket.add_dependency( ScriptDependency( - Script( - label='custom_script', + name='custom_script' + script=Script( runtime_name=RuntimeChoices.PYTHON_V5_0, source='print("custom_script")' ) ) ) - # using a existing script: + # 4.2 using an existing script. another_custom_script = Script.please.get(id=2) custom_socket.add_dependency( ScriptDependency( - another_custom_script + name='another_custom_script', + script=another_custom_script ) ) - # now it is time to publish custom_socket; - custom_socket.publish() # this will do an api call and will create script; + # 4.3 using an existing ScriptEndpoint. + script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name') + custom_socket.add_dependency( + script_endpoint=script_endpoint + ) + + # 5. Publish custom_socket. + custom_socket.publish() # this will do an API call and will create script; Some time is needed to setup the environment for this custom socket. There is possibility to check the custom socket status:: + # Reload will refresh object using syncano API. + custom_socket.reload() print(custom_socket.status) # and print(custom_socket.status_info) - # to reload object (read it again from syncano api) use: - custom_socket.reload() - - - Updating the custom socket -------------------------- @@ -103,8 +105,8 @@ To run custom socket use:: result = custom_socket.run(method='GET', endpoint_name='my_endpoint') -Read all endpoints ------------------- +Read all endpoints in custom socket +----------------------------------- To get the all defined endpoints in custom socket run:: @@ -114,27 +116,40 @@ To get the all defined endpoints in custom socket run:: print(endpoint.name) print(endpoint.calls) -To run particular endpoint:: +To run a particular endpoint:: endpoint.run(method='GET') # or: endpoint.run(method='POST', data={'name': 'test_name'}) -The data will be passed to the api call in the request body. +The data will be passed to the API call in the request body. + +Read all endpoints +------------------ + +To get all endpoints that are defined in all custom sockets:: + + socket_endpoint_list = SocketEndpoint.get_all_endpoints() + +Above code will return a list with SocketEndpoint objects. To run such endpoint, use:: + + socket_endpoint_list.run(method='GET') + # or: + socket_endpoint_list.run(method='POST', data={'custom_data': 1}) Custom sockets endpoints ------------------------ -Each custom socket is created from at least one endpoint. The endpoint is characterized by name and -defined calls. Calls is characterized by name and methods. The name is a identification for dependency, eg. -if it's equal to 'my_script' - the Script with label 'my_script' will be used (if exist and the source match), -or new one will be created. +Each custom socket requires to define at least one endpoint. The endpoint is defined by name and +a list of calls. Each call is defined by a name and a list of methods. The name is a identification for dependency, eg. +if it's equal to 'my_script' - the ScriptEndpoint with name 'my_script' will be used +(if it exists and Script source and runtime matches) or a new one will be created. There's a special wildcard method: `methods=['*']` - this mean that any request with any method will be executed in this endpoint. -To add endpoint to the custom_socket use:: +To add an endpoint to the custom_socket use:: - my_endpoint = Endpoint(name='my_endpoint') # again - no api call here + my_endpoint = Endpoint(name='my_endpoint') # again - no API call here my_endpoint.add_call(ScriptCall(name='custom_script'), methods=['GET']) my_endpoint.add_call(ScriptCall(name='another_custom_script'), methods=['POST']) @@ -144,8 +159,8 @@ Custom socket dependency ------------------------ Each custom socket has dependency - this is a meta information for endpoint: which resource -should be used to return the api call results. The dependencies are bind to the endpoints call objects. -Currently supported dependency in only script. +should be used to return the API call results. The dependencies are bind to the endpoints call objects. +Currently the only supported dependency is script. **Using new script** @@ -153,8 +168,8 @@ Currently supported dependency in only script. custom_socket.add_dependency( ScriptDependency( - Script( - label='custom_script', + name='custom_script' + script=Script( runtime_name=RuntimeChoices.PYTHON_V5_0, source='print("custom_script")' ) @@ -169,10 +184,19 @@ Currently supported dependency in only script. another_custom_script = Script.please.get(id=2) custom_socket.add_dependency( ScriptDependency( - another_custom_script + name='another_custom_script', + script=another_custom_script ) ) +**Using defined script endpoint** + +:: + + script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name') + custom_socket.add_dependency( + script_endpoint=script_endpoint + ) Custom socket recheck --------------------- @@ -183,13 +207,14 @@ custom socket. To check the statuses use:: print(custom_socket.status) print(custom_socket.status_info) -There is a possibility to re-check socket - this mean that if conditions are met - the socket will be -`created` again and available to use - if not the error will be returned in status field. +There is a possibility to re-check socket - this mean that if conditions are met - the socket endpoints and dependencies +will be checked - and if some of them are missing (eg. mistake deletion), they will be created again. +If the endpoints and dependencies do not met the criteria - the error will be returned in the status field. Custom socket - raw format -------------------------- -There is a possibility to create a custom socket from the raw JSON format:: +If you prefer raw JSON format for creating sockets, you can resort to use it in python library as well:::: CustomSocket.please.create( name='my_custom_socket_3', diff --git a/setup.py b/setup.py index 2551e61..66d524b 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,8 @@ def readme(): version=__version__, description='Python Library for syncano.com api', long_description=readme(), - author='Daniel Kopka', - author_email='daniel.kopka@syncano.com', + author='Syncano', + author_email='support@syncano.io', url='http://syncano.com', packages=find_packages(exclude=['tests']), zip_safe=False, diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index 59a93b5..f7a4fda 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -128,15 +128,23 @@ def run(self, method='GET', data={}): connection = self._get_connection() if not self._validate_method(method): raise SyncanoValueError('Method: {} not specified in calls for this custom socket.'.format(method)) - - if method in ['GET', 'DELETE']: + method = method.lower() + if method in ['get', 'delete']: response = connection.request(method, endpoint_path) - elif method in ['POST', 'PUT', 'PATCH']: + elif method in ['post', 'put', 'patch']: response = connection.request(method, endpoint_path, data=data) else: raise SyncanoValueError('Method: {} not supported.'.format(method)) return response + @classmethod + def get_all_endpoints(cls): + connection = cls._meta.connection + all_endpoints_path = Instance._meta.resolve_endpoint('endpoints', + {'name': cls.please.properties.get('instance_name')}) + response = connection.request('GET', all_endpoints_path) + return [cls(**endpoint) for endpoint in response['objects']] + def _validate_method(self, method): methods = [] diff --git a/syncano/models/custom_sockets_utils.py b/syncano/models/custom_sockets_utils.py index 34c3256..450a40e 100644 --- a/syncano/models/custom_sockets_utils.py +++ b/syncano/models/custom_sockets_utils.py @@ -2,17 +2,17 @@ import six from syncano.exceptions import SyncanoValueError -from .incentives import Script +from .incentives import Script, ScriptEndpoint -class CallTypeE(object): +class CallType(object): """ The type of the call object used in the custom socket; """ SCRIPT = 'script' -class DependencyTypeE(object): +class DependencyType(object): """ The type of the dependency object used in the custom socket; """ @@ -56,7 +56,7 @@ class ScriptCall(BaseCall): * ['GET'] * ['*'] - which will do a call on every request method; """ - call_type = CallTypeE.SCRIPT + call_type = CallType.SCRIPT class Endpoint(object): @@ -99,21 +99,23 @@ class BaseDependency(object): fields = [] dependency_type = None - field_mapping = {} - - def __init__(self, dependency_object): - self.dependency_object = dependency_object def to_dependency_data(self): if self.dependency_type is None: raise SyncanoValueError('dependency_type not set.') dependency_data = {'type': self.dependency_type} - dependency_data.update({field_name: getattr( - self.dependency_object, - self.field_mapping.get(field_name, field_name) - ) for field_name in self.fields}) + dependency_data.update(self.get_dependency_data()) return dependency_data + def get_name(self): + raise NotImplementedError() + + def get_dependency_data(self): + raise NotImplementedError() + + def create_from_raw_data(self, raw_data): + raise NotImplementedError() + class ScriptDependency(BaseDependency): """ @@ -123,17 +125,49 @@ class ScriptDependency(BaseDependency): { 'type': 'script', 'runtime_name': '', + 'source': '', + 'name': '' } """ - dependency_type = DependencyTypeE.SCRIPT + + dependency_type = DependencyType.SCRIPT fields = [ 'runtime_name', - 'name', 'source' ] - field_mapping = {'name': 'label'} - id_name = 'label' + def __init__(self, name=None, script=None, script_endpoint=None): + if name and script and script_endpoint: + raise SyncanoValueError("Usage: ScriptDependency(name='', script=Script(...)) or " + "ScriptDependency(ScriptEndpoint(...))") + if (name and not script) or (not name and script): + raise SyncanoValueError("Usage: ScriptDependency(name='', script=Script(...))") + + if script and not isinstance(script, Script): + raise SyncanoValueError("Expected Script type object.") + + if script_endpoint and not isinstance(script_endpoint, ScriptEndpoint): + raise SyncanoValueError("Expected ScriptEndpoint type object.") + + if not script_endpoint: + self.dependency_object = ScriptEndpoint(name=name, script=script) + else: + self.dependency_object = script_endpoint + + def get_name(self): + return {'name': self.dependency_object.name} + + def get_dependency_data(self): + dependency_data = self.get_name() + dependency_data.update({ + field_name: getattr(self.dependency_object.script, field_name) for field_name in self.fields + }) + return dependency_data + + @classmethod + def create_from_raw_data(cls, raw_data): + return cls(**{'script_endpoint': ScriptEndpoint(name=raw_data['name'], script=Script( + source=raw_data['source'], runtime_name=raw_data['runtime_name']))}) class EndpointMetadataMixin(object): @@ -161,7 +195,7 @@ def update_endpoints(self): @classmethod def _get_call_class(cls, call_type): - if call_type == CallTypeE.SCRIPT: + if call_type == CallType.SCRIPT: return ScriptCall def add_endpoint(self, endpoint): @@ -194,19 +228,13 @@ def __init__(self, *args, **kwargs): def update_dependencies(self): for raw_depedency in self.dependencies: - depedency_class, object_class = self._get_depedency_klass(raw_depedency['type']) - - self.add_dependency(depedency_class( - object_class(**{ - depedency_class.field_mapping.get(field_name, field_name): raw_depedency.get(field_name) - for field_name in depedency_class.fields - }) - )) + depedency_class = self._get_depedency_klass(raw_depedency['type']) + self.add_dependency(depedency_class.create_from_raw_data(raw_depedency)) @classmethod def _get_depedency_klass(cls, depedency_type): - if depedency_type == DependencyTypeE.SCRIPT: - return ScriptDependency, Script + if depedency_type == DependencyType.SCRIPT: + return ScriptDependency def add_dependency(self, depedency): self._dependencies.append(depedency) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index feb70a8..cf74253 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -523,6 +523,10 @@ def to_python(self, value): if isinstance(value, dict): return self.rel(**value) + # try to fetch object; + if isinstance(value, int): + return self.rel.please.get(id=value) + raise self.ValidationError("'{0}' has unsupported format.".format(value)) def to_native(self, value): diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 12a0d4d..435433a 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -45,7 +45,7 @@ class Script(Model): >>> Script.please.run('instance-name', 1234) >>> Script.please.run('instance-name', 1234, payload={'variable_one': 1, 'variable_two': 2}) - >>> Script.please.run('instance-name', 1234, payload="{\"variable_one\": 1, \"variable_two\": 2}") + >>> Script.please.run('instance-name', 1234, payload='{"variable_one": 1, "variable_two": 2}') or via instance:: @@ -54,7 +54,7 @@ class Script(Model): >>> s.run(variable_one=1, variable_two=2) """ - label = fields.StringField(max_length=80) + label = fields.StringField(max_length=80, required=False) description = fields.StringField(required=False) source = fields.StringField() runtime_name = fields.StringField() @@ -222,7 +222,7 @@ class ScriptEndpoint(Model): """ name = fields.SlugField(max_length=50, primary_key=True) - script = fields.IntegerField(label='script id') + script = fields.ModelField('Script', label='script id') public = fields.BooleanField(required=False, default=False) public_link = fields.ChoiceField(required=False, read_only=True) links = fields.LinksField() diff --git a/syncano/models/instances.py b/syncano/models/instances.py index f4f128a..788ba05 100644 --- a/syncano/models/instances.py +++ b/syncano/models/instances.py @@ -82,6 +82,10 @@ class Meta: 'config': { 'methods': ['put', 'get'], 'path': '/v1.1/instances/{name}/snippets/config/', + }, + 'endpoints': { + 'methods': ['get'], + 'path': '/v1.1/instances/{name}/endpoints/sockets/' } } diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 5218d9d..7dc3ecc 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -1,6 +1,166 @@ # -*- coding: utf-8 -*- +import time + +from syncano.models import CustomSocket, Endpoint, RuntimeChoices, Script, ScriptCall, ScriptDependency, ScriptEndpoint from tests.integration_test import InstanceMixin, IntegrationTest class CustomSocketTest(InstanceMixin, IntegrationTest): - pass + + def setUp(self): + self.custom_socket = self._create_custom_socket('default', self._define_dependencies_new_script_endpoint) + + def test_publish_custom_socket(self): + # this test new ScriptEndpoint dependency create; + self.assert_custom_socket('publishing', self._define_dependencies_new_script_endpoint) + + def test_dependencies_new_script(self): + self.assert_custom_socket('new_script_publishing', self._define_dependencies_new_script) + + def test_dependencies_existing_script(self): + self.assert_custom_socket('existing_script_publishing', self._define_dependencies_existing_script) + + def test_dependencies_existing_script_endpoint(self): + self.assert_custom_socket('existing_script_endpoint_publishing', + self._define_dependencies_existing_script_endpoint) + + def test_creating_raw_data(self): + custom_socket = CustomSocket.please.create( + name='my_custom_socket_123', + endpoints={ + "my_custom_endpoint_123": { + "calls": [{"type": "script", "name": "script_123", "methods": ["POST"]}] + } + }, + dependencies=[ + { + "type": "script", + "runtime_name": "python_library_v5.0", + "name": "script_123", + "source": "print(123)" + } + ] + ) + + self.assertTrue(custom_socket.id) + + def test_custom_socket_run(self): + results = self.custom_socket.run('GET', 'my_endpoint_default') + self.assertEqual(results['stdout'], 'script_default') + + def test_custom_socket_recheck(self): + custom_socket = self.custom_socket.recheck() + self.assertTrue(custom_socket.id) + + def test_fetching_all_endpoints(self): + all_endpoints = ScriptEndpoint.get_all_endpoints() + self.assertTrue(isinstance(all_endpoints, list)) + self.assertTrue(len(all_endpoints) >= 1) + self.assertTrue(all_endpoints[0].name) + + def test_endpoint_run(self): + script_endpoint = ScriptEndpoint.please.first() + result = script_endpoint.run('GET') + suffix = script_endpoint.name.split('_')[-1] + self.assertTrue(result['stdout'].endswith(suffix)) + + def test_custom_socket_update(self): + socket_to_update = self._create_custom_socket('to_update', self._define_dependencies_new_script_endpoint) + socket_to_update.remove_endpoint(endpoint_name='my_endpoint_to_update') + + new_endpoint = Endpoint(name='my_endpoint_new_to_update') + new_endpoint.add_call( + ScriptCall(name='script_default', methods=['GET']) + ) + + self.custom_socket.update() + time.sleep(2) # wait for custom socket setup; + self.custom_socket.reload() + self.assertIn('my_endpoint_new_default', self.custom_socket.endpoints) + + def assert_custom_socket(self, suffix, dependency_method): + custom_socket = self._create_custom_socket(suffix, dependency_method=dependency_method) + self.assertTrue(custom_socket.id) + + @classmethod + def _create_custom_socket(cls, suffix, dependency_method): + custom_socket = CustomSocket(name='my_custom_socket_{}'.format(suffix)) + + cls._define_endpoints(suffix, custom_socket) + dependency_method(suffix, custom_socket) + + custom_socket.publish() + return custom_socket + + @classmethod + def _initialize_socket(cls, suffix): + return CustomSocket(name='my_custom_socket_{}'.format(suffix)) + + @classmethod + def _define_endpoints(cls, suffix, custom_socket): + endpoint = Endpoint(name='my_endpoint_{}'.format(suffix)) + endpoint.add_call( + ScriptCall( + name='script_{}'.format(suffix), + methods=['GET', 'POST'] + ) + ) + custom_socket.add_endpoint(endpoint) + + @classmethod + def _define_dependencies_new_script_endpoint(cls, suffix, custom_socket): + custom_socket.add_dependency( + ScriptDependency( + script_endpoint=ScriptEndpoint( + name='script_endpoint_{}'.format(suffix), + script=Script( + source='print({})'.format(suffix), + runtime_name=RuntimeChoices.PYTHON_V5_0 + ) + ) + ) + ) + + @classmethod + def _define_dependencies_new_script(cls, suffix, custom_socket): + custom_socket.add_dependency( + ScriptDependency( + name='script_endpoint_{}'.format(suffix), + script=Script( + source='print({})'.format(suffix), + runtime_name=RuntimeChoices.PYTHON_V5_0 + ) + ) + ) + + @classmethod + def _define_dependencies_existing_script(cls, suffix, custom_socket): + # create Script first: + cls._create_script(suffix) + custom_socket.add_dependency( + ScriptDependency( + name='script_endpoint_{}'.format(suffix), + script=Script.please.first() + ) + ) + + @classmethod + def _define_dependencies_existing_script_endpoint(cls, suffix, custom_socket): + script = cls._create_script(suffix) + ScriptEndpoint.please.create( + name='script_endpoint_{}'.format(suffix), + script=script + ) + custom_socket.add_dependency( + ScriptDependency( + script_endpoint=ScriptEndpoint.please.first() + ) + ) + + @classmethod + def _create_script(cls, suffix): + return Script.please.create( + label='script_{}'.format(suffix), + runtime_name=RuntimeChoices.PYTHON_V5_0, + source='print({})'.format(suffix) + ) From e108b5a7806f55cbbfbe00a06abd4aedab8e8c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 12 Aug 2016 15:24:42 +0200 Subject: [PATCH 487/558] [LIB-837] Correct after tests; --- syncano/models/incentives.py | 2 +- tests/integration_test_custom_socket.py | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 435433a..585a03a 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -222,7 +222,7 @@ class ScriptEndpoint(Model): """ name = fields.SlugField(max_length=50, primary_key=True) - script = fields.ModelField('Script', label='script id') + script = fields.ModelField('Script', just_pk=True) public = fields.BooleanField(required=False, default=False) public_link = fields.ChoiceField(required=False, read_only=True) links = fields.LinksField() diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 7dc3ecc..c7884a8 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -80,7 +80,7 @@ def test_custom_socket_update(self): def assert_custom_socket(self, suffix, dependency_method): custom_socket = self._create_custom_socket(suffix, dependency_method=dependency_method) - self.assertTrue(custom_socket.id) + self.assertTrue(custom_socket.name) @classmethod def _create_custom_socket(cls, suffix, dependency_method): @@ -92,10 +92,6 @@ def _create_custom_socket(cls, suffix, dependency_method): custom_socket.publish() return custom_socket - @classmethod - def _initialize_socket(cls, suffix): - return CustomSocket(name='my_custom_socket_{}'.format(suffix)) - @classmethod def _define_endpoints(cls, suffix, custom_socket): endpoint = Endpoint(name='my_endpoint_{}'.format(suffix)) From 5eab4640a047792885b26cd8223fe975f464f290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 12 Aug 2016 15:54:40 +0200 Subject: [PATCH 488/558] [LIB-837] another portion of corrects; --- syncano/models/fields.py | 4 +++- tests/integration_test_custom_socket.py | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index cf74253..ead0dea 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -478,6 +478,8 @@ def to_native(self, value): class ModelField(Field): + read_only = False + def __init__(self, rel, *args, **kwargs): self.rel = rel self.just_pk = kwargs.pop('just_pk', True) @@ -508,7 +510,7 @@ def validate(self, value, model_instance): if not isinstance(value, (self.rel, dict)) and not self.is_data_object_mixin: raise self.ValidationError('Value needs to be a {0} instance.'.format(self.rel.__name__)) - if (self.required and isinstance(value, self.rel))or \ + if (self.required and isinstance(value, self.rel)) or \ (self.is_data_object_mixin and hasattr(value, 'validate')): value.validate() diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index c7884a8..146dce3 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -7,8 +7,10 @@ class CustomSocketTest(InstanceMixin, IntegrationTest): - def setUp(self): - self.custom_socket = self._create_custom_socket('default', self._define_dependencies_new_script_endpoint) + @classmethod + def setUpClass(cls): + super(CustomSocketTest, cls).setUpClass() + cls.custom_socket = cls._create_custom_socket('default', cls._define_dependencies_new_script_endpoint) def test_publish_custom_socket(self): # this test new ScriptEndpoint dependency create; @@ -42,7 +44,7 @@ def test_creating_raw_data(self): ] ) - self.assertTrue(custom_socket.id) + self.assertTrue(custom_socket.name) def test_custom_socket_run(self): results = self.custom_socket.run('GET', 'my_endpoint_default') From 1afcd9f983907ed6c512cd2baef71d58cd4a3010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 12 Aug 2016 16:09:47 +0200 Subject: [PATCH 489/558] [LIB-837] correct tests again; --- tests/integration_test_custom_socket.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 146dce3..b69d955 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -1,7 +1,16 @@ # -*- coding: utf-8 -*- import time -from syncano.models import CustomSocket, Endpoint, RuntimeChoices, Script, ScriptCall, ScriptDependency, ScriptEndpoint +from syncano.models import ( + CustomSocket, + Endpoint, + RuntimeChoices, + Script, + ScriptCall, + ScriptDependency, + ScriptEndpoint, + SocketEndpoint +) from tests.integration_test import InstanceMixin, IntegrationTest @@ -23,7 +32,7 @@ def test_dependencies_existing_script(self): self.assert_custom_socket('existing_script_publishing', self._define_dependencies_existing_script) def test_dependencies_existing_script_endpoint(self): - self.assert_custom_socket('existing_script_endpoint_publishing', + self.assert_custom_socket('existing_script_e_publishing', self._define_dependencies_existing_script_endpoint) def test_creating_raw_data(self): @@ -48,14 +57,14 @@ def test_creating_raw_data(self): def test_custom_socket_run(self): results = self.custom_socket.run('GET', 'my_endpoint_default') - self.assertEqual(results['stdout'], 'script_default') + self.assertEqual(results.result['stdout'], 'script_default') def test_custom_socket_recheck(self): custom_socket = self.custom_socket.recheck() - self.assertTrue(custom_socket.id) + self.assertTrue(custom_socket.name) def test_fetching_all_endpoints(self): - all_endpoints = ScriptEndpoint.get_all_endpoints() + all_endpoints = SocketEndpoint.get_all_endpoints() self.assertTrue(isinstance(all_endpoints, list)) self.assertTrue(len(all_endpoints) >= 1) self.assertTrue(all_endpoints[0].name) @@ -64,17 +73,18 @@ def test_endpoint_run(self): script_endpoint = ScriptEndpoint.please.first() result = script_endpoint.run('GET') suffix = script_endpoint.name.split('_')[-1] - self.assertTrue(result['stdout'].endswith(suffix)) + self.assertTrue(result.result['stdout'].endswith(suffix)) def test_custom_socket_update(self): socket_to_update = self._create_custom_socket('to_update', self._define_dependencies_new_script_endpoint) - socket_to_update.remove_endpoint(endpoint_name='my_endpoint_to_update') + socket_to_update.remove_endpoint(endpoint_name='my_endpoint_default') new_endpoint = Endpoint(name='my_endpoint_new_to_update') new_endpoint.add_call( ScriptCall(name='script_default', methods=['GET']) ) + self.custom_socket.add_endpoint(Endpoint) self.custom_socket.update() time.sleep(2) # wait for custom socket setup; self.custom_socket.reload() From 48eb08eb673d140b01cb14d680a7f2e612da7caa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 12 Aug 2016 16:53:39 +0200 Subject: [PATCH 490/558] [LIB-837] change tests instead of doing a magic in ModelField; --- syncano/models/backups.py | 2 +- syncano/models/fields.py | 4 ---- tests/integration_test.py | 6 +++--- tests/integration_test_cache.py | 2 +- tests/integration_test_custom_socket.py | 8 ++++---- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/syncano/models/backups.py b/syncano/models/backups.py index bf77353..3e92342 100644 --- a/syncano/models/backups.py +++ b/syncano/models/backups.py @@ -28,7 +28,7 @@ class Backup(Model): size = fields.IntegerField(read_only=True) status = fields.StringField(read_only=True) status_info = fields.StringField(read_only=True) - author = fields.ModelField('Admin') + author = fields.ModelField('Admin', read_only=True) details = fields.JSONField(read_only=True) updated_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index ead0dea..c8ba8a1 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -525,10 +525,6 @@ def to_python(self, value): if isinstance(value, dict): return self.rel(**value) - # try to fetch object; - if isinstance(value, int): - return self.rel.please.get(id=value) - raise self.ValidationError("'{0}' has unsupported format.".format(value)) def to_native(self, value): diff --git a/tests/integration_test.py b/tests/integration_test.py index 622722c..816ea7a 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -548,7 +548,7 @@ def test_list(self): def test_create(self): script_endpoint = self.model.please.create( instance_name=self.instance.name, - script=self.script.id, + script=self.script, name='wh%s' % self.generate_hash()[:10], ) @@ -557,7 +557,7 @@ def test_create(self): def test_script_run(self): script_endpoint = self.model.please.create( instance_name=self.instance.name, - script=self.script.id, + script=self.script, name='wh%s' % self.generate_hash()[:10], ) @@ -569,7 +569,7 @@ def test_script_run(self): def test_custom_script_run(self): script_endpoint = self.model.please.create( instance_name=self.instance.name, - script=self.custom_script.id, + script=self.custom_script, name='wh%s' % self.generate_hash()[:10], ) diff --git a/tests/integration_test_cache.py b/tests/integration_test_cache.py index 3aff7ad..ba45bc8 100644 --- a/tests/integration_test_cache.py +++ b/tests/integration_test_cache.py @@ -51,7 +51,7 @@ def setUpClass(cls): cls.script_endpoint = cls.instance.script_endpoints.create( name='test_script_endpoint', - script=cls.script.id + script=cls.script ) def test_cache_request(self): diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index b69d955..b805ad9 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -84,11 +84,11 @@ def test_custom_socket_update(self): ScriptCall(name='script_default', methods=['GET']) ) - self.custom_socket.add_endpoint(Endpoint) - self.custom_socket.update() + socket_to_update.add_endpoint(new_endpoint) + socket_to_update.update() time.sleep(2) # wait for custom socket setup; - self.custom_socket.reload() - self.assertIn('my_endpoint_new_default', self.custom_socket.endpoints) + socket_to_update.reload() + self.assertIn('my_endpoint_new_default', socket_to_update.endpoints) def assert_custom_socket(self, suffix, dependency_method): custom_socket = self._create_custom_socket(suffix, dependency_method=dependency_method) From 4e2ee332c17d09d8bddd4e1855783ac02cfbd54c Mon Sep 17 00:00:00 2001 From: Mariusz Wisniewski Date: Fri, 12 Aug 2016 18:41:24 -0400 Subject: [PATCH 491/558] Language updates --- docs/source/custom_sockets.rst | 79 ++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index 41efde8..0dd81d8 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -4,10 +4,11 @@ Custom Sockets in Syncano ========================= -``Syncano`` provides possibility of creating custom sockets. It means that there's a possibility -to define a very specific endpoints in syncano application and use them as normal API calls. -Currently custom sockets allow only one dependency - script. This mean that on the backend side -each time the API is called - the script is executed and result from this script is returned as a result of the +``Syncano`` gives its users an ability to create custom sockets. It means, that users can define +a very specific endpoints in their Syncano application, and use them as other Syncano +modules (Classes, Scripts, etc), using standard API calls. +Currently, custom sockets allow only one dependency - Scripts. It means that on the backend side, +each API call executes a Script and result of the execution, is returned as a result of the API call. Creating a custom socket @@ -19,16 +20,16 @@ To create a custom socket follow these steps:: from syncano.models import CustomSocket, Endpoint, ScriptCall, ScriptDependency, RuntimeChoices from syncano.connection import Connection - # 1. Initialize the custom socket. + # 1. Initialize a custom socket. custom_socket = CustomSocket(name='my_custom_socket') # this will create an object in place (do API call) # 2. Define endpoints. - my_endpoint = Endpoint(name='my_endpoint') # again - no API call here + my_endpoint = Endpoint(name='my_endpoint') # no API call here my_endpoint.add_call(ScriptCall(name='custom_script'), methods=['GET']) my_endpoint.add_call(ScriptCall(name='another_custom_script'), methods=['POST']) - # explanation for the above lines: - # The endpoint will be seen under `my_endpoint` name: + # Explanation of the above lines: + # Defined endpoint will be visible under `my_endpoint` name: # On this syncano API endpoint the above endpoint will be called (after custom socket creation) # :///instances//endpoints/sockets/my_endpoint/ # On this syncano API endpoint the details of the defined endpoint will be returned @@ -37,11 +38,11 @@ To create a custom socket follow these steps:: # there, second uses the POST method and then the another_custom_script will be called; # Currently only script are available for calls; - # 3. After the creation of the endpoint, add them to custom_socket. + # 3. After creation of the endpoint, add it to your custom_socket. custom_socket.add_endpoint(my_endpoint) - # 4. Define dependency now. - # 4.1 using a new script - defining new source code. + # 4. Define dependency. + # 4.1 Using a new script - define a new source code. custom_socket.add_dependency( ScriptDependency( name='custom_script' @@ -51,7 +52,7 @@ To create a custom socket follow these steps:: ) ) ) - # 4.2 using an existing script. + # 4.2 Using an existing script. another_custom_script = Script.please.get(id=2) custom_socket.add_dependency( ScriptDependency( @@ -60,19 +61,19 @@ To create a custom socket follow these steps:: ) ) - # 4.3 using an existing ScriptEndpoint. + # 4.3 Using an existing ScriptEndpoint. script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name') custom_socket.add_dependency( script_endpoint=script_endpoint ) # 5. Publish custom_socket. - custom_socket.publish() # this will do an API call and will create script; + custom_socket.publish() # this will make an API call and create a script; -Some time is needed to setup the environment for this custom socket. -There is possibility to check the custom socket status:: +Sometimes, it's needed to set up the environment for the custom socket. +It's possible to check the custom socket status:: - # Reload will refresh object using syncano API. + # Reload will refresh object using Syncano API. custom_socket.reload() print(custom_socket.status) # and @@ -85,30 +86,34 @@ To update custom socket, use:: custom_socket = CustomSocket.please.get(name='my_custom_socket') + # to remove endpoint/dependency + custom_socket.remove_endpoint(endpoint_name='my_endpoint') custom_socket.remove_dependency(dependency_name='custom_script') - # or add new: + # or to add a new endpoint/dependency: custom_socket.add_endpoint(new_endpoint) # see above code for endpoint examples; custom_socket.add_dependency(new_dependency) # see above code for dependency examples; + # save changes on Syncano + custom_socket.update() -Running the custom socket +Running custom socket ------------------------- -To run custom socket use:: +To run a custom socket use:: - # this will run the my_endpoint - and call the custom_script (method is GET); + # this will run `my_endpoint` - and call `custom_script` (using GET method); result = custom_socket.run(method='GET', endpoint_name='my_endpoint') -Read all endpoints in custom socket +Read all endpoints in a custom socket ----------------------------------- -To get the all defined endpoints in custom socket run:: +To get the all defined endpoints in a custom socket run:: endpoints = custom_socket.get_endpoints() @@ -122,7 +127,7 @@ To run a particular endpoint:: # or: endpoint.run(method='POST', data={'name': 'test_name'}) -The data will be passed to the API call in the request body. +Data will be passed to the API call in the request body. Read all endpoints ------------------ @@ -140,16 +145,16 @@ Above code will return a list with SocketEndpoint objects. To run such endpoint, Custom sockets endpoints ------------------------ -Each custom socket requires to define at least one endpoint. The endpoint is defined by name and -a list of calls. Each call is defined by a name and a list of methods. The name is a identification for dependency, eg. +Each custom socket requires a definition of at least one endpoint. This endpoint is defined by name and +a list of calls. Each call is defined by its name and a list of methods. Name is used in identification for dependency, eg. if it's equal to 'my_script' - the ScriptEndpoint with name 'my_script' will be used -(if it exists and Script source and runtime matches) or a new one will be created. -There's a special wildcard method: `methods=['*']` - this mean that any request with +(if it exists and Script source and passed runtime match) -- otherwise a new one will be created. +There's a special wildcard method: `methods=['*']` - it means that any request with any method will be executed in this endpoint. -To add an endpoint to the custom_socket use:: +To add an endpoint to a chosen custom_socket use:: - my_endpoint = Endpoint(name='my_endpoint') # again - no API call here + my_endpoint = Endpoint(name='my_endpoint') # no API call here my_endpoint.add_call(ScriptCall(name='custom_script'), methods=['GET']) my_endpoint.add_call(ScriptCall(name='another_custom_script'), methods=['POST']) @@ -158,9 +163,9 @@ To add an endpoint to the custom_socket use:: Custom socket dependency ------------------------ -Each custom socket has dependency - this is a meta information for endpoint: which resource -should be used to return the API call results. The dependencies are bind to the endpoints call objects. -Currently the only supported dependency is script. +Each custom socket has a dependency -- meta information for an endpoint: which resource +should be used to return the API call results. These dependencies are bound to the endpoints call objects. +Currently the only supported dependency is a Script. **Using new script** @@ -201,15 +206,15 @@ Currently the only supported dependency is script. Custom socket recheck --------------------- -The creation of the socket can fail - this happen, eg. when endpoint name is already taken by another +The creation of the socket can fail - this can happen, e.g. when an endpoint name is already taken by another custom socket. To check the statuses use:: print(custom_socket.status) print(custom_socket.status_info) There is a possibility to re-check socket - this mean that if conditions are met - the socket endpoints and dependencies -will be checked - and if some of them are missing (eg. mistake deletion), they will be created again. -If the endpoints and dependencies do not met the criteria - the error will be returned in the status field. +will be checked - and if some of them are missing (e.g. some were deleted by mistake), they will be created again. +If the endpoints and dependencies do not meet the criteria - an error will be returned in the status field. Custom socket - raw format -------------------------- @@ -236,4 +241,4 @@ If you prefer raw JSON format for creating sockets, you can resort to use it in ] ) -The disadvantage of this method is that - the JSON internal structure must be known by developer. +The disadvantage of this method is that internal structure of the JSON file must be known by developer. From fe52e7a88af493a4d2b74ccd30e5fd7203583710 Mon Sep 17 00:00:00 2001 From: Mariusz Wisniewski Date: Fri, 12 Aug 2016 18:47:03 -0400 Subject: [PATCH 492/558] [ci skip] readme --- docs/source/custom_sockets.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index 0dd81d8..9c15956 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -4,11 +4,11 @@ Custom Sockets in Syncano ========================= -``Syncano`` gives its users an ability to create custom sockets. It means, that users can define -a very specific endpoints in their Syncano application, and use them as other Syncano -modules (Classes, Scripts, etc), using standard API calls. -Currently, custom sockets allow only one dependency - Scripts. It means that on the backend side, -each API call executes a Script and result of the execution, is returned as a result of the +``Syncano`` gives its users an ability to create Custom Sockets. What it means is that users can define +a very specific endpoints in their Syncano application, and use them exactly like they would any other Syncano +module (Classes, Scripts, etc), using standard API calls. +Currently, Custom Sockets allow only one dependency - Scripts. It means that under the hood, +each API call executes a Script, and result of this execution is returned as a result of the API call. Creating a custom socket From 7bc974a80e2a0bc3e8c4113a4f6638ffd5c7abb8 Mon Sep 17 00:00:00 2001 From: Mariusz Wisniewski Date: Mon, 15 Aug 2016 16:27:50 -0400 Subject: [PATCH 493/558] [ci skip] Endpoints readme update --- docs/source/custom_sockets.rst | 35 +++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index 9c15956..e0b070f 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -28,15 +28,23 @@ To create a custom socket follow these steps:: my_endpoint.add_call(ScriptCall(name='custom_script'), methods=['GET']) my_endpoint.add_call(ScriptCall(name='another_custom_script'), methods=['POST']) - # Explanation of the above lines: - # Defined endpoint will be visible under `my_endpoint` name: - # On this syncano API endpoint the above endpoint will be called (after custom socket creation) - # :///instances//endpoints/sockets/my_endpoint/ - # On this syncano API endpoint the details of the defined endpoint will be returned + # What happened here: + # - We defined a new endpoint, that will be visible under `my_endpoint` name. + # - You will be able to call this endpoint (execute attached `call`), + # by sending a reuqest, using any defined method to following API route: + # :///instances//endpoints/sockets/my_endpoint/ + # - To get details on that endpoint, you need to send a GET request to following API route: # :///instances//sockets/my_custom_socket/endpoints/my_endpoint/ - # For the above endpoint - the two calls are defined, one uses GET method - the custom_script will be executed - # there, second uses the POST method and then the another_custom_script will be called; - # Currently only script are available for calls; + # + # Following example above - we defined two calls on our endpoint. + # First one means that using GET method will call the `custom_script` script, + # and second one means that using POST method will call the `another_custom_script` script. + # At the moment, only scripts are available as endpoint calls. + # + # As a general rule - to get endpoints details (but not call them), use following API route: + # :///instances//sockets/my_custom_socket/endpoints// + # and to run your endpoints (e.g. execute Script connected to them(, use following API route: + # :///instances//endpoints/sockets// # 3. After creation of the endpoint, add it to your custom_socket. custom_socket.add_endpoint(my_endpoint) @@ -136,11 +144,16 @@ To get all endpoints that are defined in all custom sockets:: socket_endpoint_list = SocketEndpoint.get_all_endpoints() -Above code will return a list with SocketEndpoint objects. To run such endpoint, use:: +Above code will return a list with SocketEndpoint objects. To run an endpoint, +choose one endpoint first, e.g.: - socket_endpoint_list.run(method='GET') + endpoint = socket_endpoint_list[0] + +and now run it:: + + endpoint.run(method='GET') # or: - socket_endpoint_list.run(method='POST', data={'custom_data': 1}) + endpoint.run(method='POST', data={'custom_data': 1}) Custom sockets endpoints ------------------------ From bc71ad14e0cde7788290fc0526c2183d4704ecc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 16 Aug 2016 09:53:09 +0200 Subject: [PATCH 494/558] [LIB-837] revert ModelField on script in script endpoint model; --- syncano/models/backups.py | 2 +- syncano/models/incentives.py | 2 +- tests/integration_test.py | 6 +++--- tests/integration_test_cache.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/syncano/models/backups.py b/syncano/models/backups.py index 3e92342..bf77353 100644 --- a/syncano/models/backups.py +++ b/syncano/models/backups.py @@ -28,7 +28,7 @@ class Backup(Model): size = fields.IntegerField(read_only=True) status = fields.StringField(read_only=True) status_info = fields.StringField(read_only=True) - author = fields.ModelField('Admin', read_only=True) + author = fields.ModelField('Admin') details = fields.JSONField(read_only=True) updated_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/incentives.py b/syncano/models/incentives.py index 585a03a..84ef2e4 100644 --- a/syncano/models/incentives.py +++ b/syncano/models/incentives.py @@ -222,7 +222,7 @@ class ScriptEndpoint(Model): """ name = fields.SlugField(max_length=50, primary_key=True) - script = fields.ModelField('Script', just_pk=True) + script = fields.IntegerField(label='script id') public = fields.BooleanField(required=False, default=False) public_link = fields.ChoiceField(required=False, read_only=True) links = fields.LinksField() diff --git a/tests/integration_test.py b/tests/integration_test.py index 816ea7a..622722c 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -548,7 +548,7 @@ def test_list(self): def test_create(self): script_endpoint = self.model.please.create( instance_name=self.instance.name, - script=self.script, + script=self.script.id, name='wh%s' % self.generate_hash()[:10], ) @@ -557,7 +557,7 @@ def test_create(self): def test_script_run(self): script_endpoint = self.model.please.create( instance_name=self.instance.name, - script=self.script, + script=self.script.id, name='wh%s' % self.generate_hash()[:10], ) @@ -569,7 +569,7 @@ def test_script_run(self): def test_custom_script_run(self): script_endpoint = self.model.please.create( instance_name=self.instance.name, - script=self.custom_script, + script=self.custom_script.id, name='wh%s' % self.generate_hash()[:10], ) diff --git a/tests/integration_test_cache.py b/tests/integration_test_cache.py index ba45bc8..3aff7ad 100644 --- a/tests/integration_test_cache.py +++ b/tests/integration_test_cache.py @@ -51,7 +51,7 @@ def setUpClass(cls): cls.script_endpoint = cls.instance.script_endpoints.create( name='test_script_endpoint', - script=cls.script + script=cls.script.id ) def test_cache_request(self): From 327a3fc993cefd7bfd2113891251c210b9fc41d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 16 Aug 2016 09:55:01 +0200 Subject: [PATCH 495/558] [LIB-837] correct data passing on run methods; --- syncano/models/custom_sockets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index f7a4fda..e5a6909 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -50,9 +50,9 @@ def get_endpoints(self): endpoints.append(SocketEndpoint(**endpoint)) return endpoints - def run(self, method, endpoint_name, data={}): + def run(self, method, endpoint_name, data=None): endpoint = self._find_endpoint(endpoint_name) - return endpoint.run(method, data=data) + return endpoint.run(method, data=data or {}) def _find_endpoint(self, endpoint_name): endpoints = self.get_endpoints() @@ -123,7 +123,7 @@ class Meta: } } - def run(self, method='GET', data={}): + def run(self, method='GET', data=None): endpoint_path = self.links.endpoint connection = self._get_connection() if not self._validate_method(method): @@ -132,7 +132,7 @@ def run(self, method='GET', data={}): if method in ['get', 'delete']: response = connection.request(method, endpoint_path) elif method in ['post', 'put', 'patch']: - response = connection.request(method, endpoint_path, data=data) + response = connection.request(method, endpoint_path, data=data or {}) else: raise SyncanoValueError('Method: {} not supported.'.format(method)) return response From d50344b8f706160b7865aa3f20c783cc1b1619ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 16 Aug 2016 10:31:41 +0200 Subject: [PATCH 496/558] [LIB-837] correct custom socket behaviour; --- syncano/models/custom_sockets.py | 2 +- syncano/models/custom_sockets_utils.py | 40 ++++++++++++++----------- tests/integration_test_custom_socket.py | 23 +++++++------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index e5a6909..1bd177a 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -50,7 +50,7 @@ def get_endpoints(self): endpoints.append(SocketEndpoint(**endpoint)) return endpoints - def run(self, method, endpoint_name, data=None): + def run(self, endpoint_name, method='GET', data=None): endpoint = self._find_endpoint(endpoint_name) return endpoint.run(method, data=data or {}) diff --git a/syncano/models/custom_sockets_utils.py b/syncano/models/custom_sockets_utils.py index 450a40e..4b143b2 100644 --- a/syncano/models/custom_sockets_utils.py +++ b/syncano/models/custom_sockets_utils.py @@ -99,6 +99,7 @@ class BaseDependency(object): fields = [] dependency_type = None + name = None def to_dependency_data(self): if self.dependency_type is None: @@ -136,38 +137,41 @@ class ScriptDependency(BaseDependency): 'source' ] - def __init__(self, name=None, script=None, script_endpoint=None): - if name and script and script_endpoint: - raise SyncanoValueError("Usage: ScriptDependency(name='', script=Script(...)) or " - "ScriptDependency(ScriptEndpoint(...))") - if (name and not script) or (not name and script): - raise SyncanoValueError("Usage: ScriptDependency(name='', script=Script(...))") + def __init__(self, script_or_script_endpoint, name=None): + if not isinstance(script_or_script_endpoint, (Script, ScriptEndpoint)): + raise SyncanoValueError('Script or ScriptEndpoint expected') - if script and not isinstance(script, Script): - raise SyncanoValueError("Expected Script type object.") + if isinstance(script_or_script_endpoint, Script) and not name: + raise SyncanoValueError('Name should be provided') - if script_endpoint and not isinstance(script_endpoint, ScriptEndpoint): - raise SyncanoValueError("Expected ScriptEndpoint type object.") - - if not script_endpoint: - self.dependency_object = ScriptEndpoint(name=name, script=script) - else: - self.dependency_object = script_endpoint + self.dependency_object = script_or_script_endpoint + self.name = name def get_name(self): + if self.name is not None: + return {'name': self.name} return {'name': self.dependency_object.name} def get_dependency_data(self): + + if isinstance(self.dependency_object, ScriptEndpoint): + script = Script.please.get(id=self.dependency_object.script, + instance_name=self.dependency_object.instance_name) + else: + script = self.dependency_object + dependency_data = self.get_name() dependency_data.update({ - field_name: getattr(self.dependency_object.script, field_name) for field_name in self.fields + field_name: getattr(script, field_name) for field_name in self.fields }) return dependency_data @classmethod def create_from_raw_data(cls, raw_data): - return cls(**{'script_endpoint': ScriptEndpoint(name=raw_data['name'], script=Script( - source=raw_data['source'], runtime_name=raw_data['runtime_name']))}) + return cls(**{ + 'script_or_script_endpoint': Script(source=raw_data['source'], runtime_name=raw_data['runtime_name']), + 'name': raw_data['name'], + }) class EndpointMetadataMixin(object): diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index b805ad9..9550237 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -117,15 +117,14 @@ def _define_endpoints(cls, suffix, custom_socket): @classmethod def _define_dependencies_new_script_endpoint(cls, suffix, custom_socket): + script = cls.__create_script(suffix) + script_endpoint = ScriptEndpoint( + name='script_endpoint_{}'.format(suffix), + script=script.id + ) custom_socket.add_dependency( ScriptDependency( - script_endpoint=ScriptEndpoint( - name='script_endpoint_{}'.format(suffix), - script=Script( - source='print({})'.format(suffix), - runtime_name=RuntimeChoices.PYTHON_V5_0 - ) - ) + script_endpoint ) ) @@ -133,11 +132,11 @@ def _define_dependencies_new_script_endpoint(cls, suffix, custom_socket): def _define_dependencies_new_script(cls, suffix, custom_socket): custom_socket.add_dependency( ScriptDependency( - name='script_endpoint_{}'.format(suffix), - script=Script( + Script( source='print({})'.format(suffix), runtime_name=RuntimeChoices.PYTHON_V5_0 - ) + ), + name='script_endpoint_{}'.format(suffix), ) ) @@ -147,8 +146,8 @@ def _define_dependencies_existing_script(cls, suffix, custom_socket): cls._create_script(suffix) custom_socket.add_dependency( ScriptDependency( + Script.please.first(), name='script_endpoint_{}'.format(suffix), - script=Script.please.first() ) ) @@ -161,7 +160,7 @@ def _define_dependencies_existing_script_endpoint(cls, suffix, custom_socket): ) custom_socket.add_dependency( ScriptDependency( - script_endpoint=ScriptEndpoint.please.first() + ScriptEndpoint.please.first() ) ) From 42e4373ca31430ce5d41d0320237347be81ac562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 16 Aug 2016 10:50:17 +0200 Subject: [PATCH 497/558] [LIB-837] corrects after qa; --- syncano/models/fields.py | 2 -- tests/integration_test.py | 2 +- tests/integration_test_custom_socket.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index c8ba8a1..8e4087e 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -478,8 +478,6 @@ def to_native(self, value): class ModelField(Field): - read_only = False - def __init__(self, rel, *args, **kwargs): self.rel = rel self.just_pk = kwargs.pop('just_pk', True) diff --git a/tests/integration_test.py b/tests/integration_test.py index 622722c..8f99229 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -478,7 +478,7 @@ def test_source_run(self): ) trace = script.run() - while trace.status == 'pending': + while trace.status == ['pending', 'processing']: sleep(1) trace.reload() diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 9550237..4f7d4ec 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -117,7 +117,7 @@ def _define_endpoints(cls, suffix, custom_socket): @classmethod def _define_dependencies_new_script_endpoint(cls, suffix, custom_socket): - script = cls.__create_script(suffix) + script = cls._create_script(suffix) script_endpoint = ScriptEndpoint( name='script_endpoint_{}'.format(suffix), script=script.id From 04c5ef6687dc631982edc34ad30696165e48e472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 16 Aug 2016 11:04:16 +0200 Subject: [PATCH 498/558] [LIB-837] corrects after qa; --- tests/integration_test.py | 2 +- tests/integration_test_custom_socket.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/integration_test.py b/tests/integration_test.py index 8f99229..2acfcf3 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -478,7 +478,7 @@ def test_source_run(self): ) trace = script.run() - while trace.status == ['pending', 'processing']: + while trace.status in ['pending', 'processing']: sleep(1) trace.reload() diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 4f7d4ec..e9453ce 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -56,7 +56,7 @@ def test_creating_raw_data(self): self.assertTrue(custom_socket.name) def test_custom_socket_run(self): - results = self.custom_socket.run('GET', 'my_endpoint_default') + results = self.custom_socket.run('my_endpoint_default') self.assertEqual(results.result['stdout'], 'script_default') def test_custom_socket_recheck(self): @@ -70,14 +70,14 @@ def test_fetching_all_endpoints(self): self.assertTrue(all_endpoints[0].name) def test_endpoint_run(self): - script_endpoint = ScriptEndpoint.please.first() - result = script_endpoint.run('GET') + script_endpoint = SocketEndpoint.please.first() + result = script_endpoint.run() suffix = script_endpoint.name.split('_')[-1] self.assertTrue(result.result['stdout'].endswith(suffix)) def test_custom_socket_update(self): socket_to_update = self._create_custom_socket('to_update', self._define_dependencies_new_script_endpoint) - socket_to_update.remove_endpoint(endpoint_name='my_endpoint_default') + socket_to_update.remove_endpoint(endpoint_name='my_endpoint_to_update') new_endpoint = Endpoint(name='my_endpoint_new_to_update') new_endpoint.add_call( @@ -88,7 +88,7 @@ def test_custom_socket_update(self): socket_to_update.update() time.sleep(2) # wait for custom socket setup; socket_to_update.reload() - self.assertIn('my_endpoint_new_default', socket_to_update.endpoints) + self.assertIn('my_endpoint_new_to_update', socket_to_update.endpoints) def assert_custom_socket(self, suffix, dependency_method): custom_socket = self._create_custom_socket(suffix, dependency_method=dependency_method) @@ -156,7 +156,7 @@ def _define_dependencies_existing_script_endpoint(cls, suffix, custom_socket): script = cls._create_script(suffix) ScriptEndpoint.please.create( name='script_endpoint_{}'.format(suffix), - script=script + script=script.id ) custom_socket.add_dependency( ScriptDependency( From 9f5adba3afaa90a3eae2e0e9a038098a8fe48530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 16 Aug 2016 11:47:59 +0200 Subject: [PATCH 499/558] [LIB-837] correct docs examples, polish the tests; --- docs/source/custom_sockets.rst | 30 ++++++++++++++++--------- syncano/models/custom_sockets.py | 4 +++- tests/integration_test_custom_socket.py | 11 +++++++-- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index e0b070f..14d78f1 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -53,26 +53,26 @@ To create a custom socket follow these steps:: # 4.1 Using a new script - define a new source code. custom_socket.add_dependency( ScriptDependency( - name='custom_script' - script=Script( + Script( runtime_name=RuntimeChoices.PYTHON_V5_0, source='print("custom_script")' - ) + ), + name='custom_script' ) ) # 4.2 Using an existing script. another_custom_script = Script.please.get(id=2) custom_socket.add_dependency( ScriptDependency( + another_custom_script, name='another_custom_script', - script=another_custom_script ) ) # 4.3 Using an existing ScriptEndpoint. script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name') custom_socket.add_dependency( - script_endpoint=script_endpoint + script_endpoint ) # 5. Publish custom_socket. @@ -186,11 +186,11 @@ Currently the only supported dependency is a Script. custom_socket.add_dependency( ScriptDependency( - name='custom_script' - script=Script( + Script( runtime_name=RuntimeChoices.PYTHON_V5_0, source='print("custom_script")' - ) + ), + name='custom_script' ) ) @@ -202,8 +202,8 @@ Currently the only supported dependency is a Script. another_custom_script = Script.please.get(id=2) custom_socket.add_dependency( ScriptDependency( - name='another_custom_script', - script=another_custom_script + another_custom_script, + name='another_custom_script' ) ) @@ -213,7 +213,15 @@ Currently the only supported dependency is a Script. script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name') custom_socket.add_dependency( - script_endpoint=script_endpoint + script_endpoint + ) + +You can overwrite the name in the following way:: + + script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name') + custom_socket.add_dependency( + script_endpoint, + name='custom_name' ) Custom socket recheck diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index 1bd177a..98923fe 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -26,6 +26,8 @@ class CustomSocket(EndpointMetadataMixin, DependencyMetadataMixin, Model): metadata = fields.JSONField(read_only=True, required=False) status = fields.StringField(read_only=True, required=False) status_info = fields.StringField(read_only=True, required=False) + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) links = fields.LinksField() class Meta: @@ -52,7 +54,7 @@ def get_endpoints(self): def run(self, endpoint_name, method='GET', data=None): endpoint = self._find_endpoint(endpoint_name) - return endpoint.run(method, data=data or {}) + return endpoint.run(method=method, data=data or {}) def _find_endpoint(self, endpoint_name): endpoints = self.get_endpoints() diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index e9453ce..e9b1efa 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -20,6 +20,7 @@ class CustomSocketTest(InstanceMixin, IntegrationTest): def setUpClass(cls): super(CustomSocketTest, cls).setUpClass() cls.custom_socket = cls._create_custom_socket('default', cls._define_dependencies_new_script_endpoint) + cls._assert_custom_socket(cls.custom_socket) def test_publish_custom_socket(self): # this test new ScriptEndpoint dependency create; @@ -70,7 +71,7 @@ def test_fetching_all_endpoints(self): self.assertTrue(all_endpoints[0].name) def test_endpoint_run(self): - script_endpoint = SocketEndpoint.please.first() + script_endpoint = SocketEndpoint.get_all_endpoints()[0] result = script_endpoint.run() suffix = script_endpoint.name.split('_')[-1] self.assertTrue(result.result['stdout'].endswith(suffix)) @@ -92,7 +93,13 @@ def test_custom_socket_update(self): def assert_custom_socket(self, suffix, dependency_method): custom_socket = self._create_custom_socket(suffix, dependency_method=dependency_method) - self.assertTrue(custom_socket.name) + self._assert_custom_socket(custom_socket) + + @classmethod + def _assert_custom_socket(cls, custom_socket): + cls.assertTrue(custom_socket.name) + cls.assertTrue(custom_socket.created_at) + cls.assertTrue(custom_socket.updated_at) @classmethod def _create_custom_socket(cls, suffix, dependency_method): From 69f3a11f58000099a5442e476b4e088abf20aab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 16 Aug 2016 11:53:50 +0200 Subject: [PATCH 500/558] [LIB-837] remove default custom socket in setupclass; --- tests/integration_test_custom_socket.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index e9b1efa..b435dd0 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -16,12 +16,6 @@ class CustomSocketTest(InstanceMixin, IntegrationTest): - @classmethod - def setUpClass(cls): - super(CustomSocketTest, cls).setUpClass() - cls.custom_socket = cls._create_custom_socket('default', cls._define_dependencies_new_script_endpoint) - cls._assert_custom_socket(cls.custom_socket) - def test_publish_custom_socket(self): # this test new ScriptEndpoint dependency create; self.assert_custom_socket('publishing', self._define_dependencies_new_script_endpoint) @@ -57,12 +51,18 @@ def test_creating_raw_data(self): self.assertTrue(custom_socket.name) def test_custom_socket_run(self): - results = self.custom_socket.run('my_endpoint_default') - self.assertEqual(results.result['stdout'], 'script_default') + suffix = 'default' + custom_socket = self._create_custom_socket(suffix) + self._assert_custom_socket(custom_socket) + results = custom_socket.run('my_endpoint_{}'.format(suffix)) + self.assertEqual(results.result['stdout'], 'script_{}'.format(suffix)) def test_custom_socket_recheck(self): - custom_socket = self.custom_socket.recheck() - self.assertTrue(custom_socket.name) + suffix = 'recheck' + custom_socket = self._create_custom_socket(suffix) + self._assert_custom_socket(custom_socket) + custom_socket = custom_socket.recheck() + self._assert_custom_socket(custom_socket) def test_fetching_all_endpoints(self): all_endpoints = SocketEndpoint.get_all_endpoints() From 3d1f4ddf78812cb8f5721b25168187c4bd87707d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 16 Aug 2016 11:58:29 +0200 Subject: [PATCH 501/558] [LIB-837] remove default custom socket in setupclass; --- tests/integration_test_custom_socket.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index b435dd0..7b1e579 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -52,14 +52,14 @@ def test_creating_raw_data(self): def test_custom_socket_run(self): suffix = 'default' - custom_socket = self._create_custom_socket(suffix) + custom_socket = self._create_custom_socket(suffix, self._define_dependencies_new_script_endpoint) self._assert_custom_socket(custom_socket) results = custom_socket.run('my_endpoint_{}'.format(suffix)) self.assertEqual(results.result['stdout'], 'script_{}'.format(suffix)) def test_custom_socket_recheck(self): suffix = 'recheck' - custom_socket = self._create_custom_socket(suffix) + custom_socket = self._create_custom_socket(suffix, self._define_dependencies_new_script_endpoint) self._assert_custom_socket(custom_socket) custom_socket = custom_socket.recheck() self._assert_custom_socket(custom_socket) @@ -95,11 +95,10 @@ def assert_custom_socket(self, suffix, dependency_method): custom_socket = self._create_custom_socket(suffix, dependency_method=dependency_method) self._assert_custom_socket(custom_socket) - @classmethod - def _assert_custom_socket(cls, custom_socket): - cls.assertTrue(custom_socket.name) - cls.assertTrue(custom_socket.created_at) - cls.assertTrue(custom_socket.updated_at) + def _assert_custom_socket(self, custom_socket): + self.assertTrue(custom_socket.name) + self.assertTrue(custom_socket.created_at) + self.assertTrue(custom_socket.updated_at) @classmethod def _create_custom_socket(cls, suffix, dependency_method): From d982fd03f6926e4b248c91eedb621aa082ab0692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 16 Aug 2016 13:15:37 +0200 Subject: [PATCH 502/558] [LIB-837] final test correction; --- syncano/connection.py | 1 - tests/integration_test_custom_socket.py | 20 +++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/syncano/connection.py b/syncano/connection.py index c4c7706..3fcf6ad 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -273,7 +273,6 @@ def make_request(self, method_name, path, **kwargs): retry_after = response.headers.get('retry-after', 1) time.sleep(float(retry_after)) response = method(url, **params) - content = self.get_response_content(url, response) if files: diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 7b1e579..6819235 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -35,7 +35,7 @@ def test_creating_raw_data(self): name='my_custom_socket_123', endpoints={ "my_custom_endpoint_123": { - "calls": [{"type": "script", "name": "script_123", "methods": ["POST"]}] + "calls": [{"type": "script", "name": "script_123", "methods": ["GET", "POST"]}] } }, dependencies=[ @@ -55,7 +55,7 @@ def test_custom_socket_run(self): custom_socket = self._create_custom_socket(suffix, self._define_dependencies_new_script_endpoint) self._assert_custom_socket(custom_socket) results = custom_socket.run('my_endpoint_{}'.format(suffix)) - self.assertEqual(results.result['stdout'], 'script_{}'.format(suffix)) + self.assertEqual(results['result']['stdout'], suffix) def test_custom_socket_recheck(self): suffix = 'recheck' @@ -73,8 +73,8 @@ def test_fetching_all_endpoints(self): def test_endpoint_run(self): script_endpoint = SocketEndpoint.get_all_endpoints()[0] result = script_endpoint.run() - suffix = script_endpoint.name.split('_')[-1] - self.assertTrue(result.result['stdout'].endswith(suffix)) + self.assertIsInstance(result, dict) + self.assertTrue(result['result']['stdout']) def test_custom_socket_update(self): socket_to_update = self._create_custom_socket('to_update', self._define_dependencies_new_script_endpoint) @@ -96,6 +96,7 @@ def assert_custom_socket(self, suffix, dependency_method): self._assert_custom_socket(custom_socket) def _assert_custom_socket(self, custom_socket): + self._wait_till_socket_process(custom_socket) self.assertTrue(custom_socket.name) self.assertTrue(custom_socket.created_at) self.assertTrue(custom_socket.updated_at) @@ -115,7 +116,7 @@ def _define_endpoints(cls, suffix, custom_socket): endpoint = Endpoint(name='my_endpoint_{}'.format(suffix)) endpoint.add_call( ScriptCall( - name='script_{}'.format(suffix), + name='script_endpoint_{}'.format(suffix), methods=['GET', 'POST'] ) ) @@ -139,7 +140,7 @@ def _define_dependencies_new_script(cls, suffix, custom_socket): custom_socket.add_dependency( ScriptDependency( Script( - source='print({})'.format(suffix), + source='print("{}")'.format(suffix), runtime_name=RuntimeChoices.PYTHON_V5_0 ), name='script_endpoint_{}'.format(suffix), @@ -175,5 +176,10 @@ def _create_script(cls, suffix): return Script.please.create( label='script_{}'.format(suffix), runtime_name=RuntimeChoices.PYTHON_V5_0, - source='print({})'.format(suffix) + source='print("{}")'.format(suffix) ) + + @classmethod + def _wait_till_socket_process(cls, custom_socket): + while custom_socket.status == 'checking': + custom_socket.reload() From 8662e428f098e2e684472b76387e50fc4fb0fac9 Mon Sep 17 00:00:00 2001 From: Devin Visslailli Date: Tue, 16 Aug 2016 11:24:19 -0400 Subject: [PATCH 503/558] Update language --- docs/source/custom_sockets.rst | 100 ++++++++++++++++----------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index 14d78f1..ac6fd63 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -4,23 +4,23 @@ Custom Sockets in Syncano ========================= -``Syncano`` gives its users an ability to create Custom Sockets. What it means is that users can define -a very specific endpoints in their Syncano application, and use them exactly like they would any other Syncano +``Syncano`` gives its users the ability to create Custom Sockets. What this means is that users can define very specific +endpoints in their Syncano application, and use them exactly like they would any other Syncano module (Classes, Scripts, etc), using standard API calls. -Currently, Custom Sockets allow only one dependency - Scripts. It means that under the hood, -each API call executes a Script, and result of this execution is returned as a result of the +Currently, Custom Sockets allow only one dependency - Scripts. Under the hood, +each API call executes a Script, and the result of this execution is returned as a result of the API call. -Creating a custom socket +Creating a custom Socket ------------------------ -To create a custom socket follow these steps:: +To create a custom Socket follow these steps:: import syncano from syncano.models import CustomSocket, Endpoint, ScriptCall, ScriptDependency, RuntimeChoices from syncano.connection import Connection - # 1. Initialize a custom socket. + # 1. Initialize a custom Socket. custom_socket = CustomSocket(name='my_custom_socket') # this will create an object in place (do API call) # 2. Define endpoints. @@ -29,28 +29,28 @@ To create a custom socket follow these steps:: my_endpoint.add_call(ScriptCall(name='another_custom_script'), methods=['POST']) # What happened here: - # - We defined a new endpoint, that will be visible under `my_endpoint` name. + # - We defined a new endpoint that will be visible under the name `my_endpoint` # - You will be able to call this endpoint (execute attached `call`), - # by sending a reuqest, using any defined method to following API route: + # by sending a request, using any defined method to the following API route: # :///instances//endpoints/sockets/my_endpoint/ - # - To get details on that endpoint, you need to send a GET request to following API route: + # - To get details for that endpoint, you need to send a GET request to following API route: # :///instances//sockets/my_custom_socket/endpoints/my_endpoint/ # - # Following example above - we defined two calls on our endpoint. - # First one means that using GET method will call the `custom_script` script, - # and second one means that using POST method will call the `another_custom_script` script. - # At the moment, only scripts are available as endpoint calls. + # Following the example above - we defined two calls on our endpoint with the `add_call` method + # The first one means that using a GET method will call the `custom_script` Script, + # and second one means that using a POST method will call the `another_custom_script` Script. + # At the moment, only Scripts are available as endpoint calls. # - # As a general rule - to get endpoints details (but not call them), use following API route: + # As a general rule - to get endpoint details (but not call them), use following API route: # :///instances//sockets/my_custom_socket/endpoints// - # and to run your endpoints (e.g. execute Script connected to them(, use following API route: + # and to run your endpoints (e.g. execute Script connected to them), use following API route: # :///instances//endpoints/sockets// # 3. After creation of the endpoint, add it to your custom_socket. custom_socket.add_endpoint(my_endpoint) # 4. Define dependency. - # 4.1 Using a new script - define a new source code. + # 4.1 Using a new Script - define a new source code. custom_socket.add_dependency( ScriptDependency( Script( @@ -60,7 +60,7 @@ To create a custom socket follow these steps:: name='custom_script' ) ) - # 4.2 Using an existing script. + # 4.2 Using an existing Script. another_custom_script = Script.please.get(id=2) custom_socket.add_dependency( ScriptDependency( @@ -78,8 +78,8 @@ To create a custom socket follow these steps:: # 5. Publish custom_socket. custom_socket.publish() # this will make an API call and create a script; -Sometimes, it's needed to set up the environment for the custom socket. -It's possible to check the custom socket status:: +It may take some time to set up the Socket, so you can check the status. +It's possible to check the custom Socket status:: # Reload will refresh object using Syncano API. custom_socket.reload() @@ -87,10 +87,10 @@ It's possible to check the custom socket status:: # and print(custom_socket.status_info) -Updating the custom socket +Updating the custom Socket -------------------------- -To update custom socket, use:: +To update custom Socket, use:: custom_socket = CustomSocket.please.get(name='my_custom_socket') @@ -109,19 +109,19 @@ To update custom socket, use:: custom_socket.update() -Running custom socket +Running custom Socket ------------------------- -To run a custom socket use:: +To run a custom Socket use:: - # this will run `my_endpoint` - and call `custom_script` (using GET method); + # this will run `my_endpoint` - and call `custom_script` using GET method; result = custom_socket.run(method='GET', endpoint_name='my_endpoint') -Read all endpoints in a custom socket +Read all endpoints in a custom Socket ----------------------------------- -To get the all defined endpoints in a custom socket run:: +To get the all defined endpoints in a custom Socket run:: endpoints = custom_socket.get_endpoints() @@ -140,7 +140,7 @@ Data will be passed to the API call in the request body. Read all endpoints ------------------ -To get all endpoints that are defined in all custom sockets:: +To get all endpoints that are defined in all custom Sockets:: socket_endpoint_list = SocketEndpoint.get_all_endpoints() @@ -155,15 +155,15 @@ and now run it:: # or: endpoint.run(method='POST', data={'custom_data': 1}) -Custom sockets endpoints +Custom Sockets endpoints ------------------------ -Each custom socket requires a definition of at least one endpoint. This endpoint is defined by name and -a list of calls. Each call is defined by its name and a list of methods. Name is used in identification for dependency, eg. -if it's equal to 'my_script' - the ScriptEndpoint with name 'my_script' will be used -(if it exists and Script source and passed runtime match) -- otherwise a new one will be created. -There's a special wildcard method: `methods=['*']` - it means that any request with -any method will be executed in this endpoint. +Each custom socket requires defining at least one endpoint. This endpoint is defined by name and +a list of calls. Each call is defined by its name and a list of methods. `name` is used as an +identification for the dependency, eg. if `name` is equal to 'my_script' - the ScriptEndpoint with name 'my_script' +will be used (if it exists and Script source and passed runtime match) -- otherwise a new one will be created. +There's a special wildcard method: `methods=['*']` - this allows you to execute the provided custom Socket +with any request method (GET, POST, PATCH, etc.). To add an endpoint to a chosen custom_socket use:: @@ -173,14 +173,14 @@ To add an endpoint to a chosen custom_socket use:: custom_socket.add_endpoint(my_endpoint) -Custom socket dependency +Custom Socket dependency ------------------------ Each custom socket has a dependency -- meta information for an endpoint: which resource -should be used to return the API call results. These dependencies are bound to the endpoints call objects. +should be used to return the API call results. These dependencies are bound to the endpoints call object. Currently the only supported dependency is a Script. -**Using new script** +**Using new Script** :: @@ -195,7 +195,7 @@ Currently the only supported dependency is a Script. ) -**Using defined script** +**Using defined Script** :: @@ -207,7 +207,7 @@ Currently the only supported dependency is a Script. ) ) -**Using defined script endpoint** +**Using defined Script endpoint** :: @@ -216,7 +216,7 @@ Currently the only supported dependency is a Script. script_endpoint ) -You can overwrite the name in the following way:: +You can overwrite the Script name in the following way:: script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name') custom_socket.add_dependency( @@ -224,23 +224,23 @@ You can overwrite the name in the following way:: name='custom_name' ) -Custom socket recheck +Custom Socket recheck --------------------- -The creation of the socket can fail - this can happen, e.g. when an endpoint name is already taken by another -custom socket. To check the statuses use:: +The creation of a Socket can fail - this can happen, for example, when an endpoint name is already taken by another +custom Socket. To check the creation status use:: print(custom_socket.status) print(custom_socket.status_info) -There is a possibility to re-check socket - this mean that if conditions are met - the socket endpoints and dependencies -will be checked - and if some of them are missing (e.g. some were deleted by mistake), they will be created again. -If the endpoints and dependencies do not meet the criteria - an error will be returned in the status field. +You can also re-check a Socket. This mean that all dependencies will be checked - if some of them are missing +(e.g. some were deleted by mistake), they will be created again. If the endpoints and dependencies do not meet +the criteria - an error will be returned in the status field. -Custom socket - raw format +Custom Socket - raw format -------------------------- -If you prefer raw JSON format for creating sockets, you can resort to use it in python library as well:::: +If you prefer raw JSON format for creating Sockets, the Python library allows you to do so:::: CustomSocket.please.create( name='my_custom_socket_3', @@ -262,4 +262,4 @@ If you prefer raw JSON format for creating sockets, you can resort to use it in ] ) -The disadvantage of this method is that internal structure of the JSON file must be known by developer. +The disadvantage of this method is that the internal structure of the JSON file must be known by the developer. From 2ff6c68f3093882007e42c9d75023d7122e6882d Mon Sep 17 00:00:00 2001 From: Marcin Skiba Date: Thu, 18 Aug 2016 08:36:23 +0200 Subject: [PATCH 504/558] [LIB-819] - Add support for creating objects through DataEndpoint --- syncano/models/data_views.py | 3 + tests/integration_test_data_endpoint.py | 139 +++++++++++++----------- 2 files changed, 81 insertions(+), 61 deletions(-) diff --git a/syncano/models/data_views.py b/syncano/models/data_views.py index ae0384d..69dd488 100644 --- a/syncano/models/data_views.py +++ b/syncano/models/data_views.py @@ -127,3 +127,6 @@ def _get_response_template_name(self, response_template): 'Invalid response_template. Must be template\'s name or ResponseTemplate object.' ) return name + + def add_object(self, **kwargs): + return Object(instance_name=self.instance_name, class_name=self.class_name, **kwargs).save() diff --git a/tests/integration_test_data_endpoint.py b/tests/integration_test_data_endpoint.py index 4cccd9f..6ce9c1e 100644 --- a/tests/integration_test_data_endpoint.py +++ b/tests/integration_test_data_endpoint.py @@ -7,76 +7,93 @@ class DataEndpointTest(InstanceMixin, IntegrationTest): - schema = [ - { - 'name': 'title', - 'type': 'string', - 'order_index': True, - 'filter_index': True - } - ] + @classmethod + def setUpClass(cls): + super(DataEndpointTest, cls).setUpClass() - template_content = ''' - {% if action == 'list' %} - {% set objects = response.objects %} - {% elif action == 'retrieve' %} - {% set objects = [response] %} - {% else %} - {% set objects = [] %} - {% endif %} - {% if objects %} - + schema = [ + { + 'name': 'title', + 'type': 'string', + 'order_index': True, + 'filter_index': True + } + ] - - {% for key in objects[0] if key not in fields_to_skip %} - {{ key }} - {% endfor %} - - {% for object in objects %} - - {% for key, value in object.iteritems() if key not in fields_to_skip %} - {{ value }} - {% endfor %} + template_content = ''' + {% if action == 'list' %} + {% set objects = response.objects %} + {% elif action == 'retrieve' %} + {% set objects = [response] %} + {% else %} + {% set objects = [] %} + {% endif %} + {% if objects %} + + + + {% for key in objects[0] if key not in fields_to_skip %} + {{ key }} + {% endfor %} - {% endfor %} - - {% endif %} - ''' + {% for object in objects %} + + {% for key, value in object.iteritems() if key not in fields_to_skip %} + {{ value }} + {% endfor %} + + {% endfor %} + + {% endif %} + ''' - template_context = { - "tr_header_classes": "", - "th_header_classes": "", - "tr_row_classes": "", - "table_classes": "", - "td_row_classes": "", - "fields_to_skip": [ - "id", - "channel", - "channel_room", - "group", - "links", - "group_permissions", - "owner_permissions", - "other_permissions", - "owner", - "revision", - "updated_at", - "created_at" - ] - } + template_context = { + "tr_header_classes": "", + "th_header_classes": "", + "tr_row_classes": "", + "table_classes": "", + "td_row_classes": "", + "fields_to_skip": [ + "id", + "channel", + "channel_room", + "group", + "links", + "group_permissions", + "owner_permissions", + "other_permissions", + "owner", + "revision", + "updated_at", + "created_at" + ] + } - def test_template_response(self): - Class(name='test_class', schema=self.schema).save() - Object(class_name='test_class', title='test_title').save() - template = ResponseTemplate( + cls.klass = Class(name='test_class', schema=schema).save() + cls.template = ResponseTemplate( name='test_template', - content=self.template_content, + content=template_content, content_type='text/html', - context=self.template_context + context=template_context ).save() - data_endpoint = DataEndpoint(name='test_endpoint', class_name='test_class').save() + cls.data_endpoint = DataEndpoint(name='test_endpoint', class_name='test_class').save() - response = list(data_endpoint.get(response_template=template)) + def setUp(self): + for obj in self.instance.classes.get(name='test_class').objects.all(): + obj.delete() + + def test_template_response(self): + Object(class_name=self.klass.name, title='test_title').save() + response = list(self.data_endpoint.get(response_template=self.template)) self.assertEqual(len(response), 1, 'Data endpoint should return 1 element if queried with response_template.') data = re.sub('[\s+]', '', response[0]) self.assertEqual(data, '
    title
    test_title
    ') + + def test_create_object(self): + objects_count = len(list(self.data_endpoint.get())) + self.assertEqual(objects_count, 0) + self.data_endpoint.add_object(title='another title') + objects_count = len(list(self.data_endpoint.get())) + self.assertEqual(objects_count, 1, 'New object should have been created.') + obj = next(self.data_endpoint.get()) + self.assertEqual(obj['title'], 'another title', 'Created object should have proper title.') From 086416324dae1cbcae07fb64f4d2186e5338de68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 18 Aug 2016 11:42:18 +0200 Subject: [PATCH 505/558] [LIB-837] add possibility to directly point instance name when getting all endpoints; --- syncano/models/custom_sockets.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index 98923fe..54156e3 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -140,10 +140,12 @@ def run(self, method='GET', data=None): return response @classmethod - def get_all_endpoints(cls): + def get_all_endpoints(cls, instance_name=None): connection = cls._meta.connection - all_endpoints_path = Instance._meta.resolve_endpoint('endpoints', - {'name': cls.please.properties.get('instance_name')}) + all_endpoints_path = Instance._meta.resolve_endpoint( + 'endpoints', + {'name': cls.please.properties.get('instance_name') or instance_name} + ) response = connection.request('GET', all_endpoints_path) return [cls(**endpoint) for endpoint in response['objects']] From 9246b98a74b49869259b55ea6924cd99102a573c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 19 Aug 2016 14:55:52 +0200 Subject: [PATCH 506/558] [LIB-837] change interface: publish to install in custom socket context; --- docs/source/custom_sockets.rst | 4 ++-- syncano/models/custom_sockets.py | 8 ++++---- tests/integration_test_custom_socket.py | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index 14d78f1..7751515 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -75,8 +75,8 @@ To create a custom socket follow these steps:: script_endpoint ) - # 5. Publish custom_socket. - custom_socket.publish() # this will make an API call and create a script; + # 5. Install custom_socket. + custom_socket.install() # this will make an API call and create a script; Sometimes, it's needed to set up the environment for the custom socket. It's possible to check the custom socket status:: diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index 54156e3..a3a9339 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -23,7 +23,7 @@ class CustomSocket(EndpointMetadataMixin, DependencyMetadataMixin, Model): name = fields.StringField(max_length=64, primary_key=True) endpoints = fields.JSONField() dependencies = fields.JSONField() - metadata = fields.JSONField(read_only=True, required=False) + metadata = fields.JSONField(required=False) status = fields.StringField(read_only=True, required=False) status_info = fields.StringField(read_only=True, required=False) created_at = fields.DateTimeField(read_only=True, required=False) @@ -63,9 +63,9 @@ def _find_endpoint(self, endpoint_name): return endpoint raise SyncanoValueError('Endpoint {} not found.'.format(endpoint_name)) - def publish(self): + def install(self): if not self.is_new(): - raise SyncanoValueError('Can not publish already defined custom socket.') + raise SyncanoValueError('Can not install already defined custom socket.') created_socket = self.__class__.please.create( name=self.name, @@ -79,7 +79,7 @@ def publish(self): def update(self): if self.is_new(): - raise SyncanoValueError('Publish socket first.') + raise SyncanoValueError('Install socket first.') update_socket = self.__class__.please.update( name=self.name, diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 6819235..4f1f14d 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -16,18 +16,18 @@ class CustomSocketTest(InstanceMixin, IntegrationTest): - def test_publish_custom_socket(self): + def test_install_custom_socket(self): # this test new ScriptEndpoint dependency create; - self.assert_custom_socket('publishing', self._define_dependencies_new_script_endpoint) + self.assert_custom_socket('installing', self._define_dependencies_new_script_endpoint) def test_dependencies_new_script(self): - self.assert_custom_socket('new_script_publishing', self._define_dependencies_new_script) + self.assert_custom_socket('new_script_installing', self._define_dependencies_new_script) def test_dependencies_existing_script(self): - self.assert_custom_socket('existing_script_publishing', self._define_dependencies_existing_script) + self.assert_custom_socket('existing_script_installing', self._define_dependencies_existing_script) def test_dependencies_existing_script_endpoint(self): - self.assert_custom_socket('existing_script_e_publishing', + self.assert_custom_socket('existing_script_e_installing', self._define_dependencies_existing_script_endpoint) def test_creating_raw_data(self): @@ -108,7 +108,7 @@ def _create_custom_socket(cls, suffix, dependency_method): cls._define_endpoints(suffix, custom_socket) dependency_method(suffix, custom_socket) - custom_socket.publish() + custom_socket.install() return custom_socket @classmethod From 14f6ff7e0ae34a489519ec1cb82ac43649c647c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 19 Aug 2016 15:04:59 +0200 Subject: [PATCH 507/558] [LIB-837] add status badges to README --- README.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.rst b/README.rst index 2744d96..73e82a8 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,19 @@ Syncano ======= +Build Status +------------ + +**Master** + +.. image:: https://circleci.com/gh/Syncano/syncano-python/tree/master.svg?style=svg&circle-token=738c379fd91cc16b82758e6be89d0c21926655e0 + :target: https://circleci.com/gh/Syncano/syncano-python/tree/master + +**Develop** + +.. image:: https://circleci.com/gh/Syncano/syncano-python/tree/develop.svg?style=svg&circle-token=738c379fd91cc16b82758e6be89d0c21926655e0 + :target: https://circleci.com/gh/Syncano/syncano-python/tree/develop + Python QuickStart Guide ----------------------- From 2372986679ebee9c1df107615d662e20a213914f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 22 Aug 2016 12:51:32 +0200 Subject: [PATCH 508/558] [LIB-837] small changes after CORE change; --- syncano/models/custom_sockets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index a3a9339..e52a6e4 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -109,7 +109,7 @@ class SocketEndpoint(Model): :ivar links: :class:`~syncano.models.fields.LinksField` """ name = fields.StringField(max_length=64, primary_key=True) - calls = fields.JSONField() + allowed_methods = fields.JSONField() links = fields.LinksField() class Meta: @@ -126,7 +126,7 @@ class Meta: } def run(self, method='GET', data=None): - endpoint_path = self.links.endpoint + endpoint_path = self.links.self connection = self._get_connection() if not self._validate_method(method): raise SyncanoValueError('Method: {} not specified in calls for this custom socket.'.format(method)) From 515e50b83e582be4ea83702f0797f3eb000ebcb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 22 Aug 2016 13:17:52 +0200 Subject: [PATCH 509/558] [LIB-837] correct SocketEndpoint behaviour (run mainly); --- syncano/models/custom_sockets.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index e52a6e4..d9fc55b 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -59,7 +59,7 @@ def run(self, endpoint_name, method='GET', data=None): def _find_endpoint(self, endpoint_name): endpoints = self.get_endpoints() for endpoint in endpoints: - if endpoint_name == endpoint.name: + if '{}/{}'.format(self.name, endpoint_name) == endpoint.name: return endpoint raise SyncanoValueError('Endpoint {} not found.'.format(endpoint_name)) @@ -127,14 +127,15 @@ class Meta: def run(self, method='GET', data=None): endpoint_path = self.links.self + _, endpoint_name = self.name.split('/', 1) connection = self._get_connection() if not self._validate_method(method): raise SyncanoValueError('Method: {} not specified in calls for this custom socket.'.format(method)) method = method.lower() if method in ['get', 'delete']: - response = connection.request(method, endpoint_path) + response = connection.request(method, "{}/{}".format(endpoint_path, endpoint_name)) elif method in ['post', 'put', 'patch']: - response = connection.request(method, endpoint_path, data=data or {}) + response = connection.request(method, "{}/{}".format(endpoint_path, endpoint_name), data=data or {}) else: raise SyncanoValueError('Method: {} not supported.'.format(method)) return response @@ -150,10 +151,6 @@ def get_all_endpoints(cls, instance_name=None): return [cls(**endpoint) for endpoint in response['objects']] def _validate_method(self, method): - - methods = [] - for call in self.calls: - methods.extend(call['methods']) - if '*' in methods or method in methods: + if '*' in self.allowed_methods or method in self.allowed_methods: return True return False From 16795dd92c858abbaa6422b8af7cb48110b4c996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 22 Aug 2016 13:53:19 +0200 Subject: [PATCH 510/558] [LIB-837] correct run in CustomSocket; --- syncano/models/custom_sockets.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index d9fc55b..bd4d566 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -44,13 +44,7 @@ class Meta: } def get_endpoints(self): - endpoints_path = self.links.endpoints - connection = self._get_connection() - response = connection.request('GET', endpoints_path) - endpoints = [] - for endpoint in response['objects']: - endpoints.append(SocketEndpoint(**endpoint)) - return endpoints + return SocketEndpoint.get_all_endpoints(instance_name=self.instance_name) def run(self, endpoint_name, method='GET', data=None): endpoint = self._find_endpoint(endpoint_name) @@ -127,15 +121,14 @@ class Meta: def run(self, method='GET', data=None): endpoint_path = self.links.self - _, endpoint_name = self.name.split('/', 1) connection = self._get_connection() if not self._validate_method(method): raise SyncanoValueError('Method: {} not specified in calls for this custom socket.'.format(method)) method = method.lower() if method in ['get', 'delete']: - response = connection.request(method, "{}/{}".format(endpoint_path, endpoint_name)) + response = connection.request(method, endpoint_path) elif method in ['post', 'put', 'patch']: - response = connection.request(method, "{}/{}".format(endpoint_path, endpoint_name), data=data or {}) + response = connection.request(method, endpoint_path, data=data or {}) else: raise SyncanoValueError('Method: {} not supported.'.format(method)) return response From b8c252f84fc903328349d2f2e8916335d7558214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 23 Aug 2016 13:55:28 +0200 Subject: [PATCH 511/558] [LIB-837] add possiblity to install from url; --- syncano/models/custom_sockets.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index bd4d566..c1ae7dd 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -21,6 +21,7 @@ class CustomSocket(EndpointMetadataMixin, DependencyMetadataMixin, Model): """ name = fields.StringField(max_length=64, primary_key=True) + description = fields.StringField(required=False) endpoints = fields.JSONField() dependencies = fields.JSONField() metadata = fields.JSONField(required=False) @@ -57,6 +58,16 @@ def _find_endpoint(self, endpoint_name): return endpoint raise SyncanoValueError('Endpoint {} not found.'.format(endpoint_name)) + def install_from_url(self, url, instance_name=None): + instance_name = self.__class__.please.properties.get('instance_name') or instance_name + instance = Instance.please.get(name=instance_name) + + install_path = instance.links.sockets_install + connection = self._get_connection() + response = connection.request('POST', install_path, data={'name': self.name, 'install_url': url}) + + return response + def install(self): if not self.is_new(): raise SyncanoValueError('Can not install already defined custom socket.') From 3cf2465593f565f7d4f23f8f8b267ccabb75baa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 23 Aug 2016 13:58:26 +0200 Subject: [PATCH 512/558] [LIB-837] update documentation for installing socket from url; --- docs/source/custom_sockets.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index c584ff3..b7fefe7 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -237,6 +237,17 @@ You can also re-check a Socket. This mean that all dependencies will be checked (e.g. some were deleted by mistake), they will be created again. If the endpoints and dependencies do not meet the criteria - an error will be returned in the status field. +Custom Socket - install from url +-------------------------------- + +To install a socket from url use:: + + CustomSocket(name='new_socket_name').install_from_url(url='https://...') + +If instance name was not provided in connection arguments, do:: + + CustomSocket(name='new_socket_name').install_from_url(url='https://...', instance_name='instance_name') + Custom Socket - raw format -------------------------- From ef39be6ace6ac145f84dd6a003a6a7483d061837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 24 Aug 2016 12:54:00 +0200 Subject: [PATCH 513/558] [LIB-837] fixes after qa; --- docs/source/custom_sockets.rst | 2 +- syncano/models/custom_sockets.py | 2 +- syncano/models/custom_sockets_utils.py | 6 +++--- tests/integration_test_custom_socket.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index b7fefe7..f61c811 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -247,7 +247,7 @@ To install a socket from url use:: If instance name was not provided in connection arguments, do:: CustomSocket(name='new_socket_name').install_from_url(url='https://...', instance_name='instance_name') - + Custom Socket - raw format -------------------------- diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index c1ae7dd..441d59c 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -70,7 +70,7 @@ def install_from_url(self, url, instance_name=None): def install(self): if not self.is_new(): - raise SyncanoValueError('Can not install already defined custom socket.') + raise SyncanoValueError('Custom socket already installed.') created_socket = self.__class__.please.create( name=self.name, diff --git a/syncano/models/custom_sockets_utils.py b/syncano/models/custom_sockets_utils.py index 4b143b2..96eaf6e 100644 --- a/syncano/models/custom_sockets_utils.py +++ b/syncano/models/custom_sockets_utils.py @@ -66,7 +66,7 @@ class Endpoint(object): The JSON format is as follows:: { - ': { + '': { 'calls': [ ] @@ -139,10 +139,10 @@ class ScriptDependency(BaseDependency): def __init__(self, script_or_script_endpoint, name=None): if not isinstance(script_or_script_endpoint, (Script, ScriptEndpoint)): - raise SyncanoValueError('Script or ScriptEndpoint expected') + raise SyncanoValueError('Script or ScriptEndpoint expected.') if isinstance(script_or_script_endpoint, Script) and not name: - raise SyncanoValueError('Name should be provided') + raise SyncanoValueError('Name should be provided.') self.dependency_object = script_or_script_endpoint self.name = name diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 4f1f14d..2ddd2f3 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -17,7 +17,7 @@ class CustomSocketTest(InstanceMixin, IntegrationTest): def test_install_custom_socket(self): - # this test new ScriptEndpoint dependency create; + # this tests new ScriptEndpoint dependency create; self.assert_custom_socket('installing', self._define_dependencies_new_script_endpoint) def test_dependencies_new_script(self): From 21ddeaf3835840bdd4781dab5ffba739adbd1fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Wed, 24 Aug 2016 14:25:16 +0200 Subject: [PATCH 514/558] [RELEASE v5.4] bump the version; --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 93b0df2..e0c54af 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.3.0' +__version__ = '5.4.0' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From 12b3ea2e40083fe0b30b6a971a1b187e2c8ac39b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 25 Aug 2016 11:52:30 +0200 Subject: [PATCH 515/558] [LIB-849] fix for filtering; --- syncano/models/manager.py | 13 ++++++++++--- tests/integration_test_relations.py | 7 +++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index 07d6d5c..c6ab313 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -972,9 +972,16 @@ def _build_query(self, query_data, **kwargs): if self.LOOKUP_SEPARATOR in field_name: model_name, field_name, lookup = self._get_lookup_attributes(field_name) - for field in model._meta.fields: - if field.name == field_name: - break + # if filter is made on relation field: relation__name__eq='test'; + if model_name: + for field in model._meta.fields: + if field.name == model_name: + break + # if filter is made on normal field: name__eq='test'; + else: + for field in model._meta.fields: + if field.name == field_name: + break self._validate_lookup(model, model_name, field_name, lookup, field) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 37348b6..7b01efc 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -90,3 +90,10 @@ def test_related_field_lookup_is(self): self.assertEqual(len(list(filtered_books)), 1) for book in filtered_books: self.assertEqual(book.title, self.niezwyciezony.title) + + def test_multiple_lookups(self): + filtered_books = self.books.objects.list().filter(authors__id__in=[self.prus.id], title__eq='Lalka') + + self.assertEqual(len(list(filtered_books)), 1) + for book in filtered_books: + self.assertEqual(book.title, self.niezwyciezony.title) From 165bd59ea476dc047664429d0900c23a434a1691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 25 Aug 2016 12:03:22 +0200 Subject: [PATCH 516/558] [LIB-849] correct naming; --- tests/integration_test_relations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 7b01efc..980f69c 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -3,11 +3,11 @@ from tests.integration_test import InstanceMixin, IntegrationTest -class ResponseTemplateApiTest(InstanceMixin, IntegrationTest): +class RelationApiTest(InstanceMixin, IntegrationTest): @classmethod def setUpClass(cls): - super(ResponseTemplateApiTest, cls).setUpClass() + super(RelationApiTest, cls).setUpClass() # prapare data cls.author = Class.please.create(name="author", schema=[ @@ -92,7 +92,7 @@ def test_related_field_lookup_is(self): self.assertEqual(book.title, self.niezwyciezony.title) def test_multiple_lookups(self): - filtered_books = self.books.objects.list().filter(authors__id__in=[self.prus.id], title__eq='Lalka') + filtered_books = self.book.objects.list().filter(authors__id__in=[self.prus.id], title__eq='Lalka') self.assertEqual(len(list(filtered_books)), 1) for book in filtered_books: From 27ae02f40b5b65165afc24c142e3e70de693c60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 25 Aug 2016 12:25:34 +0200 Subject: [PATCH 517/558] [LIB-849] correct naming; --- tests/integration_test_relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py index 980f69c..052a424 100644 --- a/tests/integration_test_relations.py +++ b/tests/integration_test_relations.py @@ -96,4 +96,4 @@ def test_multiple_lookups(self): self.assertEqual(len(list(filtered_books)), 1) for book in filtered_books: - self.assertEqual(book.title, self.niezwyciezony.title) + self.assertEqual(book.title, self.lalka.title) From 962253e4db463a5522568d57b04ae236187c900f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 29 Aug 2016 13:57:28 +0200 Subject: [PATCH 518/558] [LIB-852] re-work hosting; add HostingFile models; add update_file method; --- syncano/models/hosting.py | 80 ++++++++++++++++++++++++++---- tests/integration_tests_hosting.py | 21 ++++++-- 2 files changed, 87 insertions(+), 14 deletions(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index 39c32e7..92f6afe 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- + +from syncano.exceptions import SyncanoRequestError + from . import fields from .base import Instance, Model, logger @@ -31,24 +34,51 @@ class Meta: } def upload_file(self, path, file): + """ + Upload a new file to the hosting. + :param path: the file path; + :param file: the file to be uploaded; + :return: the response from the API; + """ files_path = self.links.files data = {'path': path} connection = self._get_connection() - params = connection.build_params(params={}) - headers = params['headers'] - headers.pop('content-type') - response = connection.session.post(connection.host + files_path, headers=headers, + headers = self._prepare_header(connection) + response = connection.session.post('{}{}'.format(connection.host, files_path), headers=headers, data=data, files=[('file', file)]) if response.status_code != 201: logger.error(response.text) return - return response + return HostingFile(**response.json()) + + def update_file(self, path, file): + """ + Updates an existing file. + :param path: the file path; + :param file: the file to be uploaded; + :return: the response from the API; + """ + hosting_files = self._get_files() + is_found = False + for hosting_file in hosting_files: + if hosting_file.path == path: + is_found = True + break + + if not is_found: + raise SyncanoRequestError('File with path {} not found.'.format(path)) - def list_files(self): - files_path = self.links.files connection = self._get_connection() - response = connection.request('GET', files_path) - return [f['path'] for f in response['objects']] + headers = self._prepare_header(connection) + response = connection.session.patch('{}{}'.format(connection.host, hosting_file.links.self), headers=headers, + files=[('file', file)]) + if response.status_code != 200: + logger.error(response.text) + return + return HostingFile(**response.json()) + + def list_files(self): + return self._get_files() def set_default(self): default_path = self.links.set_default @@ -57,3 +87,35 @@ def set_default(self): response = connection.make_request('POST', default_path) self.to_python(response) return self + + def _prepare_header(self, connection): + params = connection.build_params(params={}) + headers = params['headers'] + headers.pop('content-type') + return headers + + def _get_files(self): + return [hfile for hfile in HostingFile.please.list(hosting_id=self.id)] + + +class HostingFile(Model): + """ + OO wrapper around hosting file. + """ + + path = fields.StringField(max_length=300) + file = fields.FileField() + links = fields.LinksField() + + class Meta: + parent = Hosting + endpoints = { + 'detail': { + 'methods': ['delete', 'get', 'put', 'patch'], + 'path': '/files/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/files/', + } + } diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 8056ae1..1be56a5 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -25,12 +25,23 @@ def test_create_file(self): a_hosting_file.write('h1 {color: #541231;}') a_hosting_file.seek(0) - self.hosting.upload_file(path='styles/main.css', file=a_hosting_file) - - files_list = self.hosting.list_files() - - self.assertIn('styles/main.css', files_list) + hosting_file = self.hosting.upload_file(path='styles/main.css', file=a_hosting_file) + self.assertEqual(hosting_file.path, 'styles/main.css') def test_set_default(self): hosting = self.hosting.set_default() self.assertIn('default', hosting.domains) + + def test_update_file(self): + a_hosting_file = StringIO() + a_hosting_file.write('h1 {color: #541231;}') + a_hosting_file.seek(0) + + self.hosting.upload_file(path='styles/main.css', file=a_hosting_file) + + a_hosting_file = StringIO() + a_hosting_file.write('h2 {color: #541231;}') + a_hosting_file.seek(0) + + hosting_file = self.hosting.update_file(path='styles/main.css', file=a_hosting_file) + self.assertEqual(hosting_file.path, 'styles/main.css') From d7b0d83ced094cfba57c9eca7ba02e4f1e025e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 29 Aug 2016 16:20:08 +0200 Subject: [PATCH 519/558] [LIB-852] allow to create file when updating non exsiting; --- syncano/models/hosting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index 92f6afe..377d7c5 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -60,13 +60,16 @@ def update_file(self, path, file): """ hosting_files = self._get_files() is_found = False + for hosting_file in hosting_files: if hosting_file.path == path: is_found = True break if not is_found: - raise SyncanoRequestError('File with path {} not found.'.format(path)) + # create if not found; + hosting_file = self.upload_file(path, file) + return hosting_file connection = self._get_connection() headers = self._prepare_header(connection) From 790ca62bb1d141fc1f84f58aed06791c5f6002b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 29 Aug 2016 16:25:08 +0200 Subject: [PATCH 520/558] [LIB-852] remove unused import; --- syncano/models/hosting.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index 377d7c5..ab80819 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from syncano.exceptions import SyncanoRequestError - from . import fields from .base import Instance, Model, logger From 0e21d15a81f92015c0e91339da6e69047600f0f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 30 Aug 2016 15:18:15 +0200 Subject: [PATCH 521/558] [RELEASE v5.4.1] bump the version; --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index e0c54af..311afea 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.4.0' +__version__ = '5.4.1' __author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From 1b37898285822ef66fee9fa1f0f540e115246148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 9 Sep 2016 14:57:45 +0200 Subject: [PATCH 522/558] [LIB-886] add possibility to register in LIB; --- syncano/connection.py | 24 ++++++++++++++++++++---- tests/integration_test_register.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 tests/integration_test_register.py diff --git a/syncano/connection.py b/syncano/connection.py index 3fcf6ad..f7c54f9 100644 --- a/syncano/connection.py +++ b/syncano/connection.py @@ -65,12 +65,14 @@ class Connection(object): CONTENT_TYPE = 'application/json' - AUTH_SUFFIX = 'v1/account/auth' - ACCOUNT_SUFFIX = 'v1/account/' + AUTH_SUFFIX = 'v1.1/account/auth' + ACCOUNT_SUFFIX = 'v1.1/account/' SOCIAL_AUTH_SUFFIX = AUTH_SUFFIX + '/{social_backend}/' - USER_AUTH_SUFFIX = 'v1/instances/{name}/user/auth/' - USER_INFO_SUFFIX = 'v1/instances/{name}/user/' + USER_AUTH_SUFFIX = 'v1.1/instances/{name}/user/auth/' + USER_INFO_SUFFIX = 'v1.1/instances/{name}/user/' + + REGISTER_SUFFIX = 'v1.1/account/register/' LOGIN_PARAMS = {'email', 'password'} @@ -432,6 +434,20 @@ def _process_apns_cert_files(self, files): files[key] = value return files + def register(self, email, password, first_name=None, last_name=None, invitation_key=None): + register_data = { + 'email': email, + 'password': password, + } + for name, value in zip(['first_name', 'last_name', 'invitation_key'], + [first_name, last_name, invitation_key]): + if value: + register_data.update({name: value}) + response = self.make_request('POST', self.REGISTER_SUFFIX, data=register_data) + + self.api_key = response['account_key'] + return self.api_key + class ConnectionMixin(object): """Injects connection attribute with support of basic validation.""" diff --git a/tests/integration_test_register.py b/tests/integration_test_register.py new file mode 100644 index 0000000..14a1d09 --- /dev/null +++ b/tests/integration_test_register.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +import os +import random +import unittest + +import syncano + + +class RegistrationTest(unittest.TestCase): + + def test_register(self): + connection = syncano.connect( + host=os.getenv('INTEGRATION_API_ROOT'), + ) + + email = 'syncano.bot+997999{}@syncano.com'.format(random.randint(100000, 50000000)) + + connection.connection().register( + email=email, + password='test11', + first_name='Jan', + last_name='Nowak' + ) + + # test if LIB has a key now; + account_info = connection.connection().get_account_info() + self.assertIn('email', account_info) + self.assertEqual(account_info['email'], email) From 692e0fec9c9bfc4803f1840d9bc93a41c428e1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 12 Sep 2016 12:18:36 +0200 Subject: [PATCH 523/558] [RELEASE v5.4.2] bump version; correct package information; --- setup.py | 3 ++- syncano/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 66d524b..587588b 100644 --- a/setup.py +++ b/setup.py @@ -17,10 +17,11 @@ def readme(): packages=find_packages(exclude=['tests']), zip_safe=False, classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', ], install_requires=[ 'requests==2.7.0', diff --git a/syncano/__init__.py b/syncano/__init__.py index 311afea..dfa96cc 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,8 +2,8 @@ import os __title__ = 'Syncano Python' -__version__ = '5.4.1' -__author__ = "Daniel Kopka, Michal Kobus, and Sebastian Opalczynski" +__version__ = '5.4.2' +__author__ = "Daniel Kopka, Michal Kobus and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", "Sebastian Opalczynski"] From e2418a5f670e8085ac6b9d6caa70cc0f62bac5a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 22 Sep 2016 16:15:46 +0200 Subject: [PATCH 524/558] [LIB-898] add class dependency to the library; add config field to CustomSocket; --- docs/source/custom_sockets.rst | 31 +++++++++++- syncano/models/base.py | 2 +- syncano/models/custom_sockets.py | 1 + syncano/models/custom_sockets_utils.py | 55 +++++++++++++++++---- tests/integration_test_custom_socket.py | 63 +++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 12 deletions(-) diff --git a/docs/source/custom_sockets.rst b/docs/source/custom_sockets.rst index f61c811..787e564 100644 --- a/docs/source/custom_sockets.rst +++ b/docs/source/custom_sockets.rst @@ -25,8 +25,8 @@ To create a custom Socket follow these steps:: # 2. Define endpoints. my_endpoint = Endpoint(name='my_endpoint') # no API call here - my_endpoint.add_call(ScriptCall(name='custom_script'), methods=['GET']) - my_endpoint.add_call(ScriptCall(name='another_custom_script'), methods=['POST']) + my_endpoint.add_call(ScriptCall(name='custom_script', methods=['GET'])) + my_endpoint.add_call(ScriptCall(name='another_custom_script', methods=['POST'])) # What happened here: # - We defined a new endpoint that will be visible under the name `my_endpoint` @@ -224,6 +224,33 @@ You can overwrite the Script name in the following way:: name='custom_name' ) +** Class dependency ** + +Custom socket with this dependency will check if this class is defined - if not then will create it; +This allows you to define which classes are used to store data for this particular custom socket. + +:: + + custom_socket.add_dependency( + ClassDependency( + Class( + name='class_dep_test', + schema=[ + {'name': 'test', 'type': 'string'} + ] + ), + ) + ) + +Existing class:: + + class_instance = Class.plase.get(name='user_profile') + custom_socket.add_dependency( + ClassDependency( + class_instance + ) + ) + Custom Socket recheck --------------------- diff --git a/syncano/models/base.py b/syncano/models/base.py index d8c4db9..015217b 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -14,4 +14,4 @@ from .hosting import * # NOQA from .data_views import DataEndpoint as EndpointData # NOQA from .custom_sockets import * # NOQA -from .custom_sockets_utils import Endpoint, ScriptCall, ScriptDependency # NOQA +from .custom_sockets_utils import Endpoint, ScriptCall, ScriptDependency, ClassDependency # NOQA diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index 441d59c..363ce66 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -25,6 +25,7 @@ class CustomSocket(EndpointMetadataMixin, DependencyMetadataMixin, Model): endpoints = fields.JSONField() dependencies = fields.JSONField() metadata = fields.JSONField(required=False) + config = fields.JSONField(required=False) status = fields.StringField(read_only=True, required=False) status_info = fields.StringField(read_only=True, required=False) created_at = fields.DateTimeField(read_only=True, required=False) diff --git a/syncano/models/custom_sockets_utils.py b/syncano/models/custom_sockets_utils.py index 96eaf6e..45d1196 100644 --- a/syncano/models/custom_sockets_utils.py +++ b/syncano/models/custom_sockets_utils.py @@ -2,6 +2,7 @@ import six from syncano.exceptions import SyncanoValueError +from .classes import Class from .incentives import Script, ScriptEndpoint @@ -17,6 +18,7 @@ class DependencyType(object): The type of the dependency object used in the custom socket; """ SCRIPT = 'script' + CLASS = 'class' class BaseCall(object): @@ -109,7 +111,9 @@ def to_dependency_data(self): return dependency_data def get_name(self): - raise NotImplementedError() + if self.name is not None: + return {'name': self.name} + return {'name': self.dependency_object.name} def get_dependency_data(self): raise NotImplementedError() @@ -117,6 +121,9 @@ def get_dependency_data(self): def create_from_raw_data(self, raw_data): raise NotImplementedError() + def _build_dict(self, instance): + return {field_name: getattr(instance, field_name) for field_name in self.fields} + class ScriptDependency(BaseDependency): """ @@ -147,11 +154,6 @@ def __init__(self, script_or_script_endpoint, name=None): self.dependency_object = script_or_script_endpoint self.name = name - def get_name(self): - if self.name is not None: - return {'name': self.name} - return {'name': self.dependency_object.name} - def get_dependency_data(self): if isinstance(self.dependency_object, ScriptEndpoint): @@ -161,9 +163,7 @@ def get_dependency_data(self): script = self.dependency_object dependency_data = self.get_name() - dependency_data.update({ - field_name: getattr(script, field_name) for field_name in self.fields - }) + dependency_data.update(self._build_dict(script)) return dependency_data @classmethod @@ -174,6 +174,41 @@ def create_from_raw_data(cls, raw_data): }) +class ClassDependency(BaseDependency): + """ + Class dependency object; + + The JSON format is as follows:: + { + 'type': 'class', + 'name': '', + 'schema': [ + {"name": "f1", "type": "string"}, + {"name": "f2", "type": "string"}, + {"name": "f3", "type": "integer"} + ], + } + """ + dependency_type = DependencyType.CLASS + fields = [ + 'name', + 'schema' + ] + + def __init__(self, class_instance): + self.dependency_object = class_instance + self.name = class_instance.name + + def get_dependency_data(self): + data_dict = self._build_dict(self.dependency_object) + data_dict['schema'] = data_dict['schema'].schema + return data_dict + + @classmethod + def create_from_raw_data(cls, raw_data): + return cls(**{'class_instance': Class(**raw_data)}) + + class EndpointMetadataMixin(object): """ A mixin which allows to collect Endpoints objects and transform them to the appropriate JSON format. @@ -239,6 +274,8 @@ def update_dependencies(self): def _get_depedency_klass(cls, depedency_type): if depedency_type == DependencyType.SCRIPT: return ScriptDependency + elif depedency_type == DependencyType.CLASS: + return ClassDependency def add_dependency(self, depedency): self._dependencies.append(depedency) diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 2ddd2f3..060d55a 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -2,6 +2,8 @@ import time from syncano.models import ( + Class, + ClassDependency, CustomSocket, Endpoint, RuntimeChoices, @@ -44,6 +46,13 @@ def test_creating_raw_data(self): "runtime_name": "python_library_v5.0", "name": "script_123", "source": "print(123)" + }, + { + "type": "class", + "schema": [ + {"name": "test_class", "type": "string"}, + {"name": "test_class", "type": "int"}, + ] } ] ) @@ -91,6 +100,16 @@ def test_custom_socket_update(self): socket_to_update.reload() self.assertIn('my_endpoint_new_to_update', socket_to_update.endpoints) + def test_class_dependency_new(self): + suffix = 'new_class' + custom_socket = self._create_custom_socket(suffix, self._define_dependencies_new_class) + self._assert_custom_socket(custom_socket) + + def test_class_dependency_existing(self): + suffix = 'existing_class' + custom_socket = self._create_custom_socket(suffix, self._define_dependencies_new_class) + self._assert_custom_socket(custom_socket) + def assert_custom_socket(self, suffix, dependency_method): custom_socket = self._create_custom_socket(suffix, dependency_method=dependency_method) self._assert_custom_socket(custom_socket) @@ -122,6 +141,38 @@ def _define_endpoints(cls, suffix, custom_socket): ) custom_socket.add_endpoint(endpoint) + @classmethod + def _define_dependencies_new_class(cls, suffix, custom_socket): + cls._add_base_script(suffix, custom_socket) + custom_socket.add_dependency( + ClassDependency( + Class( + name="test_class_{}".format(suffix), + schema=[ + {"name": "testA", "type": "string"}, + {"name": "testB", "type": "int"}, + ] + ) + ) + ) + + @classmethod + def _define_dependencies_existing_class(cls, suffix, custom_socket): + cls._add_base_script(suffix, custom_socket) + klass = Class( + name="test_class_{}".format(suffix), + schema=[ + {"name": "testA", "type": "string"}, + {"name": "testB", "type": "int"}, + ] + ) + klass.save() + custom_socket.add_dependency( + ClassDependency( + klass + ) + ) + @classmethod def _define_dependencies_new_script_endpoint(cls, suffix, custom_socket): script = cls._create_script(suffix) @@ -171,6 +222,18 @@ def _define_dependencies_existing_script_endpoint(cls, suffix, custom_socket): ) ) + @classmethod + def _add_base_script(cls, suffix, custom_socket): + custom_socket.add_dependency( + ScriptDependency( + Script( + source='print("{}")'.format(suffix), + runtime_name=RuntimeChoices.PYTHON_V5_0 + ), + name='script_endpoint_{}'.format(suffix), + ) + ) + @classmethod def _create_script(cls, suffix): return Script.please.create( From 7654acc14c36ddfb739eac8240b84eb861356e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 22 Sep 2016 16:36:03 +0200 Subject: [PATCH 525/558] [LIB-898] correct tests; --- tests/integration_test_custom_socket.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 060d55a..d01010b 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -51,7 +51,7 @@ def test_creating_raw_data(self): "type": "class", "schema": [ {"name": "test_class", "type": "string"}, - {"name": "test_class", "type": "int"}, + {"name": "test_class", "type": "integer"}, ] } ] @@ -150,7 +150,7 @@ def _define_dependencies_new_class(cls, suffix, custom_socket): name="test_class_{}".format(suffix), schema=[ {"name": "testA", "type": "string"}, - {"name": "testB", "type": "int"}, + {"name": "testB", "type": "integer"}, ] ) ) @@ -163,7 +163,7 @@ def _define_dependencies_existing_class(cls, suffix, custom_socket): name="test_class_{}".format(suffix), schema=[ {"name": "testA", "type": "string"}, - {"name": "testB", "type": "int"}, + {"name": "testB", "type": "integer"}, ] ) klass.save() From 1d20ef53115a08bc8877cf756343663e06044d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 22 Sep 2016 16:47:38 +0200 Subject: [PATCH 526/558] [LIB-898] correct tests; --- tests/integration_test_accounts.py | 2 +- tests/integration_test_custom_socket.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py index ac467db..5e54e87 100644 --- a/tests/integration_test_accounts.py +++ b/tests/integration_test_accounts.py @@ -14,7 +14,7 @@ def setUpClass(cls): cls.INSTANCE_NAME = os.getenv('INTEGRATION_INSTANCE_NAME') cls.USER_NAME = os.getenv('INTEGRATION_USER_NAME') cls.USER_PASSWORD = os.getenv('INTEGRATION_USER_PASSWORD') - cls.CLASS_NAME = cls.INSTANCE_NAME + cls.CLASS_NAME = "login_class_test" instance = cls.connection.Instance.please.create(name=cls.INSTANCE_NAME) api_key = instance.api_keys.create(allow_user_create=True, diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index d01010b..1d5a0f8 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -49,6 +49,7 @@ def test_creating_raw_data(self): }, { "type": "class", + "name": "klass", "schema": [ {"name": "test_class", "type": "string"}, {"name": "test_class", "type": "integer"}, From 33294e249c5f54f8c35f43fa42895ca4cf506377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Thu, 22 Sep 2016 17:01:18 +0200 Subject: [PATCH 527/558] [LIB-898] correct tests; --- tests/integration_test_accounts.py | 2 +- tests/integration_test_custom_socket.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py index 5e54e87..44988c3 100644 --- a/tests/integration_test_accounts.py +++ b/tests/integration_test_accounts.py @@ -35,7 +35,7 @@ def tearDownClass(cls): cls.connection = None def check_connection(self, con): - response = con.request('GET', '/v1.1/instances/test_login/classes/') + response = con.request('GET', '/v1.1/instances/{}/classes/'.format(self.INSTANCE_NAME)) obj_list = response['objects'] diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py index 1d5a0f8..16b4623 100644 --- a/tests/integration_test_custom_socket.py +++ b/tests/integration_test_custom_socket.py @@ -51,8 +51,8 @@ def test_creating_raw_data(self): "type": "class", "name": "klass", "schema": [ - {"name": "test_class", "type": "string"}, - {"name": "test_class", "type": "integer"}, + {"name": "fieldA", "type": "string"}, + {"name": "fieldB", "type": "integer"}, ] } ] From 18a2996effde0803d56edebb3b24b31a1d163810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 23 Sep 2016 14:48:36 +0200 Subject: [PATCH 528/558] [LIB-901] add config field to install from ulr; --- syncano/models/custom_sockets.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/syncano/models/custom_sockets.py b/syncano/models/custom_sockets.py index 441d59c..b18fb78 100644 --- a/syncano/models/custom_sockets.py +++ b/syncano/models/custom_sockets.py @@ -58,13 +58,18 @@ def _find_endpoint(self, endpoint_name): return endpoint raise SyncanoValueError('Endpoint {} not found.'.format(endpoint_name)) - def install_from_url(self, url, instance_name=None): + def install_from_url(self, url, instance_name=None, config=None): instance_name = self.__class__.please.properties.get('instance_name') or instance_name instance = Instance.please.get(name=instance_name) install_path = instance.links.sockets_install connection = self._get_connection() - response = connection.request('POST', install_path, data={'name': self.name, 'install_url': url}) + config = config or {} + response = connection.request('POST', install_path, data={ + 'name': self.name, + 'install_url': url, + 'config': config + }) return response From d96df707bb7f6f2ebe81c261d1b3a0e272ea87d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 23 Sep 2016 16:39:43 +0200 Subject: [PATCH 529/558] [RELEASE v5.4.3] bump the versionl --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index dfa96cc..31d3523 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.4.2' +__version__ = '5.4.3' __author__ = "Daniel Kopka, Michal Kobus and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From ebf8e63717c2c4c55582087d96edd30f6813d26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Mon, 26 Sep 2016 22:20:15 +0200 Subject: [PATCH 530/558] [HOTFIX] handling serialization/desrialization of the files properly; --- syncano/models/fields.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/syncano/models/fields.py b/syncano/models/fields.py index 8e4087e..967fd64 100644 --- a/syncano/models/fields.py +++ b/syncano/models/fields.py @@ -547,6 +547,8 @@ class FileField(WritableField): param_name = 'files' def to_native(self, value): + if isinstance(value, six.string_types): + return None return {self.name: value} From 697932bca75a09883c838d22f153f50050cacfca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 11:50:19 +0200 Subject: [PATCH 531/558] [HOT-FIX] files hot fix: create tests; --- tests/integration_test_data_objects.py | 126 +++++++++++++++++++++++++ tests/test_files/python-logo.png | Bin 0 -> 11155 bytes 2 files changed, 126 insertions(+) create mode 100644 tests/integration_test_data_objects.py create mode 100644 tests/test_files/python-logo.png diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py new file mode 100644 index 0000000..9254645 --- /dev/null +++ b/tests/integration_test_data_objects.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +from hashlib import md5 +from StringIO import StringIO + +import requests +from syncano.models import Object +from tests.integration_test import InstanceMixin, IntegrationTest + + +class DataObjectFileTest(InstanceMixin, IntegrationTest): + + @classmethod + def setUpClass(cls): + super(DataObjectFileTest, cls).setUpClass() + + cls.schema = [ + {'name': 'test_field_a', 'type': 'string'}, + {'name': 'test_field_file', 'type': 'file'}, + ] + cls.class_name = 'test_object_file' + cls.initial_field_a = 'some_string' + cls.file_path = 'tests/test_files/python-logo.png' + cls.instance.classes.create( + name=cls.class_name, + schema=cls.schema + ) + with open(cls.file_path, 'rb') as f: + cls.file_md5 = cls.get_file_md5(f.read()) + + def test_creating_file_object(self): + data_object = self._create_object_with_file() + self.assertEqual(data_object.test_field_a, self.initial_field_a) + self.assert_file_md5(data_object) + + def test_updating_another_field(self): + data_object = self._create_object_with_file() + file_url = data_object.test_field_file + + # no changes made to the file; + update_string = 'some_other_string' + data_object.test_field_a = update_string + data_object.save() + + self.assertEqual(data_object.test_field_file, file_url) + self.assertEqual(data_object.test_field_a, update_string) + self.assert_file_md5(data_object) + + def test_updating_file_field(self): + data_object = self._create_object_with_file() + file_url = data_object.test_field_file + + update_string = 'updating also field a' + file_content = 'some example text file;' + new_file = StringIO(file_content) + data_object.test_field_file = new_file + data_object.test_field_a = update_string + data_object.save() + + self.assertEqual(data_object.test_field_a, update_string) + self.assertNotEqual(data_object.test_field_file, file_url) + + # check file content; + file_content_s3 = requests.get(data_object.test_field_file).text + self.assertEqual(file_content_s3, file_content) + + def test_manager_update(self): + data_object = self._create_object_with_file() + file_url = data_object.test_field_file + # update only string field; + update_string = 'manager updating' + Object.please.update( + id=data_object.id, + class_name=self.class_name, + test_field_a=update_string + ) + + data_object = Object.please.get(id=data_object.id) + self.assertEqual(data_object.test_field_a, update_string) + # shouldn't change; + self.assertEqual(data_object.test_field_file, file_url) + + # update also a file; + new_update_string = 'manager with file update' + file_content = 'manager file update' + new_file = StringIO() + Object.please.update( + id=data_object.id, + class_name=self.class_name, + test_field_a=new_update_string, + test_field_file=new_file + ) + + data_object = Object.please.get(id=data_object.id) + self.assertEqual(data_object.test_field_a, new_update_string) + # should change; + self.assertNotEqual(data_object.test_field_file, file_url) + file_content_s3 = requests.get(data_object.test_field_file).text + self.assertEqual(file_content_s3, file_content) + + def test_manager_create(self): + create_string = 'manager create' + with open(self.file_path, 'rb') as f: + data_object = Object.please.create( + test_field_a=create_string, + test_field_file=f + ) + + self.assertEqual(data_object.test_field_a, create_string) + self.assert_file_md5(data_object) + + @classmethod + def get_file_md5(cls, file_content): + return md5(file_content).hexdigest() + + def assert_file_md5(self, data_object): + file_content = requests.get(data_object.test_field_file).text + file_md5 = self.get_file_md5(file_content) + self.assertEqual(self.file_md5, file_md5) + + def _create_object_with_file(self): + with open('tests/test_files/python-logo.png', 'rb') as f: + object = Object.please.create( + test_field_a=self.initial_field_a, + test_field_file=f + ) + return object diff --git a/tests/test_files/python-logo.png b/tests/test_files/python-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..738f6ed41f499d1f57fd4db356dc1d64769bf725 GIT binary patch literal 11155 zcmdUVWn5IzyDmd_NQaal2t$J+ol*jVbf+L4(%q$k_$O4P6i{-=p&38~1f+zaLz)mfY@00XQM~#e_i5LqDi%dgZMIQ?b+Z22sAjAis zmi_Vf!5>^-B@F{Y@Cqe#NCdx$ywpv7v9O2_{=Fee<>J}kB;6}jlUK?0TYDc*cSnbpw(L({`Zxx7`ndYC zYn!l(3JZ%T_?Hi3VX(%->L@DUDIuIY)C`>CCbyk_-T<}6^)I*C_H@QqdIY1*i>j@X(n9|*#px% zxcYZq+^^6(T%6O_J8ke+2j`4c&-7?jhPr8u#C44H+_G1AD~D4&%N_=2YL#@cHE7OK zpyP7k(<5fmZeax#@G6*@+Q+A)qM{xhK7TKgZV~f=$6MSsIlVkN0Et% zT?Hs9z6@e$he33Oa=hAq-Vxh^)RKw0WgQ9I&5eHcPSa z2n^f&Ex<7Up(}Uv?o?(o_s;+zEAc6Yn)s0qzWkEcM?pE^Ycjuc%sa<=g3WEPM~DsL zn}#<-$ug0df`6)CExWk39F2J*f0a+R3HXsLNCZo>w(frpJPX)YT+hAxu2mj9E-srs zraMl*E0(s{ZkRHR1#PyJa5f2b1tA06p-rs&qqFs6jc?mAIMdd&0JN=s3MJZ%5=r(#P zs14&N0d&CYtM&Z9aW6V<`Ij+I!6=n~qpBjRrm0uy?4AV7#U~|%6MPumooWuE&J=uj zaN;Ypa-!9&9F!I57(sTB`<~WINrn{1`g&6L@L+$hYxAx-PBH=C6l;zcYD}nnodc#+Y2{I zi)?l+dgfhM8&mAb$=Qc~xX>~+%&)USwNpvbLsPiGFb*CRM-hh(E9dHv#6&8h=Te)R zJEsx_<_x?#?5&2uM)ub!Rb9H{$=q*;hKBrha3Y(U9{)#^d$W1Z3^_N)vcA49Ge9bJ z>cHLCl!9D(NQ!aZvqDBXe(YOJEuk~o_oj1J3Q0Y#XBl|JmKW9zgtxdd?NRvkI}-KG zFyoPXv7eKhTe*9SgL7y z8+TP=+-z+jMfypQ6!kRdKpGd($6~+M;!-~1I3+eSTEX~ouER#{-+#v(3;6higzmLQ zI!gYBUM9SLCj%e2xzS)lUYh(;m)UlgdScT|YU%0uWDb7s^7oT(CS>t-a>FMueeX+# zIZY6vEWcWwCSr(9SY4e3+B4^f-l(Yj`D7tTU}&gUg*3l3FhsX*3u`dcr1#mpkk2`s9_OmVhs%LJ~HlzRG={VIOP~j&< zF9x=8(crl#Cv!X~h815%PqH)-N3_BU8~ky!Bb0Rj-Y|57Hp4u}^57Wvt9aI^NZMba z0{bguPoSKIenRj2v780vg17%XV37-L92Ov^P&WF@klFfoVc=DfH;xli7K^p3TwrYe z2zF*jwmkxbi18>K06-^jV^O|XI&LGa!L`90WaP~M+7nK z3KxW(l2EzSwY9YZlZMV@SyQ6F%&;qL9dpK#BS<`Ti_C(T3jr8Z#FH1Q?;OFG;R8ka zhw2MEa3>*~4RCxuu~e~&Y()0JKp5&y;^0qeCX@^#G0CiNotu3EoNXwOd2tm)YxV3j z(lIczeo`Bpv;Zo(D#q5o1l~VNkzNR&oQO631~ek_-)t!yDLFxC8zLc5 zzA5u31+Ya2zcit^Nxv}OJ*_AGdSw$c9`klgjEsz>d8(Gk>ekk|>WH)mHd3*8=+W}S z&V}WA4{SOyBldbTGH^Hz=c-m1#Z;n3{BAGhi^pH~)sU*{r46iI92MyQx55{c@`-C} zT^TB)l4KD$G1mp|3m;X->n1xGC~LJiwfC;AIXZvL{I-aP>c{V1 zsz32=_SwbP=?G;_*$}kfQu?w>C;6mTtb{}AvcWhtZTPESx6$O$RUA^tuIQ$CpebjY zj>zP9@9pN|?fd%qEFCPQ^~T(-i{wF1-!(k z`-`a-g7lhU9)mW9|81MIRw-f*z@rpc2!>=bva$6}O=Wvpj*pMOEx7kIxzNVPNB>*7 zddORr$75!Ovs!Ido+LaeDJku}eKoJ2-j4~pJVndC`d+$yRy+9y=s>6(g{q#Cv+4fe zzBY)>aizk%$z#D)G;*g7J*y6D5;B(qi%Zkirt6P`<`RTYG*I4)_jyJR>5vg*t>+}> zh?)u8FgKZwhBh=NN?H!jt+}>#A$C-pth6+qseA|R`Vn7MPgq2R>LS5Do6Vm_po$&Z zwe!Ht#3Ztimw}l%MK=3eX|hifi`juN%D}MRbHASkxgYJ*^xCdkQn#--cQ6dGBXtd!;Q{7Z#=SG+NOv)u}7RIr5kUmrY_hhczpV+bhX^-HpX+upA9rm9`+ z_gv)u?CrH@e)9SU?+lr^1P8NSV0x2V{SiC`-S!fa}4 zs!R>|7`fxmok4%BoTca*I5a$L@8KbsX`^2@AuKCKq4fB{H_$u1?(f( z{O8ZAMfyac<>uziWG;H)`;*M^DBAUp!^N9joe@QNNv^s#>Q8#`fgNli2%_RmL7|3Y~F($YK0$_>JM_V5$!&47OT*GWU= zkHrNBd_>x$f>{H74x%|&C_6j5F7J*$+Siw-yC|(lS~BjfSIvGeJD>x1*_P_fDkL~8 zPMnO##i3BB&F#&?h6X;7Ns|TnC1xLU^;oB5ydvTrI`}RL2}yQd9(}MUP>wXmrV@p6 zRIux}g;XvLUK(O>s2AyXZGansg8`WN)m7{M@VAW8(!Lk9@2ulw2Eo3>FT1T*P6pN! zOiUmHU*KgMk+j0NMDq!-Pr^tmZD$Axz8BgvN^2Mk&bBFIoQF$favsVGG9TG0$1l9Z z#l^+o!3+HHHR8(m8mL!lr8mtK5fO_fnG#`< zde!&H#d7Fl97^-AhZf4@cn=0!u zrM=~{%x9PM7wAUHcOVbEbTcco@grS@u#i?i@+r^bETDAvnxZw*xj+av*`(jOx(fY< zA|G9PMuVsJmSyYl5Be0i-4@%-i=7gK7mIIG&j2h%Et|xDl;CBK>o8Be-1_7|6hZ^QY0%k>W2r15CN)sSO|r zG<_?7tDQhyp}Y0UEYRwiP#cAKAeV+xwaJgw)vqJO2b{@0g# z$t%Qi-#lSPKl8alO$V|;w88(?i9>~lwH%(=b6%R*8zm#dj5bqcpjpdP)%FF8a@-l3 zO;)5f$e|-#&STis{l);TbgpI0LuqN|i}Nq{FBTYTntRh|qu*y3f6|S`&a6JoGbVf~ z+Zf9b#UZ!ftcsnf97yC9I_ulpz|^Vjnb&qJ-g(LdNss8{dH;vxc;5VcL#)mnp0ZK6?0&wiA0N{x)J z>Q3Y+Q!}odH?gd-6`;gzrznt(#X8vGCU#V6ZQ7-UE%~-0&*xDM;*{n^L(a#iw6E}? zuF~a@fdZYXAoHCemwPi`sMdZh(WFXx%t58}WYmYh^?9L{DP(ER4Pk{Gf=To#2fsR< zMI6wZAhfwdNTNX~iN)IGC{E#!+i0F^kWQuV(0;2tJJZnCPOgoatFCZoMq2w5nl!MM zf%FE;VtG}YUn4!Xl2?pgg8PV5%7TZfMqHbkFJwvKWT5uz_{)L_{z7+FE(;1FGg4Do zsoGaT=nqToKCMcLrax(>X%ny^19B`|0ih#J4rQk=_AF<;$V}u?Cyzs9t*%Xb&ymuj z*0A}(q5?E`-yKR$mEdQe>7Kqxb{_T|i${K8QYyY5U(WhD{9I-dJCl2*!q~qkQFjDlMf{X*VdnFNCEjvo>D@MGW zpG$Ss=W&5;|JQ5tBzxWa4lWxCeI%r;tzvoMz8cg<_UPu) z8}8TW!D5>$c{~_m|MK`)KcNEYj?(*zm0Vq}JKHYxOt%%!NR*8z zK*{-0H#Q`HiCRfT>Gkt-c1T5SMJ~MGyNPcE?vWRXheaTNbW{HZz5uhhrq@H*>1N&= zUBI&XsqIa*OP1V1A1~WMLjxxI4Yho75E)k=7KLL(0jUrWmdJbL>MNCHKF_&7QPeur zncB)0f|;vrSh=#Un7B9genJ}|ie+K#<31kDJ#z`b8*i#MTc0`1Bn_G06LwYn98V?l(+Liy>2KV`-1>fAPS}9mjZr$ahJ2L@%t8nnEBKM zm$iW)j^4ZaR_82~Jx91mbWeH|+?7_5#Q*`=w<-55-1cJWdHzzE)3J5o6N=ORC?+R8IT=}^ZmrK(L9fB^a4pJ zcQev*xVhg}(uN^UWVbn=un)5Mj%D6$De2v|L_qK^+m1RIo&l6w!0y@q#~wn&W>e90%rI>trS>cphP^w%6^PSM@YwmOM$xBW1rW#EqezS>??OK=FYUQ-aQ$p-?# z%qZrLEdd5RBD37ygtMD?tc2Ud#KO+B3F>!|T%xNReyz`gaAx)4f5q4?Te1VsWcP+b zP*58SKO)-rjp7x>_+ixg9y}apE)eJ2En(oX>a7i}Kd=NN_Ewj)L|!IR)3)s(os8!} z`z!DwkDkMBww~M&|Ct$mg32(qz~f8Ao7zpO$XGfD+_(E$2)mJ*ziaC%%k~r^5a5OS z=i26%iM!b${DBI-q$W@9ev6=PJdIgD$Z2c;hMkT)v_(?;d)+IH;tD2+R=izd&(K7- z5Q$?k)C9vM{06qg;E@jJ3ZUMTEnh-0 z7+|Zsh(hDF!s6B1*hzFkOBBUKMgN)?v&qAoD@ii!`#As2B>F{~lGmm_qaF|1 z{eZWQuZv#pkYnIQ+jI=AA%eI}4$kf{zaQAjF-7j`T*F3z35IVdxT=KI(5NOga;s<; z%DZ|KRDc4sz~_GqP|>%M(uE4N9QCaSvig-OR?=`Q}IiswKD>u5B~)1baHyi zj#ZbCkc(qNQvNH zx}BztxAiVn*Kf4ct3NB(mGmtLR#~wl4^|9*vhL-)^?sR9AC|j3V)pZ%gINcKK7L)X zQ5lgg`)`M?>ltrBhM@kns|554xhdy{y%3!#uq|HS;oTvg1`at)BwoUi2aropUIixq z@VWsxn@h0wpr(F6qHk&&P3$t+3&YmWKJN9GBQ?8s!NIMezj^#Lu=eE<=h&wrRIr<^ zCDN}Tw_K@!QrVUF0gz1{sBrIMr>_NpaP;zz6n&ZBCzcW{yId`>><2bYvQl`^sz@%g zssV3B)S>jkm6SW{S`G+KY1~qw)Ulgfc4m1h!8KS*f4)Dbf`9$4|NSgeqhvhiM8_RW z{-?Ezl9I%v0frrj4`sj?#hAqtFIt@vvMYupLsP>QdDYp8@4%vQzgo@WVM((=Dn|Y8 zPPiC;+X0{@_JqKZA~oh21-BMohLT(!MxdnBg|vc7dKiF0cFsiHM9M~~dkYVy<4N1F zfj5NzGPkP5gSxt7MZ&}1^kJ*ZV;h<5Qj@vtv*1)v?LyQ`#-ldulga}Hm=2t;nb_Dc z0^`o&oD^msi_gimG}F0gh+l?*v{3OfjbldstomY;9_H3&8D9?_F@3m$_192W9tTV~ zD?{uyjpI^^ME$FC_-2Q!H#m)rCs63AU=Dn|1oarOd!_DpkZz8JFlPP(W#g2hF#KQ1 zNliHdPdYCrAMSKV008Op>%FnrH5S-N&{Gr04^-f2KHWc)3Hy>)7r}w7*ZIren8;&MRmC4z?$sRwM5WBBG{JH-E?^~&h!!l@TEEhBB4M+Rh@7b=h`%(<&BQ|=Y0^C&70UFZF79n%8TxV7h54L7t zl(|;9k)5bxhW~NjuufZXv-z$5_t>qiEt9S{P*6aRZlk3(3jJ)|+@5aQCG+G4a@NV~ zfOEwqC1D|opu8%A9xk{@RS#ANh~T1#;1`XN9o@@}s-L>@gtW;I!H zYFRpej0cZ|!A#7;!IX~p24FGPH%D15R-7v1;s8LnJeAZ;kF~4r>)%^_`MiT!_^A)Y zT#}l6puEn_YR~#x&@mZLEAZ=Zg}tPGMIm~6ZH3OBmA$(^NuGe&W>{xAm7cVpUZ^8$ z^t{tuIO=TU;X&@}>zn4t_~9u&0OK%w^X|sS6DYqwE)wAQR>~Ck4VpKc;Q!!aYKj45 z>^F!qB&q_FJ@f*AeGaDnEXm{IWX+YzT7>uN;J+j(8uB-GunubX2q5f5rUvJG75 z4yLsCW@;3_1TzVM>X1O+llKZ2-RgX*dUn=SvdNKn;yj&fk9}}~ZK3QkFa~^apppmo zAwgGuFP(W90R60U@6*sxvIGTKPde8>*e2}F&?WgP)65Wu7C!V&&tp@nso+@w$?!8Q za=EKq8^}PU@)XQFXLyxH!49MZ%F;PA^u~zh{X7VUCnVb|G?eA}^XFYCZS?k1XUscX z=zB()zwJSlO}~-V_PutpRnP$-?VVC5O{8Dz#daB0ol7IYqMmAN_s|7`hDZ6vmt{n5 zd7@j}uF=)FZYX7HtLLNKD}UOOVQA-TlOY0g&kPBbDP`49tT^gv3e;a06u)hp5f?!tnqxQo$>h$Y_Yd!!E>Ra;(`;-kG z5N$sx5?bCW$+HUJpY7%TtOFnq;?hz(fB%Q+K|$WP%mvF`X|&aII04IN%c;wA=lo|K z^71Kp?XcL43%{Ho?Ox3lpozlRMpKISOPA=@4`(rza&m-ohqg9auuBxTD$upnRW49e zt#527e^{;H)%-?17Xu(FN&g)N1H;`5O{JDsQe>)n`MjFQPQK*xXWtEod+ezGr`?7l zrWi4`dVD^KM*xcRg7TEi;f-p~#dZW{m#b#RhQULu{N<9xjVEHQ<1LTP?v1ZPuyx(O z)8~)5xu?4sQa-DR8$uYD`k7`I-4h(u%rzs5VfJf7k2YfcuW+4zK>eSOeb9lLX9~eP z)x9#-n9Gj|1y-o`48Sv({Ko|tb*^i7`dDlm6f`a3z?U4|uTjn6`A%cD2HHFJkg?ytyVyh~-vOPTAG9!ry7V7c+>fCh(1f+Hk)@?!8Z?fwQQ;3^ zo&hN4Qx$~yYyFWsTC%Rho%uJq%fHohvE|wmp-CfAIj5~+Yu~4tE}1~gEOcU}o9|DB zMzWJFQjUTkp!snmI zUqF*PkB(|KP&+-Gg}k8mqX|Z^wnwx6bAHS6y@pG7PpMT0_wR%+eEpDEh)B!ey4$8$ zc1yxMV4b{Qh ziUs%fkiuHl<1=%$o`pwx^xNoWPhBIytd#E(Ds>A$GW|aVhSa_Q2z=pv#>~p%UVxkH zsq^D{)Wdja^6w;S9 zHXR-wGMFi=plFqi#@uE*F1WttLWhG%Z!dliUBH93wtfc>t&FqL1&*uy{k2x(g|HK% zqf$-kj?<`Jp@`HPF846$O14)JP4Q!fEm6W=Pu?fRX$&V1+~P-ETfg$ypHN+zxZRwq zljwvEZ7PmQVrm^q1tx02N zwy`Fxkl~of5w)I6d0`Z7Htr2wdkR842O(8*TNeAp?ufnN(~}~0?2pSX9v;5~I`!T| z2P8w!yna-0a@zH27N5vtSKg2Y9v3ZO>l;G`tcJHw*T#o#=eDcm7v%e`pxYmdUw$*- z`({+u4j-&ecVqzy=fT^%y6uhcHErX`vt*Qu$snB>HZXbx9~xx)m+E*swOy99z)+P>B% zknhO+hM>B~ouAu`x!7M{5`t6(EKaEa!OfKTtqL$&dZZh$FszWhtCbfcW;gqYS^iOs zL(_!sWej4oq>(%`MyUWxBR$mko^>kdet`_gEWT4G(~*W44}fl`7F44OfXDI>J~u&X z7H}itbxz~8MKE-mgH2EAfoQ%4k1OF*XRXRflYBI@oe)=LW5b?1x5 znf6DO0iyqTW$B40Xo>V*?W1xKMfu*C)tmJU4nCc22j21BJ1Wqv1fLF~jD0n8C3JVF zl5G3TsutRb#ea*_leM+~A=TwCF>f01dBBdHrGaX2DjszP z9HjbqasZqp|1VBetD^Y-!J;Z7LB>o_EjRYYJ;Fl7lZoVI0ADB!?D?~8Vq8gf%p`uN zuSg=`1i*kM5?2XxQ`Y@$Z|_97L>|E)k=3>_j^7DqQKZ&@>taJ{it|f?rU{o1eq^=G zH@LJ$llW1~g|V`#WpY9$|4Y~o`X6g(`3`wZ*8>?Me7+LTf=#)qYtGiL_v5aoVRvVT zKDS?9{RWNlKc1wf*IW$ctx6fzOD?mgj6S$OHr&q9d_i#!7V{kiP+qLPVWbwIqi(}c zbCJKIwMSj9qHKg}YLlC7FrJ^^lv2P8TbNsJ-fc)tO#H^iP-C~sT#4J2=J~BgCP3m+Qyf&L&|26GI@^z zBpadt^~i(NRPT7J!e=|EMvk7y!o%^pVYgS(y@hHws%GDJwgLYuyW-%Dg~KN>7F2Om z)SnYsxt>mAlH$wQ3kGkRXEUNbAnM{4)9`E7>r=?iD3D1f!mp-qRhXd6ef*FTgRb6j zyu!{yg>&&cQt=nfBi=dJ7NjPCjR#yH?=;Rk_t^)p9^(my1eN4Fu)2DfhkVY7 z-z(n`n{N$z9=Ml28w!OZV>c{_2K#-acx0lPR%ofH&^SEpYD3`?9MRh0aJl_#m`9_~ zJiCvCE!>zPS+Ig~)kXPT5liE196*-?wfxho=-18&pScanKhd$@t-^7yGW8E;dG1tN z3`jhhJiT!-QN{-Ep}gP-jQdxWbq19ylM6a+l{a=eUej z`3{TZ`gC4IM%eEE_=(KQlUUwYMuf3~aQhhia&~#*aM>sA#yHJ8zR>Z|&Hy%GM4*wx zWT3PB%T<0(Ieif>S)R17_^UCFtLorKfdZhQ-T6lhA~eaZ!h1k{rz5v`lHVih*G3>v z5yjp#2~w5r_d!LrvD$|ux_8$YzXD5D2k4$H%r@qrbbWq-SvWCY5l| Date: Tue, 27 Sep 2016 11:58:00 +0200 Subject: [PATCH 532/558] [HOT-FIX] files hot fix: create tests; --- tests/integration_test_data_objects.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index 9254645..ddf054f 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -1,6 +1,12 @@ # -*- coding: utf-8 -*- from hashlib import md5 -from StringIO import StringIO + +try: + # python2 + from StringIO import StringIO +except ImportError: + # python3 + from io import StringIO import requests from syncano.models import Object @@ -101,6 +107,7 @@ def test_manager_create(self): create_string = 'manager create' with open(self.file_path, 'rb') as f: data_object = Object.please.create( + class_name=self.class_name, test_field_a=create_string, test_field_file=f ) @@ -120,7 +127,8 @@ def assert_file_md5(self, data_object): def _create_object_with_file(self): with open('tests/test_files/python-logo.png', 'rb') as f: object = Object.please.create( + class_name=self.class_name, test_field_a=self.initial_field_a, - test_field_file=f + test_field_file=f, ) return object From 4d0188ea1ef13375efdf65dfeaa2e97ae6a0a628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 12:00:55 +0200 Subject: [PATCH 533/558] [HOT-FIX] files hot fix: create tests; --- tests/integration_test_data_objects.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index ddf054f..7428563 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- from hashlib import md5 +import requests +from syncano.models import Object +from tests.integration_test import InstanceMixin, IntegrationTest + try: # python2 from StringIO import StringIO @@ -8,9 +12,6 @@ # python3 from io import StringIO -import requests -from syncano.models import Object -from tests.integration_test import InstanceMixin, IntegrationTest class DataObjectFileTest(InstanceMixin, IntegrationTest): From f23984c4a1364bd88802c2bd715794138a760936 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 12:01:27 +0200 Subject: [PATCH 534/558] [HOT-FIX] files hot fix: create tests; --- tests/integration_test_data_objects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index 7428563..862647c 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -13,7 +13,6 @@ from io import StringIO - class DataObjectFileTest(InstanceMixin, IntegrationTest): @classmethod From d6253837e1202a7f48807f6391f6e7501eb15fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 12:13:08 +0200 Subject: [PATCH 535/558] [HOT-FIX] correct tests; --- tests/integration_test_data_objects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index 862647c..3ddd485 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -80,7 +80,7 @@ def test_manager_update(self): test_field_a=update_string ) - data_object = Object.please.get(id=data_object.id) + data_object = Object.please.get(id=data_object.id, class_name=self.class_name) self.assertEqual(data_object.test_field_a, update_string) # shouldn't change; self.assertEqual(data_object.test_field_file, file_url) @@ -96,7 +96,7 @@ def test_manager_update(self): test_field_file=new_file ) - data_object = Object.please.get(id=data_object.id) + data_object = Object.please.get(id=data_object.id, class_name=self.class_name) self.assertEqual(data_object.test_field_a, new_update_string) # should change; self.assertNotEqual(data_object.test_field_file, file_url) @@ -117,7 +117,7 @@ def test_manager_create(self): @classmethod def get_file_md5(cls, file_content): - return md5(file_content).hexdigest() + return md5(file_content.encode('utf-8')).hexdigest() def assert_file_md5(self, data_object): file_content = requests.get(data_object.test_field_file).text From 98b7a1de686d9575d82251010859d5d90ee2317f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 12:21:36 +0200 Subject: [PATCH 536/558] [HOT-FIX] correct tests; --- tests/integration_test_data_objects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index 3ddd485..a4b113c 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import six from hashlib import md5 import requests @@ -117,7 +118,9 @@ def test_manager_create(self): @classmethod def get_file_md5(cls, file_content): - return md5(file_content.encode('utf-8')).hexdigest() + if isinstance(file_content, six.string_types): + file_content = file_content.encode('utf-8') + return md5(file_content).hexdigest() def assert_file_md5(self, data_object): file_content = requests.get(data_object.test_field_file).text From 6115f2528b401fd3a221bc80d06416629be6e417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 12:27:19 +0200 Subject: [PATCH 537/558] [HOT-FIX] correct isort; --- tests/integration_test_data_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index a4b113c..33465b4 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -import six from hashlib import md5 import requests +import six from syncano.models import Object from tests.integration_test import InstanceMixin, IntegrationTest From 20d6d79710c3905d5e4f353eedc5f552dc60b12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 12:36:01 +0200 Subject: [PATCH 538/558] [HOT-FIX] correct isort; --- tests/integration_test_data_objects.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index 33465b4..b308ae6 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -89,7 +89,7 @@ def test_manager_update(self): # update also a file; new_update_string = 'manager with file update' file_content = 'manager file update' - new_file = StringIO() + new_file = StringIO(file_content) Object.please.update( id=data_object.id, class_name=self.class_name, @@ -117,9 +117,11 @@ def test_manager_create(self): self.assert_file_md5(data_object) @classmethod - def get_file_md5(cls, file_content): - if isinstance(file_content, six.string_types): - file_content = file_content.encode('utf-8') + def get_file_md5(cls, file_object): + if isinstance(file_object, six.string_types): + file_content = file_object.encode('utf-8') + else: + file_content = file_object.read() return md5(file_content).hexdigest() def assert_file_md5(self, data_object): From 473eb460d3745f96268f7b7145f1ef333dfaa9e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 12:42:33 +0200 Subject: [PATCH 539/558] [HOT-FIX] correct isort; --- tests/integration_test_data_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index b308ae6..bdab5dc 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -32,7 +32,7 @@ def setUpClass(cls): schema=cls.schema ) with open(cls.file_path, 'rb') as f: - cls.file_md5 = cls.get_file_md5(f.read()) + cls.file_md5 = cls.get_file_md5(f) def test_creating_file_object(self): data_object = self._create_object_with_file() From 5d8271d66f85e7ee5c99c13f7670ff54056ef667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 13:06:50 +0200 Subject: [PATCH 540/558] [HOT-FIX] correct isort; --- tests/integration_test_data_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index bdab5dc..ecf3123 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -31,7 +31,7 @@ def setUpClass(cls): name=cls.class_name, schema=cls.schema ) - with open(cls.file_path, 'rb') as f: + with open(cls.file_path, 'rt') as f: cls.file_md5 = cls.get_file_md5(f) def test_creating_file_object(self): @@ -125,7 +125,7 @@ def get_file_md5(cls, file_object): return md5(file_content).hexdigest() def assert_file_md5(self, data_object): - file_content = requests.get(data_object.test_field_file).text + file_content = requests.get(data_object.test_field_file).content file_md5 = self.get_file_md5(file_content) self.assertEqual(self.file_md5, file_md5) From cfca799768807e75f651b370d37018dce5c6883f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 13:18:02 +0200 Subject: [PATCH 541/558] [HOT-FIX] correct get_md5 method; --- tests/integration_test_data_objects.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index ecf3123..c59d582 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -118,9 +118,7 @@ def test_manager_create(self): @classmethod def get_file_md5(cls, file_object): - if isinstance(file_object, six.string_types): - file_content = file_object.encode('utf-8') - else: + if not isinstance(file_object, six.string_types): file_content = file_object.read() return md5(file_content).hexdigest() From e4dd76502d06a5b99c4207c72f390536cae589fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 13:18:48 +0200 Subject: [PATCH 542/558] [HOT-FIX] correct content get; --- tests/integration_test_data_objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index c59d582..05b106b 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -67,7 +67,7 @@ def test_updating_file_field(self): self.assertNotEqual(data_object.test_field_file, file_url) # check file content; - file_content_s3 = requests.get(data_object.test_field_file).text + file_content_s3 = requests.get(data_object.test_field_file).content self.assertEqual(file_content_s3, file_content) def test_manager_update(self): @@ -101,7 +101,7 @@ def test_manager_update(self): self.assertEqual(data_object.test_field_a, new_update_string) # should change; self.assertNotEqual(data_object.test_field_file, file_url) - file_content_s3 = requests.get(data_object.test_field_file).text + file_content_s3 = requests.get(data_object.test_field_file).content self.assertEqual(file_content_s3, file_content) def test_manager_create(self): From 95ebb90a99a79bbc97c37420347b051df4e5d854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 13:29:00 +0200 Subject: [PATCH 543/558] [HOT FIX] correct open mode in class setup; --- tests/integration_test_data_objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index 05b106b..412920b 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -31,7 +31,7 @@ def setUpClass(cls): name=cls.class_name, schema=cls.schema ) - with open(cls.file_path, 'rt') as f: + with open(cls.file_path, 'rb') as f: cls.file_md5 = cls.get_file_md5(f) def test_creating_file_object(self): From 5c69f72e02424dbd5f5fb7152f414acf37cf7397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 14:00:27 +0200 Subject: [PATCH 544/558] [HOT-FIX] correct method which get files from s3 - if byte type in py3 - decode to utf-8; --- tests/integration_test_data_objects.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index 412920b..a03cd22 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -57,7 +57,7 @@ def test_updating_file_field(self): file_url = data_object.test_field_file update_string = 'updating also field a' - file_content = 'some example text file;' + file_content = 'some example text file' new_file = StringIO(file_content) data_object.test_field_file = new_file data_object.test_field_a = update_string @@ -67,7 +67,7 @@ def test_updating_file_field(self): self.assertNotEqual(data_object.test_field_file, file_url) # check file content; - file_content_s3 = requests.get(data_object.test_field_file).content + file_content_s3 = self.get_s3_file(data_object.test_field_file) self.assertEqual(file_content_s3, file_content) def test_manager_update(self): @@ -101,7 +101,9 @@ def test_manager_update(self): self.assertEqual(data_object.test_field_a, new_update_string) # should change; self.assertNotEqual(data_object.test_field_file, file_url) - file_content_s3 = requests.get(data_object.test_field_file).content + + # check file content; + file_content_s3 = self.get_s3_file(data_object.test_field_file) self.assertEqual(file_content_s3, file_content) def test_manager_create(self): @@ -117,9 +119,9 @@ def test_manager_create(self): self.assert_file_md5(data_object) @classmethod - def get_file_md5(cls, file_object): - if not isinstance(file_object, six.string_types): - file_content = file_object.read() + def get_file_md5(cls, file_content): + if not isinstance(file_content, (six.string_types, six.binary_type)): + file_content = file_content.read() return md5(file_content).hexdigest() def assert_file_md5(self, data_object): @@ -127,6 +129,13 @@ def assert_file_md5(self, data_object): file_md5 = self.get_file_md5(file_content) self.assertEqual(self.file_md5, file_md5) + @classmethod + def get_s3_file(cls, url): + file_content_s3 = requests.get(url).content + if hasattr(file_content_s3, 'decode'): + file_content_s3 = file_content_s3.decode('utf-8') + return file_content_s3 + def _create_object_with_file(self): with open('tests/test_files/python-logo.png', 'rb') as f: object = Object.please.create( From f564a04f132fab4b743e5212e13ba460091d27d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 14:49:21 +0200 Subject: [PATCH 545/558] [HOT-FIX] use cStringIO instead of StringIO; --- tests/integration_test_data_objects.py | 2 +- tests/integration_tests_hosting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index a03cd22..0c6b4e8 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -8,7 +8,7 @@ try: # python2 - from StringIO import StringIO + from cStringIO import StringIO except ImportError: # python3 from io import StringIO diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 1be56a5..bdb7c5d 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -5,7 +5,7 @@ try: # python2 - from StringIO import StringIO + from cStringIO import StringIO except ImportError: # python3 from io import StringIO From fab97e19883d743bb63cece6d2a0487b7f738ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 15:06:29 +0200 Subject: [PATCH 546/558] [HOT-FIX] back to StringIO; --- tests/integration_test_data_objects.py | 6 +++--- tests/integration_tests_hosting.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py index 0c6b4e8..cc7ca06 100644 --- a/tests/integration_test_data_objects.py +++ b/tests/integration_test_data_objects.py @@ -8,7 +8,7 @@ try: # python2 - from cStringIO import StringIO + from StringIO import StringIO except ImportError: # python3 from io import StringIO @@ -138,9 +138,9 @@ def get_s3_file(cls, url): def _create_object_with_file(self): with open('tests/test_files/python-logo.png', 'rb') as f: - object = Object.please.create( + data_object = Object.please.create( class_name=self.class_name, test_field_a=self.initial_field_a, test_field_file=f, ) - return object + return data_object diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index bdb7c5d..1be56a5 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -5,7 +5,7 @@ try: # python2 - from cStringIO import StringIO + from StringIO import StringIO except ImportError: # python3 from io import StringIO From 70313a39615fbb011ac8d4332fae6949c967c19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Tue, 27 Sep 2016 15:14:15 +0200 Subject: [PATCH 547/558] [RELEASE v5.4.4] bump the version; --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 31d3523..77b9198 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.4.3' +__version__ = '5.4.4' __author__ = "Daniel Kopka, Michal Kobus and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From f696613a07da579a56eb430ed2ccbae2870da079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 28 Oct 2016 15:26:52 +0200 Subject: [PATCH 548/558] [LIB-965] update hosting after CORE changes; --- syncano/models/hosting.py | 4 +++- tests/integration_tests_hosting.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index ab80819..0a31ed8 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -9,7 +9,9 @@ class Hosting(Model): OO wrapper around hosting. """ - label = fields.StringField(max_length=64, primary_key=True) + name = fields.StringField(max_length=253) + is_default = fields.BooleanField(read_only=True) + is_active = fields.BooleanField(default=True) description = fields.StringField(read_only=False, required=False) domains = fields.ListField(default=[]) diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 1be56a5..72005cb 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -15,7 +15,7 @@ class HostingIntegrationTests(InstanceMixin, IntegrationTest): def setUp(self): self.hosting = self.instance.hostings.create( - label='test12', + name='test12', description='desc', domains=['test.test{}.io'.format(uuid.uuid4().hex[:5])] ) @@ -30,7 +30,7 @@ def test_create_file(self): def test_set_default(self): hosting = self.hosting.set_default() - self.assertIn('default', hosting.domains) + self.assertTrue('default', hosting.is_default) def test_update_file(self): a_hosting_file = StringIO() From 74c978c4f7f67d4a3410ee7be2333f9e820ca6a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 28 Oct 2016 15:36:48 +0200 Subject: [PATCH 549/558] [LIB-965] correct imports; --- syncano/models/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/base.py b/syncano/models/base.py index 015217b..2f4eeeb 100644 --- a/syncano/models/base.py +++ b/syncano/models/base.py @@ -11,7 +11,7 @@ from .push_notification import * # NOQA from .geo import * # NOQA from .backups import * # NOQA -from .hosting import * # NOQA +from .hosting import Hosting, HostingFile # NOQA from .data_views import DataEndpoint as EndpointData # NOQA from .custom_sockets import * # NOQA from .custom_sockets_utils import Endpoint, ScriptCall, ScriptDependency, ClassDependency # NOQA From 785da5c0abefca1a98db98aa9759e6f74a88ff75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 28 Oct 2016 15:44:42 +0200 Subject: [PATCH 550/558] [LIB-965] correct imports; --- syncano/models/hosting.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index 0a31ed8..e74ee22 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- from . import fields -from .base import Instance, Model, logger +from .instances import Instance +from .archetypes import Model +from syncano import logger class Hosting(Model): From fa2bbc3a38de1fec1622177d5f863a6062a6df61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 28 Oct 2016 15:47:28 +0200 Subject: [PATCH 551/558] [LIB-965] isort issue corrct; --- syncano/models/hosting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index e74ee22..548a1d4 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- +from syncano import logger + from . import fields -from .instances import Instance from .archetypes import Model -from syncano import logger +from .instances import Instance class Hosting(Model): From fea8e70a1f3438d11ee1c42bdaa7598242045c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 28 Oct 2016 15:59:52 +0200 Subject: [PATCH 552/558] [LIB-965] imports corrects; --- syncano/models/hosting.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py index 548a1d4..48b95e4 100644 --- a/syncano/models/hosting.py +++ b/syncano/models/hosting.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- -from syncano import logger - from . import fields -from .archetypes import Model +from .base import Model from .instances import Instance @@ -18,7 +16,6 @@ class Hosting(Model): description = fields.StringField(read_only=False, required=False) domains = fields.ListField(default=[]) - id = fields.IntegerField(read_only=True) links = fields.LinksField() created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) @@ -50,7 +47,6 @@ def upload_file(self, path, file): response = connection.session.post('{}{}'.format(connection.host, files_path), headers=headers, data=data, files=[('file', file)]) if response.status_code != 201: - logger.error(response.text) return return HostingFile(**response.json()) @@ -79,7 +75,6 @@ def update_file(self, path, file): response = connection.session.patch('{}{}'.format(connection.host, hosting_file.links.self), headers=headers, files=[('file', file)]) if response.status_code != 200: - logger.error(response.text) return return HostingFile(**response.json()) From 12198dbaed19c921c61787b3383eb9114f0ba610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 28 Oct 2016 16:14:23 +0200 Subject: [PATCH 553/558] [LIB-965] correct hosting tests; --- tests/integration_tests_hosting.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 72005cb..41e2eb9 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -13,35 +13,38 @@ class HostingIntegrationTests(InstanceMixin, IntegrationTest): - def setUp(self): - self.hosting = self.instance.hostings.create( - name='test12', - description='desc', - domains=['test.test{}.io'.format(uuid.uuid4().hex[:5])] - ) - def test_create_file(self): + hosting = self._create_hosting('created_xyz') a_hosting_file = StringIO() a_hosting_file.write('h1 {color: #541231;}') a_hosting_file.seek(0) - hosting_file = self.hosting.upload_file(path='styles/main.css', file=a_hosting_file) + hosting_file = hosting.upload_file(path='styles/main.css', file=a_hosting_file) self.assertEqual(hosting_file.path, 'styles/main.css') def test_set_default(self): - hosting = self.hosting.set_default() + hosting = self._create_hosting('default_xyz') + hosting = hosting.set_default() self.assertTrue('default', hosting.is_default) def test_update_file(self): + hosting = self._create_hosting('update_xyz') a_hosting_file = StringIO() a_hosting_file.write('h1 {color: #541231;}') a_hosting_file.seek(0) - self.hosting.upload_file(path='styles/main.css', file=a_hosting_file) + hosting.upload_file(path='styles/main.css', file=a_hosting_file) a_hosting_file = StringIO() a_hosting_file.write('h2 {color: #541231;}') a_hosting_file.seek(0) - hosting_file = self.hosting.update_file(path='styles/main.css', file=a_hosting_file) + hosting_file = hosting.update_file(path='styles/main.css', file=a_hosting_file) self.assertEqual(hosting_file.path, 'styles/main.css') + + def _create_hosting(self, name): + return self.instance.hostings.create( + name=name, + description='desc', + domains=['test.test{}.io'.format(uuid.uuid4().hex[:5])] + ) From 7ba0ffa2b6dad255131184a9d35efc6e95efab00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Opa=C5=82czy=C5=84ski?= Date: Fri, 28 Oct 2016 16:18:43 +0200 Subject: [PATCH 554/558] [LIB-965] correct hosting tests; --- tests/integration_tests_hosting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py index 41e2eb9..b60f942 100644 --- a/tests/integration_tests_hosting.py +++ b/tests/integration_tests_hosting.py @@ -14,7 +14,7 @@ class HostingIntegrationTests(InstanceMixin, IntegrationTest): def test_create_file(self): - hosting = self._create_hosting('created_xyz') + hosting = self._create_hosting('created-xyz') a_hosting_file = StringIO() a_hosting_file.write('h1 {color: #541231;}') a_hosting_file.seek(0) @@ -23,12 +23,12 @@ def test_create_file(self): self.assertEqual(hosting_file.path, 'styles/main.css') def test_set_default(self): - hosting = self._create_hosting('default_xyz') + hosting = self._create_hosting('default-xyz') hosting = hosting.set_default() self.assertTrue('default', hosting.is_default) def test_update_file(self): - hosting = self._create_hosting('update_xyz') + hosting = self._create_hosting('update-xyz') a_hosting_file = StringIO() a_hosting_file.write('h1 {color: #541231;}') a_hosting_file.seek(0) From 25cb5441fe86052ca750253a0523dfbe4abdfc02 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Tue, 15 Nov 2016 11:32:21 +0100 Subject: [PATCH 555/558] [release-v5.4.5] Version bump --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 77b9198..4b36939 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.4.4' +__version__ = '5.4.5' __author__ = "Daniel Kopka, Michal Kobus and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From 3247126f42a3b474bf4d99ed9e739feaf73812c1 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Thu, 17 Nov 2016 15:30:02 +0100 Subject: [PATCH 556/558] Update manager.py --- syncano/models/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/models/manager.py b/syncano/models/manager.py index c6ab313..61e7ae2 100644 --- a/syncano/models/manager.py +++ b/syncano/models/manager.py @@ -882,7 +882,7 @@ class for :class:`~syncano.models.base.Object` model. LOOKUP_SEPARATOR = '__' ALLOWED_LOOKUPS = [ 'gt', 'gte', 'lt', 'lte', - 'eq', 'neq', 'exists', 'in', + 'eq', 'neq', 'exists', 'in', 'nin', 'near', 'is', 'contains', 'startswith', 'endswith', 'contains', 'istartswith', From 39d1257e8e61acdf60280da7ff42d2d16f45baac Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Thu, 17 Nov 2016 15:33:55 +0100 Subject: [PATCH 557/558] [added_nin] version bump --- syncano/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 4b36939..760e46c 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -2,7 +2,7 @@ import os __title__ = 'Syncano Python' -__version__ = '5.4.5' +__version__ = '5.4.6' __author__ = "Daniel Kopka, Michal Kobus and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", From 5e2de8594598517f13000b1aab5b421f52bb0797 Mon Sep 17 00:00:00 2001 From: Robert Kopaczewski Date: Thu, 17 Nov 2016 15:39:30 +0100 Subject: [PATCH 558/558] [added_nin] Updated authors --- syncano/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/syncano/__init__.py b/syncano/__init__.py index 760e46c..a50f4be 100644 --- a/syncano/__init__.py +++ b/syncano/__init__.py @@ -6,8 +6,9 @@ __author__ = "Daniel Kopka, Michal Kobus and Sebastian Opalczynski" __credits__ = ["Daniel Kopka", "Michal Kobus", - "Sebastian Opalczynski"] -__copyright__ = 'Copyright 2015 Syncano' + "Sebastian Opalczynski", + "Robert Kopaczewski"] +__copyright__ = 'Copyright 2016 Syncano' __license__ = 'MIT' env_loglevel = os.getenv('SYNCANO_LOGLEVEL', 'INFO')