diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 92cbfa9..0000000 --- a/.coveragerc +++ /dev/null @@ -1,10 +0,0 @@ -[run] -branch = False -source = syncano -omit = */tests/* - -[report] -ignore_errors = True - -[html] -directory = coverage diff --git a/.gitignore b/.gitignore index cc1d3f5..bdc9eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,15 @@ -*.egg-info +docs *.pyc -*.pyo +.idea *.sublime-project *.sublime-workspace -.coverage -.DS_STORE -.idea -build +run_it.py +venv coverage +.coverage +*.egg-info +=* +tests +.DS_Store dist -junit -reports -run_it.py -syncano.egg-info -.tox/* -test -.python-version +.tox diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 4879d18..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[settings] -line_length=120 -multi_line_output=3 -default_section=THIRDPARTY -skip=base.py,.tox,conf.py diff --git a/docs/source/_static/.gitkeep b/.nojekyll similarity index 100% rename from docs/source/_static/.gitkeep rename to .nojekyll diff --git a/.pypirc.template b/.pypirc.template deleted file mode 100644 index 527026a..0000000 --- a/.pypirc.template +++ /dev/null @@ -1,7 +0,0 @@ -[distutils] # this tells distutils what package indexes you can push to -index-servers = pypi - -[pypi] -repository: https://pypi.python.org/pypi -username: -password: diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..bd1f2e3 --- /dev/null +++ b/.python-version @@ -0,0 +1,2 @@ +3.4.3 +2.7.6 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9561fb1..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include README.rst diff --git a/tests/__init__.py b/README.md similarity index 100% rename from tests/__init__.py rename to README.md diff --git a/README.rst b/README.rst deleted file mode 100644 index d81fd05..0000000 --- a/README.rst +++ /dev/null @@ -1,26 +0,0 @@ -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 ------------------------ - -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 `_ 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 `_. diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 0000000..9d21440 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,217 @@ + + + + + + + + + + Overview: module code — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano.html b/_modules/syncano.html new file mode 100644 index 0000000..e7d7265 --- /dev/null +++ b/_modules/syncano.html @@ -0,0 +1,287 @@ + + + + + + + + + + syncano — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano

+import logging
+import os
+
+__title__ = 'Syncano Python'
+__version__ = '5.4.6'
+__author__ = "Daniel Kopka, Michal Kobus and Sebastian Opalczynski"
+__credits__ = ["Daniel Kopka",
+               "Michal Kobus",
+               "Sebastian Opalczynski",
+               "Robert Kopaczewski"]
+__copyright__ = 'Copyright 2016 Syncano'
+__license__ = 'MIT'
+
+env_loglevel = os.getenv('SYNCANO_LOGLEVEL', 'INFO')
+loglevel = getattr(logging, env_loglevel.upper(), None)
+
+if not isinstance(loglevel, int):
+    raise ValueError('Invalid log level: {0}.'.format(loglevel))
+
+console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+console_handler = logging.StreamHandler()
+console_handler.setFormatter(console_formatter)
+
+logger = logging.getLogger('syncano')
+logger.setLevel(loglevel)
+logger.addHandler(console_handler)
+
+# Few global env variables
+VERSION = __version__
+DEBUG = env_loglevel.lower() == 'debug'
+API_ROOT = os.getenv('SYNCANO_APIROOT', 'https://api.syncano.io/')
+EMAIL = os.getenv('SYNCANO_EMAIL')
+PASSWORD = os.getenv('SYNCANO_PASSWORD')
+APIKEY = os.getenv('SYNCANO_APIKEY')
+INSTANCE = os.getenv('SYNCANO_INSTANCE')
+PUSH_ENV = os.getenv('SYNCANO_PUSH_ENV', 'production')
+
+
+
[docs]def connect(*args, **kwargs): + """ + Connects to Syncano API. + + :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: Instance user name + + :type user_key: string + :param user_key: Instance user key + + :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.registry.Registry` + :return: A models registry + + Usage:: + + # Admin login + 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='') + # OR + connection = syncano.connect(user_key='', api_key='', instance_name='') + """ + from syncano.connection import DefaultConnection + from syncano.models import registry + + registry.set_default_connection(DefaultConnection()) + registry.connection.open(*args, **kwargs) + instance = kwargs.get('instance_name', INSTANCE) + + if instance is not None: + registry.set_used_instance(instance) + return registry
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/connection.html b/_modules/syncano/connection.html new file mode 100644 index 0000000..7cdc432 --- /dev/null +++ b/_modules/syncano/connection.html @@ -0,0 +1,672 @@ + + + + + + + + + + syncano.connection — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.connection

+import json
+import time
+from copy import deepcopy
+
+import requests
+import six
+import syncano
+from syncano.exceptions import RevisionMismatchException, SyncanoRequestError, SyncanoValueError
+
+if six.PY3:
+    from urllib.parse import urljoin
+else:
+    from urlparse import urljoin
+
+
+__all__ = ['Connection', 'ConnectionMixin']
+
+
+def is_success(code):
+    """Checks if response code is successful."""
+    return 200 <= code <= 299
+
+
+def is_client_error(code):
+    """Checks if response code has client error."""
+    return 400 <= code <= 499
+
+
+def is_server_error(code):
+    """Checks if response code has server error."""
+    return 500 <= code <= 599
+
+
+class DefaultConnection(object):
+    """Singleton class which holds default connection."""
+
+    def __init__(self):
+        self._connection = None
+
+    def __call__(self):
+        if not self._connection:
+            raise SyncanoValueError('Please open new connection.')
+        return self._connection
+
+    def open(self, *args, **kwargs):
+        connection = Connection(*args, **kwargs)
+        if not self._connection:
+            self._connection = connection
+        return connection
+
+
+
[docs]class Connection(object): + """Base connection class. + + :ivar host: Syncano API host + :ivar email: Your Syncano email address + :ivar password: Your Syncano password + :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 + """ + + CONTENT_TYPE = 'application/json' + + AUTH_SUFFIX = 'v1.1/account/auth' + ACCOUNT_SUFFIX = 'v1.1/account/' + SOCIAL_AUTH_SUFFIX = AUTH_SUFFIX + '/{social_backend}/' + + 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'} + 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 + 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) + + self._init_login_params(kwargs) + + if self.is_user: + 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() + + def _init_login_params(self, login_kwargs): + 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) + + def _are_params_ok(self, params): + return all(getattr(self, p) for p in params) + + @property +
[docs] def is_user(self): + 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 +
[docs] def is_social(self): + return self._are_params_ok(self.SOCIAL_LOGIN_PARAMS) +
+ @property +
[docs] 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.ALT_LOGIN_PARAMS) +
+ @property +
[docs] def auth_key(self): + if self.is_user: + return self.user_key + return self.api_key +
+
[docs] def build_params(self, params): + """ + :type params: dict + :param params: Params which will be passed to request + + :rtype: dict + :return: Request params + """ + params = deepcopy(params) + 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.is_user: + 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'] = 'token {}'.format(self.api_key) + + # We don't need to check SSL cert in DEBUG mode + if syncano.DEBUG or not self.verify_ssl: + params['verify'] = False + + return params +
+
[docs] def build_url(self, path): + """Ensures proper format for provided path. + + :type path: string + :param path: Request path + + :rtype: string + :return: Request URL + """ + if not isinstance(path, six.string_types): + raise SyncanoValueError('"path" should be a string.') + + query = None + + if path.startswith(self.host): + return path + + if '?' in path: + path, query = path.split('?', 1) + + if not path.endswith('/'): + path += '/' + + if path.startswith('/'): + path = path[1:] + + if query: + path = '{0}?{1}'.format(path, query) + + return urljoin(self.host, path) +
+
[docs] def request(self, method_name, path, **kwargs): + """Simple wrapper around :func:`~syncano.connection.Connection.make_request` which + will ensure that request is authenticated. + + :type method_name: string + :param method_name: HTTP request method e.g: GET + + :type path: string + :param path: Request path or full URL + + :rtype: dict + :return: JSON response + """ + is_auth = self.is_authenticated() + if not is_auth: + self.authenticate() + return self.make_request(method_name, path, **kwargs) +
+
[docs] def make_request(self, method_name, path, **kwargs): + """ + :type method_name: string + :param method_name: HTTP request method e.g: GET + + :type path: string + :param path: Request path or full URL + + :rtype: dict + :return: JSON response + + :raises SyncanoValueError: if invalid request method was chosen + :raises SyncanoRequestError: if something went wrong during the request + """ + data = kwargs.get('data', {}) + files = data.pop('files', None) + + self._check_batch_files(data) + + if files is None: + files = {k: v for k, v in six.iteritems(data) if hasattr(v, 'read')} + if data: + 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) + + # 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( + debug_params, + sort_keys=True, + indent=2, + separators=(',', ': ') + ) + self.logger.debug('API Root: %s', self.host) + self.logger.debug('Request: %s %s\n%s', method_name, path, formatted_params) + + if method is None: + raise SyncanoValueError('Invalid request method: {0}.'.format(method_name)) + + # 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) + + 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: + # remove 'data' and 'content-type' to avoid "ValueError: Data must not be a string." + params.pop('data') + params['headers'].pop('content-type') + params['files'] = self._process_apns_cert_files(files) + + if response.status_code == 201: + 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 +
+
[docs] def get_response_content(self, url, response): + try: + content = response.json() + except ValueError: + content = response.text + + if is_server_error(response.status_code): + raise SyncanoRequestError(response.status_code, 'Server error.') + + # Validation error + if is_client_error(response.status_code): + if response.status_code == 400 and 'expected_revision' in content: + raise RevisionMismatchException(response.status_code, content) + raise SyncanoRequestError(response.status_code, content) + + # Other errors + if not is_success(response.status_code): + self.logger.debug('Request Error: %s', url) + self.logger.debug('Status code: %d', response.status_code) + self.logger.debug('Response: %s', content) + raise SyncanoRequestError(response.status_code, content) + + return content +
+
[docs] def is_authenticated(self): + """Checks if current session is authenticated. + + :rtype: boolean + :return: Session authentication state + """ + if self.is_user: + return self.user_key is not None + return self.api_key is not None +
+
[docs] def authenticate(self, **kwargs): + """ + :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 ``Account Key`` + """ + is_auth = self.is_authenticated() + + if is_auth: + msg = 'Connection already authenticated: {}' + else: + msg = 'Authentication successful: {}' + self.logger.debug('Authenticating') + self.auth_method(**kwargs) + key = self.auth_key + self.logger.debug(msg.format(key)) + return key +
+
[docs] def validate_params(self, kwargs, params): + for k in params: + kwargs[k] = kwargs.get(k, getattr(self, k)) + + if kwargs[k] is None: + raise SyncanoValueError('"{}" is required.'.format(k)) + return kwargs +
+
[docs] def authenticate_admin(self, **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) + request_args['access_token'] = request_args.pop('token') # core expects a access_token field; + 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 +
+
[docs] def authenticate_user(self, **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') + } + response = self.make_request('POST', self.AUTH_SUFFIX, data=request_args, headers=headers) + self.user_key = response.get('user_key') + return self.user_key +
+
[docs] def get_account_info(self, api_key=None): + self.api_key = api_key or 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}) +
+
[docs] 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'): + if not getattr(self, attribute_name, None): + 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}) +
+ @classmethod + def _check_batch_files(cls, data): + if 'requests' in data: # batch requests + for request in data['requests']: + per_request_files = request.get('body', {}).get('files', {}) + if per_request_files: + raise SyncanoValueError('Batch do not support files upload.') + + def _process_apns_cert_files(self, files): + files = files.copy() + 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): + 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 + +
[docs] 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 + +
+
[docs]class ConnectionMixin(object): + """Injects connection attribute with support of basic validation.""" + + def __init__(self, *args, **kwargs): + self._connection = None + super(ConnectionMixin, self).__init__(*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 + def connection(self, value): + if not isinstance(value, Connection): + raise SyncanoValueError('"connection" needs to be a Syncano Connection instance.') + self._connection = value + + @connection.deleter +
[docs] def connection(self): + self._connection = None
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/exceptions.html b/_modules/syncano/exceptions.html new file mode 100644 index 0000000..1bf6230 --- /dev/null +++ b/_modules/syncano/exceptions.html @@ -0,0 +1,280 @@ + + + + + + + + + + syncano.exceptions — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.exceptions

+import six
+
+
+
[docs]class SyncanoException(Exception): + """ + General Syncano client exception + """ + + def __init__(self, reason=None, *args): + super(SyncanoException, self).__init__(reason, *args) + self.reason = reason + + def __repr__(self): + return self.reason + + def __str__(self): + return self.reason + +
+
[docs]class SyncanoValueError(SyncanoException): + """A Python :class:`ValueError` error occurred.""" + +
+
[docs]class SyncanoRequestError(SyncanoException): + """An HTTP error occurred. + + :ivar status_code: HTTP status code e.g: 404 + :ivar reason: Error text representation + """ + + def __init__(self, status_code, reason, *args): + self.status_code = status_code + + if isinstance(reason, dict): + 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)): + value = ', '.join(value) + message += '{0}: {1}\n'.format(name, value) + reason = message + + super(SyncanoRequestError, self).__init__(reason, *args) + + def __repr__(self): + return '{0} {1}'.format(self.status_code, self.reason) + + def __str__(self): + return '{0} {1}'.format(self.status_code, self.reason) + +
+
[docs]class SyncanoValidationError(SyncanoValueError): + """A validation error occurred.""" + +
+
[docs]class SyncanoFieldError(SyncanoValidationError): + """A field error occurred. + + :ivar field_name: Related field name + """ + field_name = None + + def __repr__(self): + return '{0}: {1}'.format(self.field_name, self.reason) + + def __str__(self): + return '{0}: {1}'.format(self.field_name, self.reason) + +
+
[docs]class SyncanoDoesNotExist(SyncanoException): + """Syncano object doesn't exist error occurred.""" + +
+
[docs]class RevisionMismatchException(SyncanoRequestError): + """Revision do not match with expected one""" + +
+
[docs]class UserNotFound(SyncanoRequestError): + """Special error to handle user not found case."""
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/accounts.html b/_modules/syncano/models/accounts.html new file mode 100644 index 0000000..58bd0b8 --- /dev/null +++ b/_modules/syncano/models/accounts.html @@ -0,0 +1,463 @@ + + + + + + + + + + syncano.models.accounts — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.accounts

+from syncano.exceptions import SyncanoRequestError, SyncanoValueError, UserNotFound
+
+from . import fields
+from .base import Model
+from .classes import Class, DataObjectMixin, Object
+from .instances import Instance
+from .manager import ObjectManager
+
+
+
[docs]class Admin(Model): + """ + OO wrapper around instance admins `link <http://docs.syncano.com/docs/administrators>`_. + + :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` + """ + 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.LinksField() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/admins/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/admins/', + } + } + +
+
[docs]class Profile(DataObjectMixin, Object): + """ + """ + + PREDEFINED_CLASS_NAME = 'user_profile' + + 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.LinksField() + created_at = fields.DateTimeField(read_only=True, required=False) + updated_at = fields.DateTimeField(read_only=True, required=False) + + class Meta: + parent = Class + endpoints = { + 'detail': { + 'methods': ['delete', 'post', 'patch', 'get'], + 'path': '/objects/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/objects/', + } + } + + please = ObjectManager() + +
+
[docs]class User(Model): + """ + OO wrapper around users `link <http://docs.syncano.com/docs/user-management>`_. + + :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` + """ + + 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) + + 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) + 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/', + }, + 'auth': { + 'methods': ['post'], + 'path': '/user/auth/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/users/', + }, + 'groups': { + 'methods': ['get', 'post', 'delete'], + 'path': '/users/{id}/groups/', + } + } + +
[docs] def reset_key(self): + properties = self.get_endpoint_data() + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('reset_key', properties, http_method) + connection = self._get_connection() + return connection.request(http_method, endpoint) +
+
[docs] def auth(self, username=None, password=None): + properties = self.get_endpoint_data() + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('auth', properties, http_method) + 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(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, method) + + if group_id is not None and method != 'POST': + endpoint += '{}/'.format(group_id) + connection = self._get_connection() + + 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']) + +
[docs] def add_to_group(self, group_id): + return self._user_groups_method(group_id, method='POST') +
+
[docs] def list_groups(self): + return self._user_groups_method() +
+
[docs] def group_details(self, group_id): + return self._user_groups_method(group_id) +
+
[docs] def remove_from_group(self, group_id): + return self._user_groups_method(group_id, method='DELETE') + +
+
[docs]class Group(Model): + """ + OO wrapper around groups `link <http://docs.syncano.com/docs/groups>`_. + + :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` + """ + + label = fields.StringField(max_length=64, required=True) + description = fields.StringField(read_only=False, 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: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'patch', 'put', 'get'], + 'path': '/groups/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + '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('users', properties, method) + 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, data=data) + except SyncanoRequestError as e: + if e.status_code == 404: + raise UserNotFound(e.status_code, 'User not found.') + raise + + if method == 'DELETE': + return + + if 'objects' in response: + return [User(**user_response['user']) for user_response in response['objects']] + + return User(**response['user']) + +
[docs] def list_users(self): + return self._group_users_method() +
+
[docs] def add_user(self, user_id): + return self._group_users_method(user_id, method='POST') +
+
[docs] def user_details(self, user_id): + return self._group_users_method(user_id) +
+
[docs] def delete_user(self, user_id): + return self._group_users_method(user_id, method='DELETE')
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/archetypes.html b/_modules/syncano/models/archetypes.html new file mode 100644 index 0000000..303ea01 --- /dev/null +++ b/_modules/syncano/models/archetypes.html @@ -0,0 +1,476 @@ + + + + + + + + + + syncano.models.archetypes — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.archetypes

+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.
+
[docs] """ + 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 getattr(b.Meta, 'abstract', None)] + 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) + + for abstract in abstracts: + 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) + + 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'): +
[docs] value.contribute_to_class(cls, name) + else: + setattr(cls, name, value) + + def create_error_class(cls): + return type(
+
[docs] str('{0}DoesNotExist'.format(cls.__name__)), + (SyncanoDoesNotExist, ), + {} + ) + + def build_doc(cls, name, meta): + """Give the class a docstring if it's not defined.
+
[docs] """ + 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.
+
[docs] """ + + def __init__(self, **kwargs): + self.is_lazy = kwargs.pop('is_lazy', False) + 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): + """ +
[docs] 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, method) + if 'expected_revision' in kwargs: + data.update({'expected_revision': kwargs['expected_revision']}) + request = {'data': data} + + if not self.is_lazy: + response = connection.request(method, endpoint, **request) + self.to_python(response) + return self + + return self.batch_object(method=method, path=endpoint, body=request['data'], properties=data) + + @classmethod + def batch_object(cls, method, path, body, properties=None):
+ properties = properties if properties else {} +
[docs] return { + 'body': { + 'method': method, + 'path': path, + 'body': body, + }, + 'meta': { + 'model': cls, + 'properties': properties + } + } + + def mark_for_batch(self): + self.is_lazy = True
+
[docs] + def delete(self, **kwargs): + """Removes the current instance.
+
[docs] """ + if self.is_new(): + raise SyncanoValidationError('Method allowed only on existing model.') + + properties = self.get_endpoint_data() + http_method = 'DELETE' + endpoint = self._meta.resolve_endpoint('detail', properties, http_method) + connection = self._get_connection(**kwargs) + connection.request(http_method, endpoint) + if self.__class__.__name__ == 'Instance': # avoid circular import; + registry.clear_used_instance() + self._raw_data = {} + + def reload(self, **kwargs): + """Reloads the current instance.
+
[docs] """ + if self.is_new(): + raise SyncanoValidationError('Method allowed only on existing model.') + + properties = self.get_endpoint_data() + http_method = 'GET' + endpoint = self._meta.resolve_endpoint('detail', properties, http_method) + connection = self._get_connection(**kwargs) + response = connection.request(http_method, endpoint) + self.to_python(response) + + def validate(self): + """
+
[docs] 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:
+
[docs] self.validate() + except SyncanoValidationError: + return False + else: + return True + + def is_new(self): + if 'links' in self._meta.field_names:
+
[docs] return not self.links + + if self._meta.pk.read_only and not self.pk: + return True + + return False + + def to_python(self, data): + """
+
[docs] 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 + + # 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 and self.is_new(): + field_name = field.mapping + + if field_name in 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
+
[docs] 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 value is None and field.blank: + continue + + if field.mapping: + data[field.mapping] = field.to_native(value) + else: + + param_name = getattr(field, 'param_name', field.name) + 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): + properties = {}
+
[docs] for field in self._meta.fields: + if field.has_endpoint_data: + properties[field.name] = getattr(self, field.name) + return properties +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/billing.html b/_modules/syncano/models/billing.html new file mode 100644 index 0000000..3d3d6f3 --- /dev/null +++ b/_modules/syncano/models/billing.html @@ -0,0 +1,269 @@ + + + + + + + + + + syncano.models.billing — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.billing

+from . import fields
+from .base import Model
+
+
+class Coupon(Model):
+    """
+
[docs] OO wrapper around coupons `link <TODO>`_. + + :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` + """ + + CURRENCY_CHOICES = ( + {'display_name': 'USD', 'value': 'usd'}, + ) + + name = fields.StringField(max_length=32, primary_key=True) + redeem_by = fields.DateField() + links = fields.LinksField() + 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.1/billing/coupons/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/v1.1/billing/coupons/', + } + } + + +class Discount(Model): + """
+
[docs] OO wrapper around discounts `link <TODO>`_. + + :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` + """ + + 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.LinksField() + + class Meta: + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/v1.1/billing/discounts/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/v1.1/billing/discounts/', + } + } +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/channels.html b/_modules/syncano/models/channels.html new file mode 100644 index 0000000..c51c404 --- /dev/null +++ b/_modules/syncano/models/channels.html @@ -0,0 +1,395 @@ + + + + + + + + + + syncano.models.channels — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.channels

+from threading import Thread
+
+import six
+from requests import Timeout
+from syncano import logger
+
+from . import fields
+from .base import Model
+from .instances import Instance
+
+
+
[docs]class PollThread(Thread): + def __init__(self, connection, endpoint, callback, error=None, *args, **kwargs): + self.connection = connection + self.endpoint = endpoint + self.callback = callback + self.error = error + self.abort = False + self.timeout = kwargs.pop('timeout', None) or 60 * 5 + self.last_id = kwargs.pop('last_id', None) + self.room = kwargs.pop('room', None) + super(PollThread, self).__init__(*args, **kwargs) + + logger.debug('%s created.', self) + + def __str__(self): + return '<PollThread: %s>' % self.getName() + + def __unicode__(self): + return six.u(str(self)) + +
[docs] def request(self): + kwargs = { + 'timeout': self.timeout, + 'params': {'last_id': self.last_id, 'room': self.room} + } + return self.connection.request('GET', self.endpoint, **kwargs) +
+
[docs] def run(self): + while self.abort is False: + try: + response = self.request() + except Timeout as e: + logger.debug('%s Timeout.', self) + if not self.callback(None): + self.stop() + + except Exception as e: + logger.error('%s Error "%s"', self, e) + if self.error: + self.error(e) + return + else: + logger.debug('%s Message "%s"', self, response['id']) + self.last_id = response['id'] + if not self.callback(Message(**response)): + self.stop() +
+
[docs] def stop(self): + self.abort = True + self.callback = None + self.error = None + +
+
[docs]class Channel(Model): + """ + .. _long polling: http://en.wikipedia.org/wiki/Push_technology#Long_polling + + 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` + :ivar group: :class:`~syncano.models.fields.IntegerField` + :ivar group_permissions: :class:`~syncano.models.fields.ChoiceField` + :ivar other_permissions: :class:`~syncano.models.fields.ChoiceField` + :ivar custom_publish: :class:`~syncano.models.fields.BooleanField` + + .. note:: + **Channel** has two special methods called ``publish`` and ``poll``. + First one will send message to the channel:: + + >>> channel = Channel.please.get('instance-name', 'channel-name') + >>> channel.publish({"x": 1}) + + second one will create `long polling`_ connection which will listen for messages:: + + >>> def callback(message=None): + ... print message + ... return True + + >>> channel = Channel.please.get('instance-name', 'channel-name') + >>> channel.poll(callback=callback) + """ + + TYPE_CHOICES = ( + {'display_name': 'Default', 'value': 'default'}, + {'display_name': 'Separate rooms', 'value': 'separate_rooms'}, + ) + + PERMISSIONS_CHOICES = ( + {'display_name': 'None', 'value': 'none'}, + {'display_name': 'Subscribe', 'value': 'subscribe'}, + {'display_name': 'Publish', 'value': 'publish'}, + ) + + name = fields.StringField(max_length=64, primary_key=True) + 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, required=False) + links = fields.LinksField() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/channels/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/channels/', + }, + 'poll': { + 'methods': ['get'], + 'path': '/channels/{name}/poll/', + }, + 'publish': { + 'methods': ['post'], + 'path': '/channels/{name}/publish/', + }, + 'history': { + 'methods': ['get'], + 'path': '/channels/{name}/history/', + }, + } + +
[docs] 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, http_method='GET') + connection = self._get_connection() + + thread = PollThread(connection, endpoint, callback, error, timeout=timeout, + last_id=last_id, room=room, name='poll_%s' % self.name) + thread.start() + return thread.stop +
+
[docs] def publish(self, payload, room=None): + properties = self.get_endpoint_data() + 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(http_method, endpoint, **request) + return Message(**response) + +
+
[docs]class Message(Model): + """ + 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` + :ivar author: :class:`~syncano.models.fields.JSONField` + :ivar metadata: :class:`~syncano.models.fields.JSONField` + :ivar payload: :class:`~syncano.models.fields.JSONField` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + """ + + ACTION_CHOICES = ( + {'display_name': 'custom', 'value': 0}, + {'display_name': 'create', 'value': 1}, + {'display_name': 'update', 'value': 2}, + {'display_name': 'delete', 'value': 3}, + ) + + room = fields.StringField(max_length=50, required=False) + action = fields.ChoiceField(choices=ACTION_CHOICES, read_only=True) + author = fields.JSONField(required=False, read_only=True) + metadata = fields.JSONField(required=False, read_only=True) + payload = fields.JSONField() + created_at = fields.DateTimeField(required=False, read_only=True) + + class Meta: + parent = Channel + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/history/{pk}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/history/', + }, + }
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/classes.html b/_modules/syncano/models/classes.html new file mode 100644 index 0000000..ac26fe2 --- /dev/null +++ b/_modules/syncano/models/classes.html @@ -0,0 +1,467 @@ + + + + + + + + + + syncano.models.classes — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.classes

+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):
+    """
+
[docs] OO wrapper around instance classes `link <http://docs.syncano.com/docs/classes>`_. + + :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` + :ivar objects: :class:`~syncano.models.fields.RelatedManagerField` + + .. note:: + This model is special because each related :class:`~syncano.models.base.Object` will be + **dynamically populated** with fields defined in schema attribute. + """ + + 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) + links = fields.LinksField() + 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') + + objects = fields.RelatedManagerField('Object') + + class Meta: + parent = Instance + plural_name = 'Classes' + endpoints = { + 'detail': { + 'methods': ['get', 'put', 'patch', 'delete'], + 'path': '/classes/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/classes/', + } + } + + def save(self, **kwargs): + if self.schema: # do not allow add empty schema to registry; +
[docs] registry.set_schema(self.name, self.schema.schema) # update the registry schema here; + return super(Class, self).save(**kwargs) + + +class Object(Model): + """
+
[docs] OO wrapper around data objects `link <http://docs.syncano.com/docs/data-objects>`_. + + :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) + owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, required=False) + group = fields.IntegerField(label='group id', required=False) + 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) + + 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 = cls._get_instance_name(kwargs) + class_name = cls._get_class_name(kwargs) + 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 _set_up_object_class(cls, model): + pass + + @classmethod + def _get_instance_name(cls, kwargs): + return kwargs.get('instance_name') or registry.instance_name + + @classmethod + def _get_class_name(cls, kwargs): + return kwargs.get('class_name') + + @classmethod + def create_subclass(cls, name, schema): + meta = deepcopy(Object._meta) +
[docs] attrs = { + '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) + field_class(required=False, read_only=False, query_allowed=query_allowed).contribute_to_class( + model, field.get('name') + ) + + for field in meta.fields: + if field.primary_key: + setattr(model, 'pk', field) + setattr(model, field.name, field) + + cls._set_up_object_class(model) + return model + + @classmethod + def get_or_create_subclass(cls, name, schema):
+ try: +
[docs] 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') +
[docs] + @classmethod + def get_class_schema(cls, instance_name, class_name):
+ schema = registry.get_schema(class_name) +
[docs] if not schema: + 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 + + @classmethod + def get_subclass_model(cls, instance_name, class_name, **kwargs):
+ """ +
[docs] 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: + 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) + + 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 + + return model + + +class DataObjectMixin(object): +
+
[docs] @classmethod + def _get_instance_name(cls, kwargs): + return cls.please.properties.get('instance_name') or kwargs.get('instance_name') + + @classmethod + def _get_class_name(cls, kwargs): + return cls.PREDEFINED_CLASS_NAME + + @classmethod + def get_class_object(cls): + return Class.please.get(name=cls.PREDEFINED_CLASS_NAME) +
[docs] + @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_object', cls.get_class_object) + setattr(model, '_get_instance_name', cls._get_instance_name) + setattr(model, '_get_class_name', cls._get_class_name) +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/custom_response.html b/_modules/syncano/models/custom_response.html new file mode 100644 index 0000000..1d70c51 --- /dev/null +++ b/_modules/syncano/models/custom_response.html @@ -0,0 +1,324 @@ + + + + + + + + + + syncano.models.custom_response — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.custom_response

+import json
+
+from syncano.exceptions import SyncanoException
+
+
+
[docs]class CustomResponseHandler(object): + """ + A helper class which allows to define and maintain custom response handlers. + + Consider an example: + Script code:: + + set_response(HttpResponse(status_code=200, content='{"one": 1}', content_type='application/json')) + + When suitable ScriptTrace is used:: + + trace = ScriptTrace.please.get(id=<code_box_trace_id>, script=<script_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:: + + 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 + * text/plain + + """ + def __init__(self): + self.handlers = {} + self.register_handler('application/json', self.json_handler) + self.register_handler('plain/text', self.plain_handler) + +
[docs] 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 +
+
[docs] 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 +
+
[docs] 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): + 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: + return response['stdout'] + + return response + + @staticmethod +
[docs] def json_handler(response): + return json.loads(response['response']['content']) +
+ @staticmethod +
[docs] def plain_handler(response): + return response['response']['content'] + +
+
[docs]class CustomResponseMixin(object): + """ + 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. + """ + + response_handler = CustomResponseHandler() + + @property +
[docs] def content(self): + return self.response_handler.process_response(self.result) +
+ @property +
[docs] def status_code(self): + return self.result.get('response', {}).get('status') if self.result else None +
+ @property +
[docs] def error(self): + return self.result.get('stderr') if self.result else None +
+ @property +
[docs] def content_type(self): + return self.result.get('response', {}).get('content_type') if self.result else None
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/custom_sockets.html b/_modules/syncano/models/custom_sockets.html new file mode 100644 index 0000000..8641544 --- /dev/null +++ b/_modules/syncano/models/custom_sockets.html @@ -0,0 +1,365 @@ + + + + + + + + + + syncano.models.custom_sockets — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.custom_sockets

+# -*- coding: utf-8 -*-
+from syncano.exceptions import SyncanoValueError
+from syncano.models.custom_sockets_utils import DependencyMetadataMixin, EndpointMetadataMixin
+
+from . import fields
+from .base import Instance, Model
+
+
+
[docs]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` + :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` + """ + + 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) + 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) + updated_at = fields.DateTimeField(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/', + } + } + +
[docs] def get_endpoints(self): + return SocketEndpoint.get_all_endpoints(instance_name=self.instance_name) +
+
[docs] def run(self, endpoint_name, method='GET', data=None): + endpoint = self._find_endpoint(endpoint_name) + return endpoint.run(method=method, data=data or {}) +
+ def _find_endpoint(self, endpoint_name): + endpoints = self.get_endpoints() + for endpoint in endpoints: + if '{}/{}'.format(self.name, endpoint_name) == endpoint.name: + return endpoint + raise SyncanoValueError('Endpoint {} not found.'.format(endpoint_name)) + +
[docs] 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() + config = config or {} + response = connection.request('POST', install_path, data={ + 'name': self.name, + 'install_url': url, + 'config': config + }) + + return response +
+
[docs] def install(self): + if not self.is_new(): + raise SyncanoValueError('Custom socket already installed.') + + created_socket = self.__class__.please.create( + name=self.name, + endpoints=self.endpoints_data, + dependencies=self.dependencies_data + ) + + created_socket._raw_data['links'] = created_socket._raw_data['links'].links_dict + self.to_python(created_socket._raw_data) + return self +
+
[docs] def update(self): + if self.is_new(): + raise SyncanoValueError('Install 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 +
+
[docs] 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 + +
+
[docs]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` + :ivar links: :class:`~syncano.models.fields.LinksField` + """ + name = fields.StringField(max_length=64, primary_key=True) + allowed_methods = fields.JSONField() + links = fields.LinksField() + + class Meta: + parent = CustomSocket + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/endpoints/{name}/' + }, + 'list': { + 'methods': ['get'], + 'path': '/endpoints/' + } + } + +
[docs] def run(self, method='GET', data=None): + 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)) + method = method.lower() + 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 or {}) + else: + raise SyncanoValueError('Method: {} not supported.'.format(method)) + return response +
+ @classmethod +
[docs] 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') or instance_name} + ) + response = connection.request('GET', all_endpoints_path) + return [cls(**endpoint) for endpoint in response['objects']] +
+ def _validate_method(self, method): + if '*' in self.allowed_methods or method in self.allowed_methods: + return True + return False
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/custom_sockets_utils.html b/_modules/syncano/models/custom_sockets_utils.html new file mode 100644 index 0000000..e7bf057 --- /dev/null +++ b/_modules/syncano/models/custom_sockets_utils.html @@ -0,0 +1,490 @@ + + + + + + + + + + syncano.models.custom_sockets_utils — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.custom_sockets_utils

+# -*- coding: utf-8 -*-
+import six
+from syncano.exceptions import SyncanoValueError
+
+from .classes import Class
+from .incentives import Script, ScriptEndpoint
+
+
+
[docs]class CallType(object): + """ + The type of the call object used in the custom socket; + """ + SCRIPT = 'script' + +
+
[docs]class DependencyType(object): + """ + The type of the dependency object used in the custom socket; + """ + SCRIPT = 'script' + CLASS = 'class' + +
+
[docs]class BaseCall(object): + """ + Base class for call object. + """ + + call_type = None + + def __init__(self, name, methods): + self.name = name + self.methods = methods + +
[docs] 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 + } + +
+
[docs]class ScriptCall(BaseCall): + """ + Script call object. + + The JSON format is as follows (to_dict in the base class):: + + { + 'type': 'script', + 'name': '<script_label>, + 'methods': [<method_list>], + } + + methods can be as follows: + * ['GET'] + * ['*'] - which will do a call on every request method; + """ + call_type = CallType.SCRIPT + +
+
[docs]class Endpoint(object): + """ + The object which stores metadata about endpoints in custom socket; + + The JSON format is as follows:: + + { + '<endpoint_name>': { + 'calls': [ + <list of JSON format of Calls objects> + ] + } + } + + """ + def __init__(self, name): + self.name = name + self.calls = [] + +
[docs] def add_call(self, call): + self.calls.append(call) +
+
[docs] def to_endpoint_data(self): + return { + self.name: { + 'calls': [call.to_dict() for call in self.calls] + } + } + +
+
[docs]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 + name = None + +
[docs] 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(self.get_dependency_data()) + return dependency_data +
+
[docs] def get_name(self): + if self.name is not None: + return {'name': self.name} + return {'name': self.dependency_object.name} +
+
[docs] def get_dependency_data(self): + raise NotImplementedError() +
+
[docs] 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} + +
+
[docs]class ScriptDependency(BaseDependency): + """ + Script dependency object; + + The JSON format is as follows:: + { + 'type': 'script', + 'runtime_name': '<runtime name defined in RuntimeChoices>', + 'source': '<source>', + 'name': '<name>' + } + """ + + dependency_type = DependencyType.SCRIPT + fields = [ + 'runtime_name', + 'source' + ] + + 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 isinstance(script_or_script_endpoint, Script) and not name: + raise SyncanoValueError('Name should be provided.') + + self.dependency_object = script_or_script_endpoint + self.name = name + +
[docs] 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(self._build_dict(script)) + return dependency_data +
+ @classmethod +
[docs] def create_from_raw_data(cls, raw_data): + return cls(**{ + 'script_or_script_endpoint': Script(source=raw_data['source'], runtime_name=raw_data['runtime_name']), + 'name': raw_data['name'], + }) + +
+
[docs]class ClassDependency(BaseDependency): + """ + Class dependency object; + + The JSON format is as follows:: + { + 'type': 'class', + 'name': '<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 + +
[docs] def get_dependency_data(self): + data_dict = self._build_dict(self.dependency_object) + data_dict['schema'] = data_dict['schema'].schema + return data_dict +
+ @classmethod +
[docs] def create_from_raw_data(cls, raw_data): + return cls(**{'class_instance': Class(**raw_data)}) + +
+
[docs]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() + +
[docs] 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 == CallType.SCRIPT: + return ScriptCall + +
[docs] def add_endpoint(self, endpoint): + self._endpoints.append(endpoint) +
+
[docs] def remove_endpoint(self, endpoint_name): + for index, endpoint in enumerate(self._endpoints): + if endpoint.name == endpoint_name: + self._endpoints.pop(index) + break +
+ @property +
[docs] def endpoints_data(self): + endpoints = {} + for endpoint in self._endpoints: + endpoints.update(endpoint.to_endpoint_data()) + return endpoints + +
+
[docs]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() + +
[docs] def update_dependencies(self): + for raw_depedency in self.dependencies: + 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 == DependencyType.SCRIPT: + return ScriptDependency + elif depedency_type == DependencyType.CLASS: + return ClassDependency + +
[docs] def add_dependency(self, depedency): + self._dependencies.append(depedency) +
+
[docs] 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 +
[docs] def dependencies_data(self): + return [dependency.to_dependency_data() for dependency in self._dependencies]
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/data_views.html b/_modules/syncano/models/data_views.html new file mode 100644 index 0000000..3fd90f8 --- /dev/null +++ b/_modules/syncano/models/data_views.html @@ -0,0 +1,331 @@ + + + + + + + + + + syncano.models.data_views — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.data_views

+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
+
+
+
[docs]class DataEndpoint(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` + """ + + PERMISSIONS_CHOICES = ( + {'display_name': 'None', 'value': 'none'}, + {'display_name': 'Read', 'value': 'read'}, + {'display_name': 'Write', 'value': 'write'}, + {'display_name': 'Full', 'value': 'full'}, + ) + + name = fields.StringField(max_length=64, primary_key=True) + description = fields.StringField(required=False) + + query = fields.JSONField(read_only=False, required=False) + + 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.LinksField() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['get', 'put', 'patch', 'delete'], + 'path': '/endpoints/data/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/endpoints/data/', + }, + 'get': { + 'methods': ['get'], + 'path': '/endpoints/data/{name}/get/', + }, + 'rename': { + 'methods': ['post'], + 'path': '/endpoints/data/{name}/rename/', + }, + 'clear_cache': { + 'methods': ['post'], + 'path': '/endpoints/data/{name}/clear_cache/', + } + } + +
[docs] def rename(self, new_name): + properties = self.get_endpoint_data() + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('rename', properties, http_method) + connection = self._get_connection() + return connection.request(http_method, + endpoint, + data={'new_name': new_name}) +
+
[docs] def clear_cache(self): + properties = self.get_endpoint_data() + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('clear_cache', properties, http_method) + connection = self._get_connection() + return connection.request(http_method, endpoint) +
+
[docs] 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) + + http_method = 'GET' + endpoint = self._meta.resolve_endpoint('get', properties, http_method) + + kwargs = {} + params = {} + params.update({'query': json.dumps(query)}) + + if cache_key is not None: + params = {'cache_key': cache_key} + + 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) + 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 + +
[docs] def add_object(self, **kwargs): + return Object(instance_name=self.instance_name, class_name=self.class_name, **kwargs).save()
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/fields.html b/_modules/syncano/models/fields.html new file mode 100644 index 0000000..7f42371 --- /dev/null +++ b/_modules/syncano/models/fields.html @@ -0,0 +1,1123 @@ + + + + + + + + + + syncano.models.fields — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.fields

+import json
+import re
+from datetime import date, datetime
+
+import six
+import validictory
+from syncano import PUSH_ENV, logger
+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
+from .relations import RelationManager, RelationValidatorMixin
+
+
+
[docs]class JSONToPythonMixin(object): + +
[docs] 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 + +
+
[docs]class Field(object): + """Base class for all field types.""" + + required = False + read_only = True + blank = True + default = None + primary_key = False + + has_data = True + has_endpoint_data = False + + query_allowed = True + allow_increment = False + + creation_counter = 0 + field_lookups = [] + + def __init__(self, name=None, **kwargs): + self.name = name + self.model = None + self.default = kwargs.pop('default', self.default) + self.required = kwargs.pop('required', self.required) + 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) + self.has_data = kwargs.pop('has_data', self.has_data) + self.has_endpoint_data = kwargs.pop('has_endpoint_data', self.has_endpoint_data) + self.primary_key = kwargs.pop('primary_key', self.primary_key) + + # Adjust the appropriate creation counter, and save our local copy. + self.creation_counter = Field.creation_counter + Field.creation_counter += 1 + + def __repr__(self): + """Displays current instance class name and field name.""" + return '<{0}: {1}>'.format(self.__class__.__name__, self.name) + + def __eq__(self, other): + if isinstance(other, Field): + return self.creation_counter == other.creation_counter + return NotImplemented + + 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 + return hash(self.creation_counter) + + 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 __get__(self, instance, owner): + 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): + 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) + + def __delete__(self, instance): + if self.name in instance._raw_data: + del instance._raw_data[self.name] + +
[docs] def validate(self, value, model_instance): + """ + Validates the current field instance. + + :raises: SyncanoFieldError + """ + if self.required and not value: + raise self.ValidationError('This field is required.') + + if isinstance(value, six.string_types): + if self.max_length and len(value) > self.max_length: + raise self.ValidationError('Max length reached.') + + if self.min_length and len(value) < self.min_length: + raise self.ValidationError('Min length reached.') +
+
[docs] def to_python(self, value): + """ + Returns field's value prepared for usage in Python. + """ + if isinstance(value, dict) and 'type' in value and 'value' in value: + return value['value'] + + return value +
+
[docs] def to_native(self, value): + """ + Returns field's value prepared for serialization into JSON. + """ + return value +
+
[docs] def to_query(self, value, lookup_type, **kwargs): + """ + Returns field's value prepared for usage in HTTP request query. + """ + if not self.query_allowed: + raise self.ValidationError('Query on this field is not supported.') + + return self.to_native(value) +
+
[docs] def contribute_to_class(self, cls, name): + if name in cls._meta.endpoint_fields: + self.has_endpoint_data = True + + if not self.name: + self.name = name + + if not self.label: + self.label = self.name.replace('_', ' ').capitalize() + + if self.primary_key: + if cls._meta.pk: + raise SyncanoValueError('Multiple pk fields detected.') + + cls._meta.pk = self + setattr(cls, 'pk', self) + + self.model = cls + cls._meta.add_field(self) + setattr(cls, name, self) + + error_class = type( + '{0}ValidationError'.format(self.__class__.__name__), + (SyncanoFieldError, ), + {'field_name': name} + ) + + setattr(self, 'ValidationError', error_class) + +
+
[docs]class RelatedManagerField(Field): + + def __init__(self, model_name, endpoint='list', *args, **kwargs): + super(RelatedManagerField, self).__init__(*args, **kwargs) + self.model_name = model_name + self.endpoint = endpoint + + 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) + +
[docs] def contribute_to_class(self, cls, name): + setattr(cls, name, self) + +
+
[docs]class PrimaryKeyField(Field): + primary_key = True + +
+
[docs]class WritableField(Field): + required = True + read_only = False + +
+
[docs]class EndpointField(WritableField): + has_data = False + has_endpoint_data = True + +
+
[docs]class StringField(WritableField): + + field_lookups = [ + 'startswith', + 'endswith', + 'contains', + 'istartswith', + 'iendswith', + 'icontains', + 'ieq', + ] + +
[docs] def to_python(self, value): + value = super(StringField, self).to_python(value) + + if isinstance(value, six.string_types) or value is None: + return value + return force_text(value) + +
+
[docs]class IntegerField(WritableField): + allow_increment = True + +
[docs] def to_python(self, value): + value = super(IntegerField, self).to_python(value) + + if value is None: + return + try: + return int(value) + except (TypeError, ValueError): + raise self.ValidationError('Invalid value. Value should be an integer.') + +
+
[docs]class ReferenceField(IntegerField): + +
[docs] def to_python(self, value): + if isinstance(value, int): + return value + + if hasattr(value, 'pk') and isinstance(value.pk, int): + value = value.pk + + return super(ReferenceField, self).to_python(value) + +
+
[docs]class FloatField(WritableField): + allow_increment = True + +
[docs] def to_python(self, value): + value = super(FloatField, self).to_python(value) + + if value is None: + return + try: + return float(value) + except (TypeError, ValueError): + raise self.ValidationError('Invalid value. Value should be a float.') + +
+
[docs]class BooleanField(WritableField): + +
[docs] def to_python(self, value): + value = super(BooleanField, self).to_python(value) + + if value is None: + return + + if value in (True, 't', 'true', 'True', '1'): + return True + + if value in (False, 'f', 'false', 'False', '0'): + return False + + raise self.ValidationError('Invalid value. Value should be a boolean.') + +
+
[docs]class SlugField(StringField): + regex = re.compile(r'^[-a-zA-Z0-9_]+$') + +
[docs] def validate(self, value, model_instance): + super(SlugField, self).validate(value, model_instance) + + if not isinstance(value, six.string_types): + raise self.ValidationError('Invalid value. Value should be a string.') + + if not bool(self.regex.search(value)): + raise self.ValidationError('Invalid value.') + return value + +
+
[docs]class EmailField(StringField): + regex = re.compile(r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)') + +
[docs] def validate(self, value, model_instance): + super(EmailField, self).validate(value, model_instance) + + if not isinstance(value, six.string_types): + raise self.ValidationError('Invalid value. Value should be a string.') + + if not value or '@' not in value: + raise self.ValidationError('Enter a valid email address.') + + if not bool(self.regex.match(value)): + raise self.ValidationError('Enter a valid email address.') + +
+
[docs]class ChoiceField(WritableField): + + def __init__(self, *args, **kwargs): + self.choices = kwargs.pop('choices', []) + self.allowed_values = [choice['value'] for choice in self.choices] + super(ChoiceField, self).__init__(*args, **kwargs) + +
[docs] def validate(self, value, model_instance): + super(ChoiceField, self).validate(value, model_instance) + 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)) + +
+
[docs]class DateField(WritableField): + date_regex = re = re.compile( + r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$' + ) + +
[docs] def to_python(self, value): + value = super(DateField, self).to_python(value) + + if value is None: + return + + if isinstance(value, datetime): + return value.date() + + if isinstance(value, date): + return value + + if isinstance(value, (int, float)): + dt = datetime.fromtimestamp(value) + return dt.date() + + try: + parsed = self.parse_date(value) + if parsed is not None: + return parsed + except (ValueError, TypeError): + pass + + raise self.ValidationError("'{0}' value has an invalid date format. It must be " + "in YYYY-MM-DD format.".format(value)) +
+
[docs] def parse_date(self, value): + match = self.date_regex.match(value) + if match: + kw = {k: int(v) for k, v in six.iteritems(match.groupdict())} + return date(**kw) +
+
[docs] def to_native(self, value): + if isinstance(value, datetime): + value = value.date() + return value.isoformat() + +
+
[docs]class DateTimeField(DateField): + FORMAT = '%Y-%m-%dT%H:%M:%S.%f' + +
[docs] def to_python(self, value): + if value is None: + return + + if isinstance(value, dict) and 'type' in value and 'value' in value: + value = value['value'] + + if isinstance(value, datetime): + return value + + if isinstance(value, date): + return datetime(value.year, value.month, value.day) + + if isinstance(value, (int, float)): + return datetime.fromtimestamp(value) + + if isinstance(value, six.string_types): + value = value.split('Z')[0] + + parsers = [ + self.parse_from_string, + self.parse_from_date, + ] + + for parser in parsers: + try: + value = parser(value) + except (ValueError, TypeError): + pass + else: + return value + + raise self.ValidationError("'{0}' value has an invalid format. It must be in " + "YYYY-MM-DD HH:MM[:ss[.uuuuuu]] format.".format(value)) +
+
[docs] def parse_from_string(self, value): + return datetime.strptime(value, self.FORMAT) +
+
[docs] def parse_from_date(self, value): + parsed = self.parse_date(value) + + if not parsed: + raise ValueError + + return datetime(parsed.year, parsed.month, parsed.day) +
+
[docs] def to_native(self, value): + if value is None: + return + ret = value.strftime(self.FORMAT) + if ret.endswith('+00:00'): + ret = ret[:-6] + 'Z' + + if not ret.endswith('Z'): + ret = ret + 'Z' + + return ret + +
+ +
[docs]class LinksField(Field): + query_allowed = False + IGNORED_LINKS = ('self', ) + + def __init__(self, *args, **kwargs): + super(LinksField, self).__init__(*args, **kwargs) + +
[docs] def to_python(self, value): + return LinksWrapper(value, self.IGNORED_LINKS) +
+
[docs] def to_native(self, value): + return value.to_native() + +
+
[docs]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) + +
[docs] def contribute_to_class(self, cls, name): + super(ModelField, self).contribute_to_class(cls, name) + + if isinstance(self.rel, six.string_types): + + def lazy_relation(cls, field): + if isinstance(field.rel, six.string_types): + field.rel = registry.get_model_by_name(field.rel) + + try: + self.rel = registry.get_model_by_name(self.rel) + except LookupError: + value = (lazy_relation, (cls, self), {}) + registry._pending_lookups.setdefault(self.rel, []).append(value) + else: + lazy_relation(cls, self) +
+
[docs] def validate(self, value, model_instance): + super(ModelField, self).validate(value, model_instance) + + if not isinstance(value, (self.rel, dict)): + 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 \ + (self.is_data_object_mixin and hasattr(value, 'validate')): + value.validate() +
+
[docs] def to_python(self, value): + + if value is None: + return + + if isinstance(value, self.rel): + return value + + if isinstance(value, dict): + return self.rel(**value) + + raise self.ValidationError("'{0}' has unsupported format.".format(value)) +
+
[docs] def to_native(self, value): + if value is None: + return + + if isinstance(value, self.rel): + if not self.just_pk: + return value.to_native() + + pk_field = value._meta.pk + pk_value = getattr(value, pk_field.name) + return pk_field.to_native(pk_value) + + if self.is_data_object_mixin and not self.just_pk and hasattr(value, 'to_native'): + return value.to_native() + + return value + +
+
[docs]class FileField(WritableField): + param_name = 'files' + +
[docs] def to_native(self, value): + if isinstance(value, six.string_types): + return None + return {self.name: value} + +
+
[docs]class JSONField(JSONToPythonMixin, WritableField): + query_allowed = False + schema = None + + def __init__(self, *args, **kwargs): + self.schema = kwargs.pop('schema', None) or self.schema + super(JSONField, self).__init__(*args, **kwargs) + +
[docs] def validate(self, value, model_instance): + super(JSONField, self).validate(value, model_instance) + if self.schema: + try: + validictory.validate(value, self.schema) + except ValueError as e: + raise self.ValidationError(e) +
+
[docs] def to_native(self, value): + if value is None: + return + + if not isinstance(value, six.string_types): + value = json.dumps(value) + return value + +
+
[docs]class ArrayField(JSONToPythonMixin, WritableField): + +
[docs] 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) + except (ValueError, TypeError): + raise SyncanoValueError('Expected an array') + + 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') + + 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') + +
+
[docs]class ObjectField(JSONToPythonMixin, WritableField): + +
[docs] 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) + except (ValueError, TypeError): + raise SyncanoValueError('Expected an object') + + if not isinstance(value, dict): + raise SyncanoValueError('Expected an object') + +
+
[docs]class SchemaField(JSONField): + required = False + query_allowed = False + not_indexable_types = ['text', 'file'] + schema = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string', + 'required': True, + }, + 'type': { + 'type': 'string', + 'required': True, + 'enum': [ + 'string', + 'text', + 'integer', + 'float', + 'boolean', + 'datetime', + 'file', + 'reference', + 'relation', + 'array', + 'object', + 'geopoint', + ], + }, + 'order_index': { + 'type': 'boolean', + 'required': False, + }, + 'filter_index': { + 'type': 'boolean', + 'required': False, + }, + 'target': { + 'type': 'string', + 'required': False, + } + } + } + } + +
[docs] def validate(self, value, model_instance): + if value is None: + return + + if isinstance(value, SchemaManager): + value = value.schema + + super(SchemaField, self).validate(value, model_instance) + + fields = [f['name'] for f in value] + if len(fields) != len(set(fields)): + raise self.ValidationError('Field names must be unique.') + + for field in value: + is_not_indexable = field['type'] in self.not_indexable_types + has_index = ('order_index' in field or 'filter_index' in field) + if is_not_indexable and has_index: + raise self.ValidationError('"{0}" type is not indexable.'.format(field['type'])) +
+
[docs] def to_python(self, value): + if isinstance(value, SchemaManager): + return value + + value = super(SchemaField, self).to_python(value) + return SchemaManager(value) +
+
[docs] def to_native(self, value): + if isinstance(value, SchemaManager): + value = value.schema + + return super(SchemaField, self).to_native(value) + +
+
[docs]class PushJSONField(JSONField): +
[docs] def to_native(self, value): + if value is None: + return + + if not isinstance(value, six.string_types): + if 'environment' not in value: + value.update({ + 'environment': PUSH_ENV, + }) + value = json.dumps(value) + return value + +
+
[docs]class ListField(WritableField): + +
[docs] def validate(self, value, model_instance): + if value is None: + return + + if not isinstance(value, list): + raise self.ValidationError('List expected.') + +
+
[docs]class GeoPointField(Field): + + field_lookups = ['near', 'exists'] + +
[docs] def validate(self, value, model_instance): + super(GeoPointField, 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, GeoPoint): + raise SyncanoValueError('Expected a GeoPoint') +
+
[docs] def to_native(self, value): + if value is None: + return + + if isinstance(value, bool): + return value # exists lookup + + if isinstance(value, dict): + value = GeoPoint(latitude=value['latitude'], longitude=value['longitude']) + + 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 +
+
[docs] 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, **kwargs) + + if lookup_type not in self.field_lookups: + 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(latitude=value.pop('latitude'), longitude=value.pop('longitude')), + Distance(**value) + ) + + 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: ' + '<field_name>__near=(GeoPoint(52.12, 22.12), Distance(kilometers=100))') + + query_dict = value[0].to_native() + query_dict.update(value[1].to_native()) + + return query_dict +
+
[docs] 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: + 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 + + if isinstance(value, dict): + latitude = value.get('latitude') + longitude = value.get('longitude') + elif isinstance(value, (tuple, list)): + try: + latitude = value[0] + longitude = value[1] + except IndexError: + raise SyncanoValueError('Can not parse the geo point.') + + return latitude, longitude + +
+
[docs]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) + +
[docs] 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'] + + if isinstance(value, dict) and ('_add' in value or '_remove' in value): + return value + + if not isinstance(value, (list, tuple)): + return [value] + + return value +
+
[docs] 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 self.field_lookups: + raise SyncanoValueError('Lookup {} not supported for relation field.'.format(lookup_type)) + + query_dict = {} + + if lookup_type == 'contains': + if self._check_relation_value(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 +
+
[docs] 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._check_relation_value(value): + value = [obj.id for obj in value] + return value + +
+MAPPING = { + 'string': StringField, + 'text': StringField, + 'file': FileField, + 'ref': StringField, + 'reference': ReferenceField, + 'relation': RelationField, + 'integer': IntegerField, + 'float': FloatField, + 'boolean': BooleanField, + 'name': SlugField, + 'email': EmailField, + 'choice': ChoiceField, + 'date': DateField, + 'datetime': DateTimeField, + 'field': Field, + 'writable': WritableField, + 'endpoint': EndpointField, + 'links': LinksField, + 'model': ModelField, + 'json': JSONField, + 'schema': SchemaField, + 'array': ArrayField, + 'object': ObjectField, + 'geopoint': GeoPointField, +} +
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/geo.html b/_modules/syncano/models/geo.html new file mode 100644 index 0000000..7fbb97b --- /dev/null +++ b/_modules/syncano/models/geo.html @@ -0,0 +1,236 @@ + + + + + + + + + + syncano.models.geo — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.geo

+# -*- coding: utf-8 -*-
+from syncano.exceptions import SyncanoValueError
+
+
+
[docs]class GeoPoint(object): + + def __init__(self, latitude, longitude): + self.latitude = latitude + self.longitude = longitude + + def __repr__(self): + return "GeoPoint(latitude={}, longitude={})".format(self.latitude, self.longitude) + +
[docs] def to_native(self): + geo_struct_dump = {'latitude': self.latitude, 'longitude': self.longitude} + return geo_struct_dump + +
+
[docs]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 + +
[docs] def to_native(self): + return { + 'distance{}'.format(self.unit): self.distance + }
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/hosting.html b/_modules/syncano/models/hosting.html new file mode 100644 index 0000000..a3a7005 --- /dev/null +++ b/_modules/syncano/models/hosting.html @@ -0,0 +1,321 @@ + + + + + + + + + + syncano.models.hosting — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.hosting

+# -*- coding: utf-8 -*-
+
+from . import fields
+from .base import Model
+from .instances import Instance
+
+
+
[docs]class Hosting(Model): + """ + OO wrapper around hosting. + """ + + 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=[]) + + 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/', + } + } + +
[docs] 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() + 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: + return + return HostingFile(**response.json()) +
+
[docs] 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: + # create if not found; + hosting_file = self.upload_file(path, file) + return hosting_file + + connection = self._get_connection() + 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: + return + return HostingFile(**response.json()) +
+
[docs] def list_files(self): + return self._get_files() +
+
[docs] def set_default(self): + default_path = self.links.set_default + connection = self._get_connection() + + 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)] + +
+
[docs]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/', + } + }
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/incentives.html b/_modules/syncano/models/incentives.html new file mode 100644 index 0000000..7317671 --- /dev/null +++ b/_modules/syncano/models/incentives.html @@ -0,0 +1,560 @@ + + + + + + + + + + syncano.models.incentives — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.incentives

+import json
+
+from syncano.exceptions import SyncanoValidationError
+
+from . import fields
+from .base import Model
+from .instances import Instance
+from .manager import ScriptEndpointManager, ScriptManager
+from .mixins import RenameMixin
+
+
+class RuntimeChoices(object):
+    """
+
[docs] 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): + """
+
[docs] OO wrapper around scripts `link <http://docs.syncano.com/docs/snippets-scripts>`_. + + :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:: + **Script** has special method called ``run`` which will execute attached source code:: + + >>> 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:: + + >>> s = Script.please.get('instance-name', 1234) + >>> s.run() + >>> s.run(variable_one=1, variable_two=2) + """ + + label = fields.StringField(max_length=80, required=False) + description = fields.StringField(required=False) + source = fields.StringField() + runtime_name = fields.StringField() + 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) + + traces = fields.RelatedManagerField('ScriptTrace') + + please = ScriptManager() + + class Meta: + parent = Instance + name = 'Script' + plural_name = 'Scripts' + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/snippets/scripts/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/snippets/scripts/', + }, + 'run': { + 'methods': ['post'], + 'path': '/snippets/scripts/{id}/run/', + }, + } + + def run(self, **payload): + """ +
[docs] Usage:: + + >>> s = Script.please.get('instance-name', 1234) + >>> s.run() + >>> s.run(variable_one=1, variable_two=2) + """ + from .traces import ScriptTrace + + if self.is_new(): + raise SyncanoValidationError('Method allowed only on existing model.') + + properties = self.get_endpoint_data() + 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(http_method, endpoint, **request) + response.update({'instance_name': self.instance_name, 'script_id': self.id}) + return ScriptTrace(**response) + + +class Schedule(Model): + """
+
[docs] OO wrapper around script schedules `link <http://docs.syncano.com/docs/schedules>`_. + + :ivar label: :class:`~syncano.models.fields.StringField` + :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` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar scheduled_next: :class:`~syncano.models.fields.DateTimeField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + """ + + label = fields.StringField(max_length=80) + 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) + 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() + + traces = fields.RelatedManagerField('ScheduleTraces') + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/schedules/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/schedules/', + } + } + + +class Trigger(Model): + """
+
[docs] OO wrapper around triggers `link <http://docs.syncano.com/docs/triggers>`_. + + :ivar label: :class:`~syncano.models.fields.StringField` + :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` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + + 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) + script = fields.IntegerField(label='script id') + class_name = fields.StringField(label='class name', mapping='class') + signal = fields.ChoiceField(choices=SIGNAL_CHOICES) + 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') + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/triggers/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/triggers/', + } + } + + +class ScriptEndpoint(Model): + """
+
[docs] OO wrapper around script endpoints `link <http://docs.syncano.com/docs/codebox-sockets>`_. + + :ivar name: :class:`~syncano.models.fields.SlugField` + :ivar script: :class:`~syncano.models.fields.IntegerField` + :ivar links: :class:`~syncano.models.fields.HyperlinkedField` + + .. note:: + **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}") + + or via instance:: + + >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') + >>> se.run() + >>> se.run(variable_one=1, variable_two=2) + + """ + + 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.LinksField() + + traces = fields.RelatedManagerField('ScriptEndpointTrace') + please = ScriptEndpointManager() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/endpoints/scripts/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/endpoints/scripts/', + }, + 'run': { + 'methods': ['post'], + 'path': '/endpoints/scripts/{name}/run/', + }, + 'reset': { + 'methods': ['post'], + 'path': '/endpoints/scripts/{name}/reset_link/', + }, + 'public': { + 'methods': ['get'], + 'path': '/endpoints/scripts/p/{public_link}/{name}/', + } + } + + def run(self, cache_key=None, **payload): + """ +
[docs] Usage:: + + >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') + >>> se.run() + >>> se.run(variable_one=1, variable_two=2) + """ + from .traces import ScriptEndpointTrace + + if self.is_new(): + raise SyncanoValidationError('Method allowed only on existing model.') + + properties = self.get_endpoint_data() + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('run', properties, http_method) + connection = self._get_connection(**payload) + + params = {} + if cache_key is not None: + params = {'cache_key': cache_key} + + kwargs = {'data': payload} + if params: + kwargs.update({'params': params}) + + 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, + 'script_name': self.name}) + return ScriptEndpointTrace(**response) + # if script is a custom one, return result 'as-it-is'; + return response + + def reset_link(self): + """
+
+
[docs] OO wrapper around templates. + + :ivar name: :class:`~syncano.models.fields.StringField` + :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` + :ivar created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + + 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.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': ['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 or {} +
[docs] properties = self.get_endpoint_data() + http_method = 'POST' + endpoint = self._meta.resolve_endpoint('render', properties, http_method) + + connection = self._get_connection() + return connection.request(http_method, endpoint, data={'context': context}) + + def rename(self, new_name): + rename_path = self.links.rename
+
[docs] data = {'new_name': new_name} + connection = self._get_connection() + response = connection.request('POST', rename_path, data=data) + self.to_python(response) + return self +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/instances.html b/_modules/syncano/models/instances.html new file mode 100644 index 0000000..9bb319a --- /dev/null +++ b/_modules/syncano/models/instances.html @@ -0,0 +1,392 @@ + + + + + + + + + + syncano.models.instances — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.instances

+import json
+
+import six
+from syncano.exceptions import SyncanoValueError
+
+from . import fields
+from .base import Model
+from .mixins import RenameMixin
+
+
+
[docs]class Instance(RenameMixin, Model): + """ + OO wrapper around instances `link <http://docs.syncano.com/docs/getting-started-with-syncano#adding-an-instance>`_. + + :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` + :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) + 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.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') + data_endpoints = fields.RelatedManagerField('DataEndpoint') + templates = fields.RelatedManagerField('ResponseTemplate') + + triggers = fields.RelatedManagerField('Trigger') + schedules = fields.RelatedManagerField('Schedule') + classes = fields.RelatedManagerField('Class') + invitations = fields.RelatedManagerField('InstanceInvitation') + hostings = fields.RelatedManagerField('Hosting') + + # 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': { + 'methods': ['delete', 'patch', 'put', 'get'], + 'path': '/v1.1/instances/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/v1.1/instances/', + }, + 'config': { + 'methods': ['put', 'get'], + 'path': '/v1.1/instances/{name}/snippets/config/', + }, + 'endpoints': { + 'methods': ['get'], + 'path': '/v1.1/instances/{name}/endpoints/sockets/' + } + } + +
[docs] 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'] +
+
[docs] 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) + data = {'config': config} + connection = self._get_connection() + connection.request(http_method, endpoint, data=data) + +
+
[docs]class ApiKey(Model): + """ + OO wrapper around instance api keys `link <http://docs.syncano.com/docs/authentication>`_. + + :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` + """ + + 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.LinksField() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['get', 'delete'], + 'path': '/api_keys/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/api_keys/', + } + } + +
+
[docs]class InstanceInvitation(Model): + """ + OO wrapper around instance + invitations `link <http://docs.syncano.com/docs/administrators#inviting-administrators>`_. + + :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` + """ + 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.LinksField() + 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/', + } + } + +
[docs] 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
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/manager.html b/_modules/syncano/models/manager.html new file mode 100644 index 0000000..97a482e --- /dev/null +++ b/_modules/syncano/models/manager.html @@ -0,0 +1,1446 @@ + + + + + + + + + + syncano.models.manager — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.manager

+import json
+from copy import deepcopy
+
+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 ArrayOperationsMixin, IncrementMixin, clone
+
+from .registry import registry
+
+# The maximum number of items to display in a Manager.__repr__
+REPR_OUTPUT_SIZE = 20
+
+
+
[docs]class ManagerDescriptor(object): + + def __init__(self, manager): + self.manager = manager + + def __get__(self, instance, owner=None): + if instance is not None: + raise AttributeError("Manager isn't accessible via {0} instances.".format(owner.__name__)) + return self.manager.all() + +
+
[docs]class Manager(ConnectionMixin): + """Base class responsible for all ORM (``please``) actions.""" + + BATCH_URI = '/v1.1/instances/{name}/batch/' + + def __init__(self): + self.name = None + self.model = None + + self.endpoint = None + self.properties = {} + + self.method = None + self.query = {} + self.data = {} + self.is_lazy = False + self._filter_kwargs = {} + + 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]) + if len(data) > REPR_OUTPUT_SIZE: + data[-1] = '...(remaining elements truncated)...' + return repr(data) + + def __str__(self): # pragma: no cover + return '<Manager: {0}>'.format(self.model.__name__) + + def __unicode__(self): # pragma: no cover + return six.u(str(self)) + + def __len__(self): # pragma: no cover + return self.iterator() + + 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] + return True + except IndexError: + return False + + def __getitem__(self, k): + """ + Retrieves an item or slice from the set of results. + """ + if not isinstance(k, (slice,) + six.integer_types): + raise TypeError + assert ((not isinstance(k, slice) and (k >= 0)) or + (isinstance(k, slice) and (k.start is None or k.start >= 0) and + (k.stop is None or k.stop >= 0))), \ + "Negative indexing is not supported." + + manager = self._clone() + + if isinstance(k, slice): + if k.stop is not None: + manager.limit(int(k.stop) + 1) + return list(manager)[k.start:k.stop:k.step] + + manager.limit(k + 1) + return list(manager)[k] + + 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 + +
[docs] def as_batch(self): + self.is_lazy = True + return self +
+
[docs] 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. + + 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}, <User: 9>, <User: 11>, ...] + + :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: + 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.instance_name), + **{'data': {'requests': requests}} + ) + + 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
+
[docs] def create(self, **kwargs): + """ + A convenience method for creating an object and saving it all in one step. Thus:: + + instance = Instance.please.create(name='test-one', description='description') + + and:: + + instance = Instance(name='test-one', description='description') + instance.save() + + are equivalent. + """ + data = self.properties.copy() + attrs = kwargs.copy() + 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) + + saved_instance = instance.save() + if not self.is_lazy: + return instance + + return saved_instance +
+
[docs] def bulk_create(self, *objects): + """ + Creates many new instances based on provided list of objects. + + Usage:: + + 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:: + + This method is restricted to handle 50 objects at once. + """ + return ModelBulkCreate(objects, self).process() +
+ @clone +
[docs] def get(self, *args, **kwargs): + """ + Returns the object matching the given lookup parameters. + + Usage:: + + instance = Instance.please.get('test-one') + instance = Instance.please.get(name='test-one') + """ + self.method = 'GET' + self.endpoint = 'detail' + self._filter(*args, **kwargs) + return self.request() +
+ @clone +
[docs] 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': <Class: 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: <SyncanoTestClassObject: 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.instance_name), + **{'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 +
+
[docs] def detail(self, *args, **kwargs): + """ + Wrapper around ``get`` method. + + Usage:: + + instance = Instance.please.detail('test-one') + instance = Instance.please.detail(name='test-one') + """ + return self.get(*args, **kwargs) +
+
[docs] def get_or_create(self, **kwargs): + """ + A convenience method for looking up an object with the given + lookup parameters, creating one if necessary. + + Returns a tuple of **(object, created)**, where **object** is the retrieved or + **created** object and created is a boolean specifying whether a new object was created. + + This is meant as a shortcut to boilerplatish code. For example:: + + try: + instance = Instance.please.get(name='test-one') + except Instance.DoesNotExist: + instance = Instance(name='test-one', description='test') + instance.save() + + The above example can be rewritten using **get_or_create()** like so:: + + instance, created = Instance.please.get_or_create(name='test-one', defaults={'description': 'test'}) + """ + defaults = deepcopy(kwargs.pop('defaults', {})) + try: + instance = self.get(**kwargs) + except self.model.DoesNotExist: + defaults.update(kwargs) + instance = self.create(**defaults) + created = True + else: + created = False + return instance, created +
+ @clone +
[docs] def delete(self, *args, **kwargs): + """ + Removes single instance based on provided arguments. + Returns None if deletion went fine. + + Usage:: + + Instance.please.delete('test-one') + Instance.please.delete(name='test-one') + """ + self.method = 'DELETE' + self.endpoint = 'detail' + 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) +
+ @clone +
[docs] 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 +
[docs] def update(self, *args, **kwargs): + if self._filter_kwargs or self.query: # means that .filter() was run; + return self.new_update(**kwargs) + return self.old_update(*args, **kwargs) +
+ @clone +
[docs] def new_update(self, **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; + + """ + + 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.data.update(self._filter_kwargs) + 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=serialized, properties=defaults)] + + instances = [] # ObjectManager context; + for obj in self: + 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=serialized, + properties=defaults) + + instances.append(updated_instance) # always a batch structure here; + + if not self.is_lazy: + instances = self.batch(instances) + + return instances +
+ @clone +
[docs] def old_update(self, *args, **kwargs): + """ + 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('PATCH', 'PUT', 'POST') + data = kwargs.pop('data', {}) + self.data = kwargs.copy() + self.data.update(data) + + model = self.serialize(self.data, self.model) + + serialized = model.to_native() + + serialized = {k: v for k, v in six.iteritems(serialized) + 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) +
+
[docs] 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. + The ``defaults`` is a dictionary of (field, value) pairs used to update the object. + + Returns a tuple of **(object, created)**, where object is the created or updated object and created + is a boolean specifying whether a new object was created. + + The **update_or_create** method tries to fetch an object from Syncano API based on the given kwargs. + If a match is found, it updates the fields passed in the defaults dictionary. + + This is meant as a shortcut to boilerplatish code. For example:: + + try: + instance = Instance.please.update(name='test-one', data=updated_values) + except Instance.DoesNotExist: + updated_values.update({'name': 'test-one'}) + instance = Instance(**updated_values) + instance.save() + + This pattern gets quite unwieldy as the number of fields in a model goes up. + The above example can be rewritten using **update_or_create()** like so:: + + instance, created = Instance.please.update_or_create(name='test-one', + defaults=updated_values) + """ + defaults = deepcopy(defaults or {}) + try: + instance = self.update(**kwargs) + except self.model.DoesNotExist: + defaults.update(kwargs) + instance = self.create(**defaults) + created = True + else: + created = False + return instance, created + + # List actions +
+ @clone +
[docs] def all(self, *args, **kwargs): + """ + Returns a copy of the current ``Manager`` with limit removed. + + Usage:: + + instances = Instance.please.all() + """ + self._limit = None + return self.list(*args, **kwargs) +
+ @clone +
[docs] def list(self, *args, **kwargs): + """ + Returns a copy of the current ``Manager`` containing objects that match the given lookup parameters. + + Usage:: + instance = Instance.please.list() + classes = Class.please.list(instance_name='test-one') + """ + self.method = 'GET' + self.endpoint = 'list' + self._filter(*args, **kwargs) + return self +
+ @clone +
[docs] def first(self, *args, **kwargs): + """ + Returns the first object matched by the lookup parameters or None, if there is no matching object. + + Usage:: + + instance = Instance.please.first() + classes = Class.please.first(instance_name='test-one') + """ + try: + self._limit = 1 + return self.list(*args, **kwargs)[0] + except KeyError: + return None +
+ @clone +
[docs] def page_size(self, value): + """ + Sets page size. + + Usage:: + + instances = Instance.please.page_size(20).all() + """ + if not value or not isinstance(value, six.integer_types): + raise SyncanoValueError('page_size value needs to be an int.') + + self.query['page_size'] = value + return self +
+ @clone +
[docs] def limit(self, value): + """ + Sets limit of returned objects. + + Usage:: + + instances = Instance.please.list().limit(10) + classes = Class.please.list(instance_name='test-one').limit(10) + """ + if not value or not isinstance(value, six.integer_types): + raise SyncanoValueError('Limit value needs to be an int.') + + self._limit = value + return self +
+ @clone +
[docs] 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 +
[docs] def raw(self): + """ + Disables serialization. ``request`` method will return raw Python types. + + Usage:: + + >>> instances = Instance.please.list().raw() + >>> instances + [{'description': 'new one', 'name': 'test-one'...}...] + """ + self._serialize = False + return self +
+ @clone +
[docs] 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 +
[docs] def using(self, connection): + """ + Connection juggling. + """ + # ConnectionMixin will validate this + self.connection = connection + return self + + # Other stuff +
+
[docs] def contribute_to_class(self, model, name): # pragma: no cover + setattr(model, name, ManagerDescriptor(self)) + + self.model = model + + 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 six.iteritems(serialized) + 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) + + 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 + + if too_much_properties and id_specified: + properties = ['id'] + + mapped_args = {k: v for k, v in zip(properties, args)} + self.properties.update(mapped_args) + self.properties.update(kwargs) + + def _clone(self): + # Maybe deepcopy ? + manager = self.__class__() + 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 + 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 + + return manager + +
[docs] 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 + + if not isinstance(data, dict): + raise SyncanoValueError('Unsupported data type.') + + properties = deepcopy(self.properties) + properties.update(data) + return model(**properties) if self._serialize else data +
+
[docs] 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 +
+
[docs] def request(self, method=None, path=None, **request): + """Internal method, which calls Syncano API and returns serialized data.""" + meta = self.model._meta + method = method or self.method + allowed_methods = meta.get_endpoint_methods(self.endpoint) + + if not path: + path, defaults = self._get_endpoint_properties() + + if method.lower() not in allowed_methods: + methods = ', '.join(allowed_methods) + raise SyncanoValueError('Unsupported request method "{0}" allowed are {1}.'.format(method, methods)) + + self.build_request(request) + + try: + response = self.connection.request(method, path, **request) + except SyncanoRequestError as e: + if e.status_code == 404: + obj_id = path.rsplit('/')[-2] + raise self.model.DoesNotExist("{} not found.".format(obj_id)) + raise + + if 'next' not in response and not self._template: + return self.serialize(response) + + return response +
+
[docs] def get_allowed_method(self, *methods): + meta = self.model._meta + allowed_methods = meta.get_endpoint_methods(self.endpoint) + + for method in methods: + if method.lower() in allowed_methods: + return method + + methods = ', '.join(methods) + raise SyncanoValueError('Unsupported request methods {0}.'.format(methods)) +
+
[docs] def iterator(self): + """Pagination handler""" + + response = self._get_response() + results = 0 + while True: + if self._template: + yield response + break + objects = response.get('objects') + next_url = response.get('next') + + for o in objects: + if self._limit and results >= self._limit: + break + + results += 1 + yield self.serialize(o) + + if not objects or not next_url or (self._limit and results >= self._limit): + break + + response = self.request(path=next_url) +
+ def _get_response(self): + return self.request() + + 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) + return self.model._meta.resolve_endpoint(self.endpoint, defaults), defaults + +
+
[docs]class ScriptManager(Manager): + """ + Custom :class:`~syncano.models.manager.Manager` + class for :class:`~syncano.models.base.Script` model. + """ + + @clone +
[docs] def run(self, *args, **kwargs): + payload = kwargs.pop('payload', {}) + + if not isinstance(payload, six.string_types): + payload = json.dumps(payload) + + self.method = 'POST' + self.endpoint = 'run' + self.data['payload'] = payload + self._filter(*args, **kwargs) + self._serialize = False + response = self.request() + return registry.ScriptTrace(**response) + +
+
[docs]class ScriptEndpointManager(Manager): + """ + Custom :class:`~syncano.models.manager.Manager` + class for :class:`~syncano.models.base.ScriptEndpoint` model. + """ + + @clone +
[docs] def run(self, *args, **kwargs): + payload = kwargs.pop('payload', {}) + + if not isinstance(payload, six.string_types): + payload = json.dumps(payload) + + self.method = 'POST' + self.endpoint = 'run' + self.data['payload'] = payload + self._filter(*args, **kwargs) + self._serialize = False + response = self.request() + + # Workaround for circular import + return registry.ScriptEndpointTrace(**response) + +
+
[docs]class ObjectManager(IncrementMixin, ArrayOperationsMixin, Manager): + """ + Custom :class:`~syncano.models.manager.Manager` + class for :class:`~syncano.models.base.Object` model. + """ + LOOKUP_SEPARATOR = '__' + ALLOWED_LOOKUPS = [ + 'gt', 'gte', 'lt', 'lte', + 'eq', 'neq', 'exists', 'in', 'nin', + 'near', 'is', 'contains', + 'startswith', 'endswith', + 'contains', 'istartswith', + 'iendswith', 'icontains', + 'ieq', 'near', + ] + + def __init__(self): + super(ObjectManager, self).__init__() + self._initial_response = None + +
[docs] def serialize(self, data, model=None): + model = model or self.model.get_subclass_model(**self.properties) + return super(ObjectManager, self).serialize(data, model) +
+ @clone +
[docs] 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' + self.query.update({ + 'include_count': True, + 'page_size': 0, + }) + response = self.request() + return response['objects_count'] +
+ @clone +
[docs] def with_count(self, page_size=20): + """ + 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(); + """ + query_data = { + 'include_count': True, + 'page_size': page_size, + } + + self.method = 'GET' + self.query.update(query_data) + response = self.request() + self._initial_response = response + return self, self._initial_response['objects_count'] +
+ @clone +
[docs] def filter(self, **kwargs): + """ + Special method just for data object :class:`~syncano.models.base.Object` model. + + Usage:: + + objects = Object.please.list('instance-name', 'class-name').filter(henryk__gte='hello') + """ + + 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) + + # 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) + + 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) + 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 + +
[docs] 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_response(self): + return self._initial_response or self.request() + + 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) + + 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 +
[docs] 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') + """ + model_fields = self._get_model_field_names() + self._validate_fields(model_fields, args) + self.query['fields'] = ','.join(args) + self.method = 'GET' + self.endpoint = 'list' + return self +
+ @clone +
[docs] 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') + """ + model_fields = self._get_model_field_names() + self._validate_fields(model_fields, args) + + fields = [f for f in model_fields if f not in args] + + self.query['fields'] = ','.join(fields) + self.method = 'GET' + self.endpoint = 'list' + return self +
+
[docs] def ordering(self, order=None): + raise AttributeError('Ordering not implemented. Use order_by instead.') +
+ @clone +
[docs] 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 +
+ def _clone(self): + manager = super(ObjectManager, self)._clone() + manager._initial_response = self._initial_response + return manager + +
+
[docs]class SchemaManager(object): + """ + Custom :class:`~syncano.models.manager.Manager` + class for :class:`~syncano.models.fields.SchemaFiled`. + """ + + def __init__(self, schema=None): + self.schema = schema or [] + + def __eq__(self, other): + if isinstance(other, SchemaManager): + return self.schema == other.schema + return NotImplemented + + def __str__(self): # pragma: no cover + return str(self.schema) + + def __repr__(self): # pragma: no cover + return '<SchemaManager>' + + def __getitem__(self, key): + if isinstance(key, int): + return self.schema[key] + + if isinstance(key, six.string_types): + for v in self.schema: + if v['name'] == key: + return v + + raise KeyError + + def __setitem__(self, key, value): + value = deepcopy(value) + value['name'] = key + self.remove(key) + self.add(value) + + def __delitem__(self, key): + self.remove(key) + + def __iter__(self): + return iter(self.schema) + + def __contains__(self, item): + if not self.schema: + return False + return item in self.schema + +
[docs] def set(self, value): + """Sets schema value.""" + self.schema = value +
+
[docs] def add(self, *objects): + """Adds multiple objects to schema.""" + self.schema.extend(objects) +
+
[docs] def remove(self, *names): + """Removes selected objects based on their names.""" + values = [v for v in self.schema if v['name'] not in names] + self.set(values) +
+
[docs] def clear(self): + """Sets empty schema.""" + self.set([]) +
+
[docs] def set_index(self, field, order=False, filter=False): + """Sets index on selected field. + + :type field: string + :param field: Name of schema field + + :type filter: bool + :param filter: Sets filter index on selected field + + :type order: bool + :param order: Sets order index on selected field + """ + if not order and not filter: + raise ValueError('Choose at least one index.') + + if order: + self[field]['order_index'] = True + + if filter: + self[field]['filter_index'] = True +
+
[docs] def set_order_index(self, field): + """Shortcut for ``set_index(field, order=True)``.""" + self.set_index(field, order=True) +
+
[docs] def set_filter_index(self, field): + """Shortcut for ``set_index(field, filter=True)``.""" + self.set_index(field, filter=True) +
+
[docs] def remove_index(self, field, order=False, filter=False): + """Removes index from selected field. + + :type field: string + :param field: Name of schema field + + :type filter: bool + :param filter: Removes filter index from selected field + + :type order: bool + :param order: Removes order index from selected field + """ + if not order and not filter: + raise ValueError('Choose at least one index.') + + if order and 'order_index' in self[field]: + del self[field]['order_index'] + + if filter and 'filter_index' in self[field]: + del self[field]['filter_index'] +
+
[docs] def remove_order_index(self, field): + """Shortcut for ``remove_index(field, order=True)``.""" + self.remove_index(field, order=True) +
+
[docs] def remove_filter_index(self, field): + """Shortcut for ``remove_index(field, filter=True)``.""" + self.remove_index(field, filter=True)
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/options.html b/_modules/syncano/models/options.html new file mode 100644 index 0000000..78ec0dc --- /dev/null +++ b/_modules/syncano/models/options.html @@ -0,0 +1,357 @@ + + + + + + + + + + syncano.models.options — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.options

+import re
+from bisect import bisect
+
+import six
+from syncano.connection import ConnectionMixin
+from syncano.exceptions import SyncanoValidationError, SyncanoValueError
+from syncano.models.registry import registry
+from syncano.utils import camelcase_to_underscore
+
+if six.PY3:
+    from urllib.parse import urljoin
+else:
+    from urlparse import urljoin
+
+
+
[docs]class Options(ConnectionMixin): + """Holds metadata related to model definition.""" + + def __init__(self, meta=None): + self.name = None + self.plural_name = None + self.related_name = None + + self.parent = None + self.parent_properties = [] + self.parent_resolved = False + + self.endpoints = {} + self.endpoint_fields = set() + + self.fields = [] + self.field_names = [] + + self.pk = None + + if meta: + meta_attrs = meta.__dict__.copy() + for name in meta.__dict__: + if name.startswith('_') or not hasattr(self, name): + del meta_attrs[name] + + for name, value in six.iteritems(meta_attrs): + setattr(self, name, value) + + self.build_properties() + +
[docs] def build_properties(self): + for name, endpoint in six.iteritems(self.endpoints): + if 'properties' not in endpoint: + properties = self.get_path_properties(endpoint['path']) + endpoint['properties'] = properties + self.endpoint_fields.update(properties) +
+
[docs] def contribute_to_class(self, cls, name): + if not self.name: + model_name = camelcase_to_underscore(cls.__name__) + self.name = model_name.replace('_', ' ').capitalize() + + if not self.plural_name: + self.plural_name = '{0}s'.format(self.name) + + if not self.related_name: + self.related_name = self.plural_name.replace(' ', '_').lower() + + if self.parent and isinstance(self.parent, six.string_types): + self.parent = registry.get_model_by_name(self.parent) + + self.resolve_parent_data() + + setattr(cls, name, self) +
+
[docs] def resolve_parent_data(self): + if not self.parent or self.parent_resolved: + return + + parent_meta = self.parent._meta + parent_name = parent_meta.name.replace(' ', '_').lower() + parent_endpoint = parent_meta.get_endpoint('detail') + prefix = parent_endpoint['path'] + + for prop in parent_endpoint.get('properties', []): + if prop in parent_meta.field_names and prop not in parent_meta.parent_properties: + prop = '{0}_{1}'.format(parent_name, prop) + self.parent_properties.append(prop) + + for old, new in zip(parent_endpoint['properties'], self.parent_properties): + prefix = prefix.replace( + '{{{0}}}'.format(old), + '{{{0}}}'.format(new) + ) + + for name, endpoint in six.iteritems(self.endpoints): + endpoint['properties'] = self.parent_properties + endpoint['properties'] + endpoint['path'] = urljoin(prefix, endpoint['path'].lstrip('/')) + self.endpoint_fields.update(endpoint['properties']) + + self.parent_resolved = True +
+
[docs] def add_field(self, field): + if field.name in self.field_names: + raise SyncanoValueError('Field "{0}" already defined'.format(field.name)) + + self.field_names.append(field.name) + self.fields.insert(bisect(self.fields, field), field) +
+
[docs] def get_field(self, field_name): + if not field_name: + raise SyncanoValueError('Field name is required.') + + if not isinstance(field_name, six.string_types): + raise SyncanoValueError('Field name should be a string.') + + for field in self.fields: + if field.name == field_name: + return field + + raise SyncanoValueError('Field "{0}" not found.'.format(field_name)) +
+
[docs] def get_endpoint(self, name): + if name not in self.endpoints: + raise SyncanoValueError('Invalid path name: "{0}".'.format(name)) + return self.endpoints[name] +
+
[docs] def get_endpoint_properties(self, name): + endpoint = self.get_endpoint(name) + return endpoint['properties'] +
+
[docs] def get_endpoint_path(self, name): + endpoint = self.get_endpoint(name) + return endpoint['path'] +
+
[docs] def get_endpoint_methods(self, name): + endpoint = self.get_endpoint(name) + return endpoint['methods'] +
+
[docs] def resolve_endpoint(self, endpoint_name, properties, http_method=None): + if http_method and not self.is_http_method_available(http_method, endpoint_name): + raise SyncanoValidationError( + 'HTTP method {0} not allowed for endpoint "{1}".'.format(http_method, endpoint_name) + ) + endpoint = self.get_endpoint(endpoint_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) +
+
[docs] 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 +
+
[docs] 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} +
+
[docs] def get_path_properties(self, path): + return re.findall('/{([^}]*)}', path)
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/push_notification.html b/_modules/syncano/models/push_notification.html new file mode 100644 index 0000000..925e869 --- /dev/null +++ b/_modules/syncano/models/push_notification.html @@ -0,0 +1,525 @@ + + + + + + + + + + syncano.models.push_notification — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.push_notification

+# -*- coding: utf-8 -*-
+
+from . import fields
+from .base import Instance, Model
+
+
+
[docs]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 = 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) + +
[docs] class Meta: + abstract = True +
+
[docs] def send_message(self, content): + """ + A method which allows to send message directly to the device; + :param contet: Message content structure - object like; + :return: + """ + 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 + +
+
[docs]class GCMDevice(DeviceBase, 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='example label', + registration_id=86152312314401555, + user_id=u.id, + device_id='10000000001', + ) + + gcm_device.save() + + Read: + gcm_device = GCMDevice.please.get(registration_id=86152312314401554) + + Delete: + gcm_device.delete() + + Update: + gcm_device.label = 'some new label' + gcm_device.save() + + """ + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'get', 'put', 'patch'], + 'path': '/push_notifications/gcm/devices/{registration_id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/push_notifications/gcm/devices/', + } + } + +
+
[docs]class APNSDevice(DeviceBase, Model): + """ + Model which handles the Apple Push Notification Server Device. + CORE supports only Create, Delete and Read; + + Usage:: + + Create a new Device: + apns_device = APNSDevice( + label='example label', + registration_id='4719084371920471208947120984731208947910827409128470912847120894', + user_id=u.id, + device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a', + ) + apns_device.save() + + Read: + apns_device = + APNSDevice.please.get(registration_id='4719084371920471208947120984731208947910827409128470912847120894') + + Delete: + apns_device.delete() + + Update: + apns_device.label = 'some new label' + apns_device.save() + + .. 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 + endpoints = { + 'detail': { + 'methods': ['delete', 'get', 'put', 'patch'], + 'path': '/push_notifications/apns/devices/{registration_id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/push_notifications/apns/devices/', + } + } + +
+
[docs]class MessageBase(object): + """ + Base abstract class for GCM and APNS Messages; + """ + + status = fields.StringField(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) + +
[docs] class Meta: + abstract = True + +
+
[docs]class GCMMessage(MessageBase, Model): + """ + Model which handles the Google Cloud Messaging Message. + Only creating and reading is allowed. + + Usage:: + + Create a new Message: + + message = GCMMessage( + content={ + 'registration_ids': [gcm_device.registration_id], # maximum 1000 elements; + 'data': { + 'example_data_one': 1, + 'example_data_two': 2, + } + } + ) + message.save() + + + 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; + + + 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. + + """ + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['delete', 'get'], + 'path': '/push_notifications/gcm/messages/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/push_notifications/gcm/messages/', + } + } + +
+
[docs]class APNSMessage(MessageBase, Model): + """ + Model which handles the Apple Push Notification Server Message. + Only creating and reading is allowed. + + Usage:: + + Create new Message: + apns_message = APNSMessage( + content={ + 'registration_ids': [gcm_device.registration_id], + 'aps': {'alert': 'test alert'}, + } + ) + + apns_message.save() + + 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; + + 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 + endpoints = { + 'detail': { + 'methods': ['delete', 'get'], + 'path': '/push_notifications/apns/messages/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/push_notifications/apns/messages/', + } + } + +
+
[docs]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) + +
[docs] 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/', + }, + } + +
+
[docs]class APNSConfig(Model): + """ + A model which stores information with APNS Push certificates; + + Usage:: + + Add (modify) new keys: + cert_file = open('cert_file.p12', 'rb') + 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) + +
[docs] 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/', + }, + }
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/registry.html b/_modules/syncano/models/registry.html new file mode 100644 index 0000000..e5bb5b7 --- /dev/null +++ b/_modules/syncano/models/registry.html @@ -0,0 +1,307 @@ + + + + + + + + + + syncano.models.registry — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.registry

+import re
+
+import six
+from syncano import logger
+
+
+class Registry(object):
+    """Models registry.
+
[docs] """ + def __init__(self, models=None): + self.models = models or {} + self.schemas = {} + self.patterns = [] + self._pending_lookups = {} + self.instance_name = None + self._default_connection = None + + def __str__(self): + return 'Registry: {0}'.format(', '.join(self.models)) + + def __unicode__(self): + return six.u(str(self)) + + def __iter__(self): + for name, model in six.iteritems(self.models): + yield model + + def get_model_patterns(self, cls): + patterns = [] +
[docs] for k, v in six.iteritems(cls._meta.endpoints): + pattern = '^{0}$'.format(v['path']) + for name in v.get('properties', []): + pattern = pattern.replace('{{{0}}}'.format(name), '([^/.]+)') + patterns.append((re.compile(pattern), cls)) + return patterns + + def get_model_by_path(self, path): + for pattern, cls in self.patterns:
+
[docs] if pattern.match(path): + return cls + raise LookupError('Invalid path: {0}'.format(path)) + + def get_model_by_name(self, name): + return self.models[name]
+
[docs] + def update(self, name, cls): + self.models[name] = cls
+
[docs] 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()) + + logger.debug('New model: %s, %s', name, related_name) + + def add(self, name, cls): +
+
[docs] if name not in self.models: + self.update(name, cls) + + if name in self._pending_lookups: + lookups = self._pending_lookups.pop(name) + for callback, args, kwargs in lookups: + callback(*args, **kwargs) + + return self + + def set_default_property(self, name, value): + for model in self:
+
[docs] if name not in model.__dict__: + continue + + 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)
+
[docs] + def set_used_instance(self, instance): + if instance and self.instance_name != instance or registry.instance_name is None:
+
[docs] self.set_default_instance(instance) # update the registry with last used instance; + self.instance_name = instance + + def clear_used_instance(self): + self.instance_name = None
+
[docs] self.set_default_instance(None) + + def get_schema(self, class_name): + return self.schemas.get(class_name)
+
[docs] + def set_schema(self, class_name, schema): + self.schemas[class_name] = schema
+
[docs] + def clear_schemas(self): + self.schemas = {}
+
[docs] + def set_default_connection(self, default_connection): + self._default_connection = default_connection
+
[docs] + @property + def connection(self):
+ if not self._default_connection: +
[docs] raise Exception('Set the default connection first.') + return self._default_connection + +registry = Registry() +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/models/traces.html b/_modules/syncano/models/traces.html new file mode 100644 index 0000000..a5a9b19 --- /dev/null +++ b/_modules/syncano/models/traces.html @@ -0,0 +1,347 @@ + + + + + + + + + + syncano.models.traces — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.models.traces

+from . import fields
+from .base import Model
+from .custom_response import CustomResponseMixin
+from .incentives import Schedule, Script, ScriptEndpoint, Trigger
+
+
+class ScriptTrace(CustomResponseMixin, Model):
+    """
+
[docs] :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': 'Processing', 'value': 'processing'}, + {'display_name': 'Pending', 'value': 'pending'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + 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) + + class Meta: + parent = Script + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/traces/', + } + } + + +class ScheduleTrace(Model): + """
+
[docs] :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'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + 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) + + class Meta: + parent = Schedule + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/traces/', + } + } + + +class TriggerTrace(Model): + """
+
[docs] :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.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) + + class Meta: + parent = Trigger + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/traces/', + } + } + + +class ScriptEndpointTrace(CustomResponseMixin, Model): + """
+
[docs] :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'}, + ) + + status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) + 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) + + class Meta: + parent = ScriptEndpoint + endpoints = { + 'detail': { + 'methods': ['get'], + 'path': '/traces/{id}/', + }, + 'list': { + 'methods': ['get', 'post'], + 'path': '/traces/', + } + } +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_modules/syncano/utils.html b/_modules/syncano/utils.html new file mode 100644 index 0000000..1d53a02 --- /dev/null +++ b/_modules/syncano/utils.html @@ -0,0 +1,256 @@ + + + + + + + + + + syncano.utils — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + +
+
+
+ +
+
+
+ +

Source code for syncano.utils

+import datetime
+import re
+from decimal import Decimal
+
+import six
+from slugify import slugify
+
+PROTECTED_TYPES = six.integer_types + (
+    type(None), float, Decimal, datetime.datetime,
+    datetime.date, datetime.time)
+
+
+
[docs]def camelcase_to_underscore(text): + """Converts camelcase text to underscore format.""" + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', text) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + +
+
[docs]def underscore_to_camelcase(text): + """Converts underscore text to camelcase format.""" + text = text.replace('_', ' ').title() + return text.replace(' ', '') + +
+
[docs]def get_class_name(*args): + """Generates safe class name based on provided arguments.""" + name = '_'.join(args) + name = slugify(name, separator='_') + return underscore_to_camelcase(name) + +
+
[docs]def force_text(s, encoding='utf-8', strings_only=False, errors='strict'): + if isinstance(s, six.text_type): + return s + + if strings_only and isinstance(s, PROTECTED_TYPES): + return s + + try: + if not isinstance(s, six.string_types): + if six.PY3: + if isinstance(s, bytes): + s = six.text_type(s, encoding, errors) + else: + s = six.text_type(s) + elif hasattr(s, '__unicode__'): + s = six.text_type(s) + else: + s = six.text_type(bytes(s), encoding, errors) + else: + s = s.decode(encoding, errors) + except UnicodeDecodeError: + if not isinstance(s, Exception): + raise + s = ' '.join(force_text(arg, encoding, strings_only, errors) + for arg in s) + return s
+
+ +
+ +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/source/custom_sockets.rst b/_sources/custom_sockets.txt similarity index 100% rename from docs/source/custom_sockets.rst rename to _sources/custom_sockets.txt diff --git a/docs/source/getting_started.rst b/_sources/getting_started.txt similarity index 100% rename from docs/source/getting_started.rst rename to _sources/getting_started.txt diff --git a/docs/source/index.rst b/_sources/index.txt similarity index 100% rename from docs/source/index.rst rename to _sources/index.txt diff --git a/docs/source/interacting.rst b/_sources/interacting.txt similarity index 100% rename from docs/source/interacting.rst rename to _sources/interacting.txt diff --git a/docs/source/refs/syncano.connection.rst b/_sources/refs/syncano.connection.txt similarity index 100% rename from docs/source/refs/syncano.connection.rst rename to _sources/refs/syncano.connection.txt diff --git a/docs/source/refs/syncano.exceptions.rst b/_sources/refs/syncano.exceptions.txt similarity index 100% rename from docs/source/refs/syncano.exceptions.rst rename to _sources/refs/syncano.exceptions.txt diff --git a/docs/source/refs/syncano.models.accounts.rst b/_sources/refs/syncano.models.accounts.txt similarity index 100% rename from docs/source/refs/syncano.models.accounts.rst rename to _sources/refs/syncano.models.accounts.txt diff --git a/docs/source/refs/syncano.models.archetypes.rst b/_sources/refs/syncano.models.archetypes.txt similarity index 100% rename from docs/source/refs/syncano.models.archetypes.rst rename to _sources/refs/syncano.models.archetypes.txt diff --git a/docs/source/refs/syncano.models.billing.rst b/_sources/refs/syncano.models.billing.txt similarity index 100% rename from docs/source/refs/syncano.models.billing.rst rename to _sources/refs/syncano.models.billing.txt diff --git a/docs/source/refs/syncano.models.channels.rst b/_sources/refs/syncano.models.channels.txt similarity index 100% rename from docs/source/refs/syncano.models.channels.rst rename to _sources/refs/syncano.models.channels.txt diff --git a/docs/source/refs/syncano.models.classes.rst b/_sources/refs/syncano.models.classes.txt similarity index 100% rename from docs/source/refs/syncano.models.classes.rst rename to _sources/refs/syncano.models.classes.txt diff --git a/docs/source/refs/syncano.models.custom_response.rst b/_sources/refs/syncano.models.custom_response.txt similarity index 100% rename from docs/source/refs/syncano.models.custom_response.rst rename to _sources/refs/syncano.models.custom_response.txt diff --git a/docs/source/refs/syncano.models.custom_sockets.rst b/_sources/refs/syncano.models.custom_sockets.txt similarity index 100% rename from docs/source/refs/syncano.models.custom_sockets.rst rename to _sources/refs/syncano.models.custom_sockets.txt diff --git a/docs/source/refs/syncano.models.custom_sockets_utils.rst b/_sources/refs/syncano.models.custom_sockets_utils.txt similarity index 100% rename from docs/source/refs/syncano.models.custom_sockets_utils.rst rename to _sources/refs/syncano.models.custom_sockets_utils.txt diff --git a/docs/source/refs/syncano.models.data_views.rst b/_sources/refs/syncano.models.data_views.txt similarity index 100% rename from docs/source/refs/syncano.models.data_views.rst rename to _sources/refs/syncano.models.data_views.txt diff --git a/docs/source/refs/syncano.models.fields.rst b/_sources/refs/syncano.models.fields.txt similarity index 100% rename from docs/source/refs/syncano.models.fields.rst rename to _sources/refs/syncano.models.fields.txt diff --git a/docs/source/refs/syncano.models.geo.rst b/_sources/refs/syncano.models.geo.txt similarity index 100% rename from docs/source/refs/syncano.models.geo.rst rename to _sources/refs/syncano.models.geo.txt diff --git a/docs/source/refs/syncano.models.hosting.rst b/_sources/refs/syncano.models.hosting.txt similarity index 100% rename from docs/source/refs/syncano.models.hosting.rst rename to _sources/refs/syncano.models.hosting.txt diff --git a/docs/source/refs/syncano.models.incentives.rst b/_sources/refs/syncano.models.incentives.txt similarity index 100% rename from docs/source/refs/syncano.models.incentives.rst rename to _sources/refs/syncano.models.incentives.txt diff --git a/docs/source/refs/syncano.models.instances.rst b/_sources/refs/syncano.models.instances.txt similarity index 100% rename from docs/source/refs/syncano.models.instances.rst rename to _sources/refs/syncano.models.instances.txt diff --git a/docs/source/refs/syncano.models.manager.rst b/_sources/refs/syncano.models.manager.txt similarity index 100% rename from docs/source/refs/syncano.models.manager.rst rename to _sources/refs/syncano.models.manager.txt diff --git a/docs/source/refs/syncano.models.options.rst b/_sources/refs/syncano.models.options.txt similarity index 100% rename from docs/source/refs/syncano.models.options.rst rename to _sources/refs/syncano.models.options.txt diff --git a/docs/source/refs/syncano.models.push_notification.rst b/_sources/refs/syncano.models.push_notification.txt similarity index 100% rename from docs/source/refs/syncano.models.push_notification.rst rename to _sources/refs/syncano.models.push_notification.txt diff --git a/docs/source/refs/syncano.models.registry.rst b/_sources/refs/syncano.models.registry.txt similarity index 100% rename from docs/source/refs/syncano.models.registry.rst rename to _sources/refs/syncano.models.registry.txt diff --git a/docs/source/refs/syncano.models.traces.rst b/_sources/refs/syncano.models.traces.txt similarity index 100% rename from docs/source/refs/syncano.models.traces.rst rename to _sources/refs/syncano.models.traces.txt diff --git a/docs/source/refs/syncano.models.rst b/_sources/refs/syncano.models.txt similarity index 100% rename from docs/source/refs/syncano.models.rst rename to _sources/refs/syncano.models.txt diff --git a/docs/source/refs/syncano.rst b/_sources/refs/syncano.txt similarity index 100% rename from docs/source/refs/syncano.rst rename to _sources/refs/syncano.txt diff --git a/docs/source/refs/syncano.utils.rst b/_sources/refs/syncano.utils.txt similarity index 100% rename from docs/source/refs/syncano.utils.rst rename to _sources/refs/syncano.utils.txt diff --git a/_static/ajax-loader.gif b/_static/ajax-loader.gif new file mode 100644 index 0000000..61faf8c Binary files /dev/null and b/_static/ajax-loader.gif differ diff --git a/_static/basic.css b/_static/basic.css new file mode 100644 index 0000000..967e36c --- /dev/null +++ b/_static/basic.css @@ -0,0 +1,537 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox input[type="text"] { + width: 170px; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + width: 30px; +} + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li div.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable dl, table.indextable dd { + margin-top: 0; + margin-bottom: 0; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- general body styles --------------------------------------------------- */ + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.field-list ul { + padding-left: 1em; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px 7px 0 7px; + background-color: #ffe; + width: 40%; + float: right; +} + +p.sidebar-title { + font-weight: bold; +} + +/* -- topics ---------------------------------------------------------------- */ + +div.topic { + border: 1px solid #ccc; + padding: 7px 7px 0 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +div.admonition dl { + margin-bottom: 0; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + border: 0; + border-collapse: collapse; +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +table.field-list td, table.field-list th { + border: 0 !important; +} + +table.footnote td, table.footnote th { + border: 0 !important; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +dl { + margin-bottom: 15px; +} + +dd p { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dt:target, .highlighted { + background-color: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.optional { + font-size: 1.3em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +td.linenos pre { + padding: 5px 0px; + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + margin-left: 0.5em; +} + +table.highlighttable td { + padding: 0 0.5em 0 0.5em; +} + +tt.descname { + background-color: transparent; + font-weight: bold; + font-size: 1.2em; +} + +tt.descclassname { + background-color: transparent; +} + +tt.xref, a tt { + background-color: transparent; + font-weight: bold; +} + +h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/_static/comment-bright.png b/_static/comment-bright.png new file mode 100644 index 0000000..551517b Binary files /dev/null and b/_static/comment-bright.png differ diff --git a/_static/comment-close.png b/_static/comment-close.png new file mode 100644 index 0000000..09b54be Binary files /dev/null and b/_static/comment-close.png differ diff --git a/_static/comment.png b/_static/comment.png new file mode 100644 index 0000000..92feb52 Binary files /dev/null and b/_static/comment.png differ diff --git a/_static/css/badge_only.css b/_static/css/badge_only.css new file mode 100644 index 0000000..4868a00 --- /dev/null +++ b/_static/css/badge_only.css @@ -0,0 +1 @@ +.fa:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#FontAwesome") format("svg")}.fa:before{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa{display:inline-block;text-decoration:inherit}li .fa{display:inline-block}li .fa-large:before,li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.fas li .fa{width:0.8em}ul.fas li .fa-large:before,ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before{content:"\f02d"}.icon-book:before{content:"\f02d"}.fa-caret-down:before{content:"\f0d7"}.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}} diff --git a/_static/css/theme.css b/_static/css/theme.css new file mode 100644 index 0000000..f595aab --- /dev/null +++ b/_static/css/theme.css @@ -0,0 +1,4 @@ +*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:20px 0;padding:0}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:0.2em 0;background:#ccc;color:#000;padding:0.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{html,body,section{background:none !important}*{box-shadow:none !important;text-shadow:none !important;filter:none !important;-ms-filter:none !important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.fa:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.0.3");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff?v=4.0.3") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.0.3") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.icon{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:0.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:0.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.rst-content .pull-left.admonition-title,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content dl dt .pull-left.headerlink,.pull-left.icon{margin-right:.3em}.fa.pull-right,.rst-content .pull-right.admonition-title,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content dl dt .pull-right.headerlink,.pull-right.icon{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=$rotation);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=$rotation);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=$rotation);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=$rotation);-webkit-transform:scale(-1, 1);-moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1);-o-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=$rotation);-webkit-transform:scale(1, -1);-moz-transform:scale(1, -1);-ms-transform:scale(1, -1);-o-transform:scale(1, -1);transform:scale(1, -1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before,.icon-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before,.icon-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:"\f057"}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before,.icon-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before,.icon-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:"\f0a8"}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before,.icon-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before,.wy-dropdown .caret:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before,.icon-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa,.rst-content .admonition-title,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink,.icon,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context{font-family:inherit}.fa:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.icon:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before{font-family:"FontAwesome";display:inline-block;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .fa,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink,a .icon{display:inline-block;text-decoration:inherit}.btn .fa,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.btn .icon,.nav .fa,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink,.nav .icon{display:inline}.btn .fa.fa-large,.btn .rst-content .fa-large.admonition-title,.rst-content .btn .fa-large.admonition-title,.btn .rst-content h1 .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.btn .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .btn .fa-large.headerlink,.btn .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .fa-large.admonition-title,.rst-content .nav .fa-large.admonition-title,.nav .rst-content h1 .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.nav .rst-content dl dt .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.nav .fa-large.icon{line-height:0.9em}.btn .fa.fa-spin,.btn .rst-content .fa-spin.admonition-title,.rst-content .btn .fa-spin.admonition-title,.btn .rst-content h1 .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.btn .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .btn .fa-spin.headerlink,.btn .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .fa-spin.admonition-title,.rst-content .nav .fa-spin.admonition-title,.nav .rst-content h1 .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.nav .rst-content dl dt .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.nav .fa-spin.icon{display:inline-block}.btn.fa:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before,.btn.icon:before{opacity:0.5;-webkit-transition:opacity 0.05s ease-in;-moz-transition:opacity 0.05s ease-in;transition:opacity 0.05s ease-in}.btn.fa:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.btn.icon:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before,.btn-mini .icon:before{font-size:14px;vertical-align:-15%}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:#6ab0de;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso{background:#f3f6f6}.wy-alert.wy-alert-neutral .wy-alert-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.seealso .admonition-title{color:#404040;background:#e1e4e5}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a{color:#2980b9}.wy-alert p:last-child,.rst-content .note p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.rst-content .seealso p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0px;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:60px;overflow:hidden;-webkit-transition:all 0.3s ease-in;-moz-transition:all 0.3s ease-in;transition:all 0.3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:60px}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);background-color:#27ae60;text-decoration:none;font-weight:normal;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset,0px -2px 0px 0px rgba(0,0,0,0.1) inset;outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all 0.1s linear;-moz-transition:all 0.1s linear;transition:all 0.1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:0px -1px 0px 0px rgba(0,0,0,0.05) inset,0px 2px 0px 0px rgba(0,0,0,0.1) inset;padding:8px 12px 6px 12px}.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9 !important}.btn-info:hover{background-color:#2e8ece !important}.btn-neutral{background-color:#f3f6f6 !important;color:#404040 !important}.btn-neutral:hover{background-color:#e5ebeb !important;color:#404040}.btn-neutral:visited{color:#404040 !important}.btn-success{background-color:#27ae60 !important}.btn-success:hover{background-color:#295 !important}.btn-danger{background-color:#e74c3c !important}.btn-danger:hover{background-color:#ea6153 !important}.btn-warning{background-color:#e67e22 !important}.btn-warning:hover{background-color:#e98b39 !important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f !important}.btn-link{background-color:transparent !important;color:#2980b9;box-shadow:none;border-color:transparent !important}.btn-link:hover{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:active{background-color:transparent !important;color:#409ad5 !important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:before,.wy-btn-group:after{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:solid 1px #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,0.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:solid 1px #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type="search"]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:0.5em 1em 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:0.5em}fieldset{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 0.3125em 0;color:#999;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full input[type="text"],.wy-control-group .wy-form-full input[type="password"],.wy-control-group .wy-form-full input[type="email"],.wy-control-group .wy-form-full input[type="url"],.wy-control-group .wy-form-full input[type="date"],.wy-control-group .wy-form-full input[type="month"],.wy-control-group .wy-form-full input[type="time"],.wy-control-group .wy-form-full input[type="datetime"],.wy-control-group .wy-form-full input[type="datetime-local"],.wy-control-group .wy-form-full input[type="week"],.wy-control-group .wy-form-full input[type="number"],.wy-control-group .wy-form-full input[type="search"],.wy-control-group .wy-form-full input[type="tel"],.wy-control-group .wy-form-full input[type="color"],.wy-control-group .wy-form-halves input[type="text"],.wy-control-group .wy-form-halves input[type="password"],.wy-control-group .wy-form-halves input[type="email"],.wy-control-group .wy-form-halves input[type="url"],.wy-control-group .wy-form-halves input[type="date"],.wy-control-group .wy-form-halves input[type="month"],.wy-control-group .wy-form-halves input[type="time"],.wy-control-group .wy-form-halves input[type="datetime"],.wy-control-group .wy-form-halves input[type="datetime-local"],.wy-control-group .wy-form-halves input[type="week"],.wy-control-group .wy-form-halves input[type="number"],.wy-control-group .wy-form-halves input[type="search"],.wy-control-group .wy-form-halves input[type="tel"],.wy-control-group .wy-form-halves input[type="color"],.wy-control-group .wy-form-thirds input[type="text"],.wy-control-group .wy-form-thirds input[type="password"],.wy-control-group .wy-form-thirds input[type="email"],.wy-control-group .wy-form-thirds input[type="url"],.wy-control-group .wy-form-thirds input[type="date"],.wy-control-group .wy-form-thirds input[type="month"],.wy-control-group .wy-form-thirds input[type="time"],.wy-control-group .wy-form-thirds input[type="datetime"],.wy-control-group .wy-form-thirds input[type="datetime-local"],.wy-control-group .wy-form-thirds input[type="week"],.wy-control-group .wy-form-thirds input[type="number"],.wy-control-group .wy-form-thirds input[type="search"],.wy-control-group .wy-form-thirds input[type="tel"],.wy-control-group .wy-form-thirds input[type="color"]{width:100%}.wy-control-group .wy-form-full{display:block;float:left;margin-right:2.35765%;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{display:block;float:left;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child{margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(2n+1){clear:left}.wy-control-group .wy-form-thirds{display:block;float:left;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child{margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control{margin:0.5em 0 0 0;font-size:90%}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:0.3em;color:#666;vertical-align:middle;font-size:90%}.wy-form-message{display:block;color:#ccc;font-size:70%;margin-top:0.3125em;font-style:italic}input{line-height:normal}input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;*overflow:visible}input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}input[type="datetime-local"]{padding:0.34375em 0.625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:0.3125em;*height:13px;*width:13px}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#333}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#f3f6f6;color:#cad2d3}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e74c3c}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%}select,textarea{padding:0.5em 0.625em;display:inline-block;border:1px solid #ccc;font-size:0.8em;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fff;color:#cad2d3;border-color:transparent}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{padding:6px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{border:solid 1px #e74c3c}.wy-control-group.wy-control-group-error textarea{border:solid 1px #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:0.5em 0.625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width: 480px){.wy-form button[type="submit"]{margin:0.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0.3em;display:block}.wy-form label{margin-bottom:0.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:0.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width: 768px){.tablet-hide{display:none}}@media screen and (max-width: 480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px;margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0 !important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22 !important}a.wy-text-warning:hover{color:#eb9950 !important}.wy-text-info{color:#2980b9 !important}a.wy-text-info:hover{color:#409ad5 !important}.wy-text-success{color:#27ae60 !important}a.wy-text-success:hover{color:#36d278 !important}.wy-text-danger{color:#e74c3c !important}a.wy-text-danger:hover{color:#ed7669 !important}.wy-text-neutral{color:#404040 !important}a.wy-text-neutral:hover{color:#595959 !important}h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif}p{line-height:24px;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}code,.rst-content tt{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:"Incosolata","Consolata","Monaco",monospace;color:#e74c3c;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li,article ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul,article ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,article ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,article ul li li li{list-style:square}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li,article ol li{list-style:decimal;margin-left:24px}.codeblock-example{border:1px solid #e1e4e5;border-bottom:none;padding:24px;padding-top:48px;font-weight:500;background:#fff;position:relative}.codeblock-example:after{content:"Example";position:absolute;top:0px;left:0px;background:#9b59b6;color:#fff;padding:6px 12px}.codeblock-example.prettyprint-example-only{border:1px solid #e1e4e5;margin-bottom:24px}.codeblock,pre.literal-block,.rst-content .literal-block,.rst-content pre.literal-block,div[class^='highlight']{border:1px solid #e1e4e5;padding:0px;overflow-x:auto;background:#fff;margin:1px 0 24px 0}.codeblock div[class^='highlight'],pre.literal-block div[class^='highlight'],.rst-content .literal-block div[class^='highlight'],div[class^='highlight'] div[class^='highlight']{border:none;background:none;margin:0}div[class^='highlight'] td.code{width:100%}.linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:"Incosolata","Consolata","Monaco",monospace;font-size:12px;line-height:1.5;color:#d9d9d9}div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;font-family:"Incosolata","Consolata","Monaco",monospace;font-size:12px;line-height:1.5;display:block;overflow:auto;color:#404040}@media print{.codeblock,pre.literal-block,.rst-content .literal-block,.rst-content pre.literal-block,div[class^='highlight'],div[class^='highlight'] pre{white-space:pre-wrap}}.hll{background-color:#ffc;margin:0 -12px;padding:0 12px;display:block}.c{color:#998;font-style:italic}.err{color:#a61717;background-color:#e3d2d2}.k{font-weight:bold}.o{font-weight:bold}.cm{color:#998;font-style:italic}.cp{color:#999;font-weight:bold}.c1{color:#998;font-style:italic}.cs{color:#999;font-weight:bold;font-style:italic}.gd{color:#000;background-color:#fdd}.gd .x{color:#000;background-color:#faa}.ge{font-style:italic}.gr{color:#a00}.gh{color:#999}.gi{color:#000;background-color:#dfd}.gi .x{color:#000;background-color:#afa}.go{color:#888}.gp{color:#555}.gs{font-weight:bold}.gu{color:purple;font-weight:bold}.gt{color:#a00}.kc{font-weight:bold}.kd{font-weight:bold}.kn{font-weight:bold}.kp{font-weight:bold}.kr{font-weight:bold}.kt{color:#458;font-weight:bold}.m{color:#099}.s{color:#d14}.n{color:#333}.na{color:teal}.nb{color:#0086b3}.nc{color:#458;font-weight:bold}.no{color:teal}.ni{color:purple}.ne{color:#900;font-weight:bold}.nf{color:#900;font-weight:bold}.nn{color:#555}.nt{color:navy}.nv{color:teal}.ow{font-weight:bold}.w{color:#bbb}.mf{color:#099}.mh{color:#099}.mi{color:#099}.mo{color:#099}.sb{color:#d14}.sc{color:#d14}.sd{color:#d14}.s2{color:#d14}.se{color:#d14}.sh{color:#d14}.si{color:#d14}.sx{color:#d14}.sr{color:#009926}.s1{color:#d14}.ss{color:#990073}.bp{color:#999}.vc{color:teal}.vg{color:teal}.vi{color:teal}.il{color:#099}.gc{color:#999;background-color:#eaf2f5}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical header{height:32px;display:inline-block;line-height:32px;padding:0 1.618em;display:block;font-weight:bold;text-transform:uppercase;font-size:80%;color:#2980b9;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:0.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:0.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.toctree-l2.current>a{background:#c9c9c9;padding:0.4045em 2.427em}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical .local-toc li ul{display:block}.wy-menu-vertical li ul li a{margin-bottom:0;color:#b3b3b3;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:0.4045em 1.618em;display:block;position:relative;font-size:90%;color:#b3b3b3}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-side-nav-search{z-index:200;background-color:#2980b9;text-align:center;padding:0.809em;display:block;color:#fcfcfc;margin-bottom:0.809em}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto 0.809em auto;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:0.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all 0.2s ease-in;-moz-transition:all 0.2s ease-in;transition:all 0.2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:left repeat-y #fcfcfc;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC);background-size:300px 1px}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:absolute;top:0;left:0;width:300px;overflow:hidden;min-height:100%;background:#343131;z-index:200}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:0.4045em 0.809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:#999}footer p{margin-bottom:12px}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}@media screen and (max-width: 768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width: 1400px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}nav.stickynav{position:fixed;top:0}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}}.rst-content img{max-width:100%;height:auto !important}.rst-content div.figure{margin-bottom:24px}.rst-content div.figure.align-center{text-align:center}.rst-content .section>img{margin-bottom:24px}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .line-block{margin-left:24px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto;display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink{display:none;visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after{visibility:visible;content:"\f0c1";font-family:FontAwesome;display:inline-block}.rst-content h1:hover .headerlink,.rst-content h2:hover .headerlink,.rst-content h3:hover .headerlink,.rst-content h4:hover .headerlink,.rst-content h5:hover .headerlink,.rst-content h6:hover .headerlink,.rst-content dl dt:hover .headerlink{display:inline-block}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:super;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:#999}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none;padding-top:5px}.rst-content table.field-list td>strong{display:inline-block;margin-top:3px}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left;padding-left:0}.rst-content tt{color:#000}.rst-content tt big,.rst-content tt em{font-size:100% !important;line-height:normal}.rst-content tt .xref,a .rst-content tt{font-weight:bold}.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:inline-block;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:gray}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}@media screen and (max-width: 480px){.rst-content .sidebar{width:100%}}span[id*='MathJax-Span']{color:#404040} diff --git a/_static/doctools.js b/_static/doctools.js new file mode 100644 index 0000000..c5455c9 --- /dev/null +++ b/_static/doctools.js @@ -0,0 +1,238 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for all documentation. + * + * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + +/** + * make the code below compatible with browsers without + * an installed firebug like debugger +if (!window.console || !console.firebug) { + var names = ["log", "debug", "info", "warn", "error", "assert", "dir", + "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", + "profile", "profileEnd"]; + window.console = {}; + for (var i = 0; i < names.length; ++i) + window.console[names[i]] = function() {}; +} + */ + +/** + * small helper function to urldecode strings + */ +jQuery.urldecode = function(x) { + return decodeURIComponent(x).replace(/\+/g, ' '); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s == 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node) { + if (node.nodeType == 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && !jQuery(node.parentNode).hasClass(className)) { + var span = document.createElement("span"); + span.className = className; + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this); + }); + } + } + return this.each(function() { + highlight(this); + }); +}; + +/** + * Small JavaScript module for the documentation. + */ +var Documentation = { + + init : function() { + this.fixFirefoxAnchorBug(); + this.highlightSearchWords(); + this.initIndexTable(); + }, + + /** + * i18n support + */ + TRANSLATIONS : {}, + PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, + LOCALE : 'unknown', + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext : function(string) { + var translated = Documentation.TRANSLATIONS[string]; + if (typeof translated == 'undefined') + return string; + return (typeof translated == 'string') ? translated : translated[0]; + }, + + ngettext : function(singular, plural, n) { + var translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated == 'undefined') + return (n == 1) ? singular : plural; + return translated[Documentation.PLURALEXPR(n)]; + }, + + addTranslations : function(catalog) { + for (var key in catalog.messages) + this.TRANSLATIONS[key] = catalog.messages[key]; + this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); + this.LOCALE = catalog.locale; + }, + + /** + * add context elements like header anchor links + */ + addContextElements : function() { + $('div[id] > :header:first').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this headline')). + appendTo(this); + }); + $('dt[id]').each(function() { + $('\u00B6'). + attr('href', '#' + this.id). + attr('title', _('Permalink to this definition')). + appendTo(this); + }); + }, + + /** + * workaround a firefox stupidity + */ + fixFirefoxAnchorBug : function() { + if (document.location.hash && $.browser.mozilla) + window.setTimeout(function() { + document.location.href += ''; + }, 10); + }, + + /** + * highlight the search words provided in the url in the text + */ + highlightSearchWords : function() { + var params = $.getQueryParameters(); + var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; + if (terms.length) { + var body = $('div.body'); + if (!body.length) { + body = $('body'); + } + window.setTimeout(function() { + $.each(terms, function() { + body.highlightText(this.toLowerCase(), 'highlighted'); + }); + }, 10); + $('') + .appendTo($('#searchbox')); + } + }, + + /** + * init the domain index toggle buttons + */ + initIndexTable : function() { + var togglers = $('img.toggler').click(function() { + var src = $(this).attr('src'); + var idnum = $(this).attr('id').substr(7); + $('tr.cg-' + idnum).toggle(); + if (src.substr(-9) == 'minus.png') + $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); + else + $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); + }).css('display', ''); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { + togglers.click(); + } + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords : function() { + $('#searchbox .highlight-link').fadeOut(300); + $('span.highlighted').removeClass('highlighted'); + }, + + /** + * make the url absolute + */ + makeURL : function(relativeURL) { + return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + }, + + /** + * get the current relative url + */ + getCurrentURL : function() { + var path = document.location.pathname; + var parts = path.split(/\//); + $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { + if (this == '..') + parts.pop(); + }); + var url = parts.join('/'); + return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + } +}; + +// quick alias for translations +_ = Documentation.gettext; + +$(document).ready(function() { + Documentation.init(); +}); diff --git a/_static/down-pressed.png b/_static/down-pressed.png new file mode 100644 index 0000000..6f7ad78 Binary files /dev/null and b/_static/down-pressed.png differ diff --git a/_static/down.png b/_static/down.png new file mode 100644 index 0000000..3003a88 Binary files /dev/null and b/_static/down.png differ diff --git a/_static/file.png b/_static/file.png new file mode 100644 index 0000000..d18082e Binary files /dev/null and b/_static/file.png differ diff --git a/_static/fonts/fontawesome-webfont.eot b/_static/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..7c79c6a Binary files /dev/null and b/_static/fonts/fontawesome-webfont.eot differ diff --git a/_static/fonts/fontawesome-webfont.svg b/_static/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..45fdf33 --- /dev/null +++ b/_static/fonts/fontawesome-webfont.svg @@ -0,0 +1,414 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_static/fonts/fontawesome-webfont.ttf b/_static/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..e89738d Binary files /dev/null and b/_static/fonts/fontawesome-webfont.ttf differ diff --git a/_static/fonts/fontawesome-webfont.woff b/_static/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..8c1748a Binary files /dev/null and b/_static/fonts/fontawesome-webfont.woff differ diff --git a/_static/jquery.js b/_static/jquery.js new file mode 100644 index 0000000..83589da --- /dev/null +++ b/_static/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v1.8.3 jquery.com | jquery.org/license */ +(function(e,t){function _(e){var t=M[e]={};return v.each(e.split(y),function(e,n){t[n]=!0}),t}function H(e,n,r){if(r===t&&e.nodeType===1){var i="data-"+n.replace(P,"-$1").toLowerCase();r=e.getAttribute(i);if(typeof r=="string"){try{r=r==="true"?!0:r==="false"?!1:r==="null"?null:+r+""===r?+r:D.test(r)?v.parseJSON(r):r}catch(s){}v.data(e,n,r)}else r=t}return r}function B(e){var t;for(t in e){if(t==="data"&&v.isEmptyObject(e[t]))continue;if(t!=="toJSON")return!1}return!0}function et(){return!1}function tt(){return!0}function ut(e){return!e||!e.parentNode||e.parentNode.nodeType===11}function at(e,t){do e=e[t];while(e&&e.nodeType!==1);return e}function ft(e,t,n){t=t||0;if(v.isFunction(t))return v.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return v.grep(e,function(e,r){return e===t===n});if(typeof t=="string"){var r=v.grep(e,function(e){return e.nodeType===1});if(it.test(t))return v.filter(t,r,!n);t=v.filter(t,r)}return v.grep(e,function(e,r){return v.inArray(e,t)>=0===n})}function lt(e){var t=ct.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function At(e,t){if(t.nodeType!==1||!v.hasData(e))return;var n,r,i,s=v._data(e),o=v._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;r").appendTo(i.body),n=t.css("display");t.remove();if(n==="none"||n===""){Pt=i.body.appendChild(Pt||v.extend(i.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!Ht||!Pt.createElement)Ht=(Pt.contentWindow||Pt.contentDocument).document,Ht.write(""),Ht.close();t=Ht.body.appendChild(Ht.createElement(e)),n=Dt(t,"display"),i.body.removeChild(Pt)}return Wt[e]=n,n}function fn(e,t,n,r){var i;if(v.isArray(t))v.each(t,function(t,i){n||sn.test(e)?r(e,i):fn(e+"["+(typeof i=="object"?t:"")+"]",i,n,r)});else if(!n&&v.type(t)==="object")for(i in t)fn(e+"["+i+"]",t[i],n,r);else r(e,t)}function Cn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var r,i,s,o=t.toLowerCase().split(y),u=0,a=o.length;if(v.isFunction(n))for(;u)[^>]*$|#([\w\-]*)$)/,E=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,S=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,T=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,N=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,C=/^-ms-/,k=/-([\da-z])/gi,L=function(e,t){return(t+"").toUpperCase()},A=function(){i.addEventListener?(i.removeEventListener("DOMContentLoaded",A,!1),v.ready()):i.readyState==="complete"&&(i.detachEvent("onreadystatechange",A),v.ready())},O={};v.fn=v.prototype={constructor:v,init:function(e,n,r){var s,o,u,a;if(!e)return this;if(e.nodeType)return this.context=this[0]=e,this.length=1,this;if(typeof e=="string"){e.charAt(0)==="<"&&e.charAt(e.length-1)===">"&&e.length>=3?s=[null,e,null]:s=w.exec(e);if(s&&(s[1]||!n)){if(s[1])return n=n instanceof v?n[0]:n,a=n&&n.nodeType?n.ownerDocument||n:i,e=v.parseHTML(s[1],a,!0),E.test(s[1])&&v.isPlainObject(n)&&this.attr.call(e,n,!0),v.merge(this,e);o=i.getElementById(s[2]);if(o&&o.parentNode){if(o.id!==s[2])return r.find(e);this.length=1,this[0]=o}return this.context=i,this.selector=e,this}return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e)}return v.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),v.makeArray(e,this))},selector:"",jquery:"1.8.3",length:0,size:function(){return this.length},toArray:function(){return l.call(this)},get:function(e){return e==null?this.toArray():e<0?this[this.length+e]:this[e]},pushStack:function(e,t,n){var r=v.merge(this.constructor(),e);return r.prevObject=this,r.context=this.context,t==="find"?r.selector=this.selector+(this.selector?" ":"")+n:t&&(r.selector=this.selector+"."+t+"("+n+")"),r},each:function(e,t){return v.each(this,e,t)},ready:function(e){return v.ready.promise().done(e),this},eq:function(e){return e=+e,e===-1?this.slice(e):this.slice(e,e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(l.apply(this,arguments),"slice",l.call(arguments).join(","))},map:function(e){return this.pushStack(v.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:[].sort,splice:[].splice},v.fn.init.prototype=v.fn,v.extend=v.fn.extend=function(){var e,n,r,i,s,o,u=arguments[0]||{},a=1,f=arguments.length,l=!1;typeof u=="boolean"&&(l=u,u=arguments[1]||{},a=2),typeof u!="object"&&!v.isFunction(u)&&(u={}),f===a&&(u=this,--a);for(;a0)return;r.resolveWith(i,[v]),v.fn.trigger&&v(i).trigger("ready").off("ready")},isFunction:function(e){return v.type(e)==="function"},isArray:Array.isArray||function(e){return v.type(e)==="array"},isWindow:function(e){return e!=null&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return e==null?String(e):O[h.call(e)]||"object"},isPlainObject:function(e){if(!e||v.type(e)!=="object"||e.nodeType||v.isWindow(e))return!1;try{if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||p.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw new Error(e)},parseHTML:function(e,t,n){var r;return!e||typeof e!="string"?null:(typeof t=="boolean"&&(n=t,t=0),t=t||i,(r=E.exec(e))?[t.createElement(r[1])]:(r=v.buildFragment([e],t,n?null:[]),v.merge([],(r.cacheable?v.clone(r.fragment):r.fragment).childNodes)))},parseJSON:function(t){if(!t||typeof t!="string")return null;t=v.trim(t);if(e.JSON&&e.JSON.parse)return e.JSON.parse(t);if(S.test(t.replace(T,"@").replace(N,"]").replace(x,"")))return(new Function("return "+t))();v.error("Invalid JSON: "+t)},parseXML:function(n){var r,i;if(!n||typeof n!="string")return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(s){r=t}return(!r||!r.documentElement||r.getElementsByTagName("parsererror").length)&&v.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&g.test(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(C,"ms-").replace(k,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,n,r){var i,s=0,o=e.length,u=o===t||v.isFunction(e);if(r){if(u){for(i in e)if(n.apply(e[i],r)===!1)break}else for(;s0&&e[0]&&e[a-1]||a===0||v.isArray(e));if(f)for(;u-1)a.splice(n,1),i&&(n<=o&&o--,n<=u&&u--)}),this},has:function(e){return v.inArray(e,a)>-1},empty:function(){return a=[],this},disable:function(){return a=f=n=t,this},disabled:function(){return!a},lock:function(){return f=t,n||c.disable(),this},locked:function(){return!f},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],a&&(!r||f)&&(i?f.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},v.extend({Deferred:function(e){var t=[["resolve","done",v.Callbacks("once memory"),"resolved"],["reject","fail",v.Callbacks("once memory"),"rejected"],["notify","progress",v.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return v.Deferred(function(n){v.each(t,function(t,r){var s=r[0],o=e[t];i[r[1]](v.isFunction(o)?function(){var e=o.apply(this,arguments);e&&v.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===i?n:this,[e])}:n[s])}),e=null}).promise()},promise:function(e){return e!=null?v.extend(e,r):r}},i={};return r.pipe=r.then,v.each(t,function(e,s){var o=s[2],u=s[3];r[s[1]]=o.add,u&&o.add(function(){n=u},t[e^1][2].disable,t[2][2].lock),i[s[0]]=o.fire,i[s[0]+"With"]=o.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=l.call(arguments),r=n.length,i=r!==1||e&&v.isFunction(e.promise)?r:0,s=i===1?e:v.Deferred(),o=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?l.call(arguments):r,n===u?s.notifyWith(t,n):--i||s.resolveWith(t,n)}},u,a,f;if(r>1){u=new Array(r),a=new Array(r),f=new Array(r);for(;t
a",n=p.getElementsByTagName("*"),r=p.getElementsByTagName("a")[0];if(!n||!r||!n.length)return{};s=i.createElement("select"),o=s.appendChild(i.createElement("option")),u=p.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:r.getAttribute("href")==="/a",opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:u.value==="on",optSelected:o.selected,getSetAttribute:p.className!=="t",enctype:!!i.createElement("form").enctype,html5Clone:i.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:i.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},u.checked=!0,t.noCloneChecked=u.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!o.disabled;try{delete p.test}catch(d){t.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",h=function(){t.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick"),p.detachEvent("onclick",h)),u=i.createElement("input"),u.value="t",u.setAttribute("type","radio"),t.radioValue=u.value==="t",u.setAttribute("checked","checked"),u.setAttribute("name","t"),p.appendChild(u),a=i.createDocumentFragment(),a.appendChild(p.lastChild),t.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,t.appendChecked=u.checked,a.removeChild(u),a.appendChild(p);if(p.attachEvent)for(l in{submit:!0,change:!0,focusin:!0})f="on"+l,c=f in p,c||(p.setAttribute(f,"return;"),c=typeof p[f]=="function"),t[l+"Bubbles"]=c;return v(function(){var n,r,s,o,u="padding:0;margin:0;border:0;display:block;overflow:hidden;",a=i.getElementsByTagName("body")[0];if(!a)return;n=i.createElement("div"),n.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",a.insertBefore(n,a.firstChild),r=i.createElement("div"),n.appendChild(r),r.innerHTML="
t
",s=r.getElementsByTagName("td"),s[0].style.cssText="padding:0;margin:0;border:0;display:none",c=s[0].offsetHeight===0,s[0].style.display="",s[1].style.display="none",t.reliableHiddenOffsets=c&&s[0].offsetHeight===0,r.innerHTML="",r.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=r.offsetWidth===4,t.doesNotIncludeMarginInBodyOffset=a.offsetTop!==1,e.getComputedStyle&&(t.pixelPosition=(e.getComputedStyle(r,null)||{}).top!=="1%",t.boxSizingReliable=(e.getComputedStyle(r,null)||{width:"4px"}).width==="4px",o=i.createElement("div"),o.style.cssText=r.style.cssText=u,o.style.marginRight=o.style.width="0",r.style.width="1px",r.appendChild(o),t.reliableMarginRight=!parseFloat((e.getComputedStyle(o,null)||{}).marginRight)),typeof r.style.zoom!="undefined"&&(r.innerHTML="",r.style.cssText=u+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=r.offsetWidth===3,r.style.display="block",r.style.overflow="visible",r.innerHTML="
",r.firstChild.style.width="5px",t.shrinkWrapBlocks=r.offsetWidth!==3,n.style.zoom=1),a.removeChild(n),n=r=s=o=null}),a.removeChild(p),n=r=s=o=u=a=p=null,t}();var D=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;v.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(v.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?v.cache[e[v.expando]]:e[v.expando],!!e&&!B(e)},data:function(e,n,r,i){if(!v.acceptData(e))return;var s,o,u=v.expando,a=typeof n=="string",f=e.nodeType,l=f?v.cache:e,c=f?e[u]:e[u]&&u;if((!c||!l[c]||!i&&!l[c].data)&&a&&r===t)return;c||(f?e[u]=c=v.deletedIds.pop()||v.guid++:c=u),l[c]||(l[c]={},f||(l[c].toJSON=v.noop));if(typeof n=="object"||typeof n=="function")i?l[c]=v.extend(l[c],n):l[c].data=v.extend(l[c].data,n);return s=l[c],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[v.camelCase(n)]=r),a?(o=s[n],o==null&&(o=s[v.camelCase(n)])):o=s,o},removeData:function(e,t,n){if(!v.acceptData(e))return;var r,i,s,o=e.nodeType,u=o?v.cache:e,a=o?e[v.expando]:v.expando;if(!u[a])return;if(t){r=n?u[a]:u[a].data;if(r){v.isArray(t)||(t in r?t=[t]:(t=v.camelCase(t),t in r?t=[t]:t=t.split(" ")));for(i=0,s=t.length;i1,null,!1))},removeData:function(e){return this.each(function(){v.removeData(this,e)})}}),v.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=v._data(e,t),n&&(!r||v.isArray(n)?r=v._data(e,t,v.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=v.queue(e,t),r=n.length,i=n.shift(),s=v._queueHooks(e,t),o=function(){v.dequeue(e,t)};i==="inprogress"&&(i=n.shift(),r--),i&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,i.call(e,o,s)),!r&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return v._data(e,n)||v._data(e,n,{empty:v.Callbacks("once memory").add(function(){v.removeData(e,t+"queue",!0),v.removeData(e,n,!0)})})}}),v.fn.extend({queue:function(e,n){var r=2;return typeof e!="string"&&(n=e,e="fx",r--),arguments.length1)},removeAttr:function(e){return this.each(function(){v.removeAttr(this,e)})},prop:function(e,t){return v.access(this,v.prop,e,t,arguments.length>1)},removeProp:function(e){return e=v.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,s,o,u;if(v.isFunction(e))return this.each(function(t){v(this).addClass(e.call(this,t,this.className))});if(e&&typeof e=="string"){t=e.split(y);for(n=0,r=this.length;n=0)r=r.replace(" "+n[s]+" "," ");i.className=e?v.trim(r):""}}}return this},toggleClass:function(e,t){var n=typeof e,r=typeof t=="boolean";return v.isFunction(e)?this.each(function(n){v(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if(n==="string"){var i,s=0,o=v(this),u=t,a=e.split(y);while(i=a[s++])u=r?u:!o.hasClass(i),o[u?"addClass":"removeClass"](i)}else if(n==="undefined"||n==="boolean")this.className&&v._data(this,"__className__",this.className),this.className=this.className||e===!1?"":v._data(this,"__className__")||""})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;n=0)return!0;return!1},val:function(e){var n,r,i,s=this[0];if(!arguments.length){if(s)return n=v.valHooks[s.type]||v.valHooks[s.nodeName.toLowerCase()],n&&"get"in n&&(r=n.get(s,"value"))!==t?r:(r=s.value,typeof r=="string"?r.replace(R,""):r==null?"":r);return}return i=v.isFunction(e),this.each(function(r){var s,o=v(this);if(this.nodeType!==1)return;i?s=e.call(this,r,o.val()):s=e,s==null?s="":typeof s=="number"?s+="":v.isArray(s)&&(s=v.map(s,function(e){return e==null?"":e+""})),n=v.valHooks[this.type]||v.valHooks[this.nodeName.toLowerCase()];if(!n||!("set"in n)||n.set(this,s,"value")===t)this.value=s})}}),v.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,s=e.type==="select-one"||i<0,o=s?null:[],u=s?i+1:r.length,a=i<0?u:s?i:0;for(;a=0}),n.length||(e.selectedIndex=-1),n}}},attrFn:{},attr:function(e,n,r,i){var s,o,u,a=e.nodeType;if(!e||a===3||a===8||a===2)return;if(i&&v.isFunction(v.fn[n]))return v(e)[n](r);if(typeof e.getAttribute=="undefined")return v.prop(e,n,r);u=a!==1||!v.isXMLDoc(e),u&&(n=n.toLowerCase(),o=v.attrHooks[n]||(X.test(n)?F:j));if(r!==t){if(r===null){v.removeAttr(e,n);return}return o&&"set"in o&&u&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r)}return o&&"get"in o&&u&&(s=o.get(e,n))!==null?s:(s=e.getAttribute(n),s===null?t:s)},removeAttr:function(e,t){var n,r,i,s,o=0;if(t&&e.nodeType===1){r=t.split(y);for(;o=0}})});var $=/^(?:textarea|input|select)$/i,J=/^([^\.]*|)(?:\.(.+)|)$/,K=/(?:^|\s)hover(\.\S+|)\b/,Q=/^key/,G=/^(?:mouse|contextmenu)|click/,Y=/^(?:focusinfocus|focusoutblur)$/,Z=function(e){return v.event.special.hover?e:e.replace(K,"mouseenter$1 mouseleave$1")};v.event={add:function(e,n,r,i,s){var o,u,a,f,l,c,h,p,d,m,g;if(e.nodeType===3||e.nodeType===8||!n||!r||!(o=v._data(e)))return;r.handler&&(d=r,r=d.handler,s=d.selector),r.guid||(r.guid=v.guid++),a=o.events,a||(o.events=a={}),u=o.handle,u||(o.handle=u=function(e){return typeof v=="undefined"||!!e&&v.event.triggered===e.type?t:v.event.dispatch.apply(u.elem,arguments)},u.elem=e),n=v.trim(Z(n)).split(" ");for(f=0;f=0&&(y=y.slice(0,-1),a=!0),y.indexOf(".")>=0&&(b=y.split("."),y=b.shift(),b.sort());if((!s||v.event.customEvent[y])&&!v.event.global[y])return;n=typeof n=="object"?n[v.expando]?n:new v.Event(y,n):new v.Event(y),n.type=y,n.isTrigger=!0,n.exclusive=a,n.namespace=b.join("."),n.namespace_re=n.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,h=y.indexOf(":")<0?"on"+y:"";if(!s){u=v.cache;for(f in u)u[f].events&&u[f].events[y]&&v.event.trigger(n,r,u[f].handle.elem,!0);return}n.result=t,n.target||(n.target=s),r=r!=null?v.makeArray(r):[],r.unshift(n),p=v.event.special[y]||{};if(p.trigger&&p.trigger.apply(s,r)===!1)return;m=[[s,p.bindType||y]];if(!o&&!p.noBubble&&!v.isWindow(s)){g=p.delegateType||y,l=Y.test(g+y)?s:s.parentNode;for(c=s;l;l=l.parentNode)m.push([l,g]),c=l;c===(s.ownerDocument||i)&&m.push([c.defaultView||c.parentWindow||e,g])}for(f=0;f=0:v.find(h,this,null,[s]).length),u[h]&&f.push(c);f.length&&w.push({elem:s,matches:f})}d.length>m&&w.push({elem:this,matches:d.slice(m)});for(r=0;r0?this.on(t,null,e,n):this.trigger(t)},Q.test(t)&&(v.event.fixHooks[t]=v.event.keyHooks),G.test(t)&&(v.event.fixHooks[t]=v.event.mouseHooks)}),function(e,t){function nt(e,t,n,r){n=n||[],t=t||g;var i,s,a,f,l=t.nodeType;if(!e||typeof e!="string")return n;if(l!==1&&l!==9)return[];a=o(t);if(!a&&!r)if(i=R.exec(e))if(f=i[1]){if(l===9){s=t.getElementById(f);if(!s||!s.parentNode)return n;if(s.id===f)return n.push(s),n}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(f))&&u(t,s)&&s.id===f)return n.push(s),n}else{if(i[2])return S.apply(n,x.call(t.getElementsByTagName(e),0)),n;if((f=i[3])&&Z&&t.getElementsByClassName)return S.apply(n,x.call(t.getElementsByClassName(f),0)),n}return vt(e.replace(j,"$1"),t,n,r,a)}function rt(e){return function(t){var n=t.nodeName.toLowerCase();return n==="input"&&t.type===e}}function it(e){return function(t){var n=t.nodeName.toLowerCase();return(n==="input"||n==="button")&&t.type===e}}function st(e){return N(function(t){return t=+t,N(function(n,r){var i,s=e([],n.length,t),o=s.length;while(o--)n[i=s[o]]&&(n[i]=!(r[i]=n[i]))})})}function ot(e,t,n){if(e===t)return n;var r=e.nextSibling;while(r){if(r===t)return-1;r=r.nextSibling}return 1}function ut(e,t){var n,r,s,o,u,a,f,l=L[d][e+" "];if(l)return t?0:l.slice(0);u=e,a=[],f=i.preFilter;while(u){if(!n||(r=F.exec(u)))r&&(u=u.slice(r[0].length)||u),a.push(s=[]);n=!1;if(r=I.exec(u))s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=r[0].replace(j," ");for(o in i.filter)(r=J[o].exec(u))&&(!f[o]||(r=f[o](r)))&&(s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=o,n.matches=r);if(!n)break}return t?u.length:u?nt.error(e):L(e,a).slice(0)}function at(e,t,r){var i=t.dir,s=r&&t.dir==="parentNode",o=w++;return t.first?function(t,n,r){while(t=t[i])if(s||t.nodeType===1)return e(t,n,r)}:function(t,r,u){if(!u){var a,f=b+" "+o+" ",l=f+n;while(t=t[i])if(s||t.nodeType===1){if((a=t[d])===l)return t.sizset;if(typeof a=="string"&&a.indexOf(f)===0){if(t.sizset)return t}else{t[d]=l;if(e(t,r,u))return t.sizset=!0,t;t.sizset=!1}}}else while(t=t[i])if(s||t.nodeType===1)if(e(t,r,u))return t}}function ft(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function lt(e,t,n,r,i){var s,o=[],u=0,a=e.length,f=t!=null;for(;u-1&&(s[f]=!(o[f]=c))}}else g=lt(g===o?g.splice(d,g.length):g),i?i(null,o,g,a):S.apply(o,g)})}function ht(e){var t,n,r,s=e.length,o=i.relative[e[0].type],u=o||i.relative[" "],a=o?1:0,f=at(function(e){return e===t},u,!0),l=at(function(e){return T.call(t,e)>-1},u,!0),h=[function(e,n,r){return!o&&(r||n!==c)||((t=n).nodeType?f(e,n,r):l(e,n,r))}];for(;a1&&ft(h),a>1&&e.slice(0,a-1).join("").replace(j,"$1"),n,a0,s=e.length>0,o=function(u,a,f,l,h){var p,d,v,m=[],y=0,w="0",x=u&&[],T=h!=null,N=c,C=u||s&&i.find.TAG("*",h&&a.parentNode||a),k=b+=N==null?1:Math.E;T&&(c=a!==g&&a,n=o.el);for(;(p=C[w])!=null;w++){if(s&&p){for(d=0;v=e[d];d++)if(v(p,a,f)){l.push(p);break}T&&(b=k,n=++o.el)}r&&((p=!v&&p)&&y--,u&&x.push(p))}y+=w;if(r&&w!==y){for(d=0;v=t[d];d++)v(x,m,a,f);if(u){if(y>0)while(w--)!x[w]&&!m[w]&&(m[w]=E.call(l));m=lt(m)}S.apply(l,m),T&&!u&&m.length>0&&y+t.length>1&&nt.uniqueSort(l)}return T&&(b=k,c=N),x};return o.el=0,r?N(o):o}function dt(e,t,n){var r=0,i=t.length;for(;r2&&(f=u[0]).type==="ID"&&t.nodeType===9&&!s&&i.relative[u[1].type]){t=i.find.ID(f.matches[0].replace($,""),t,s)[0];if(!t)return n;e=e.slice(u.shift().length)}for(o=J.POS.test(e)?-1:u.length-1;o>=0;o--){f=u[o];if(i.relative[l=f.type])break;if(c=i.find[l])if(r=c(f.matches[0].replace($,""),z.test(u[0].type)&&t.parentNode||t,s)){u.splice(o,1),e=r.length&&u.join("");if(!e)return S.apply(n,x.call(r,0)),n;break}}}return a(e,h)(r,t,s,n,z.test(e)),n}function mt(){}var n,r,i,s,o,u,a,f,l,c,h=!0,p="undefined",d=("sizcache"+Math.random()).replace(".",""),m=String,g=e.document,y=g.documentElement,b=0,w=0,E=[].pop,S=[].push,x=[].slice,T=[].indexOf||function(e){var t=0,n=this.length;for(;ti.cacheLength&&delete e[t.shift()],e[n+" "]=r},e)},k=C(),L=C(),A=C(),O="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",_=M.replace("w","w#"),D="([*^$|!~]?=)",P="\\["+O+"*("+M+")"+O+"*(?:"+D+O+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+_+")|)|)"+O+"*\\]",H=":("+M+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+P+")|[^:]|\\\\.)*|.*))\\)|)",B=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)",j=new RegExp("^"+O+"+|((?:^|[^\\\\])(?:\\\\.)*)"+O+"+$","g"),F=new RegExp("^"+O+"*,"+O+"*"),I=new RegExp("^"+O+"*([\\x20\\t\\r\\n\\f>+~])"+O+"*"),q=new RegExp(H),R=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,U=/^:not/,z=/[\x20\t\r\n\f]*[+~]/,W=/:not\($/,X=/h\d/i,V=/input|select|textarea|button/i,$=/\\(?!\\)/g,J={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),NAME:new RegExp("^\\[name=['\"]?("+M+")['\"]?\\]"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+H),POS:new RegExp(B,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),needsContext:new RegExp("^"+O+"*[>+~]|"+B,"i")},K=function(e){var t=g.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}},Q=K(function(e){return e.appendChild(g.createComment("")),!e.getElementsByTagName("*").length}),G=K(function(e){return e.innerHTML="",e.firstChild&&typeof e.firstChild.getAttribute!==p&&e.firstChild.getAttribute("href")==="#"}),Y=K(function(e){e.innerHTML="";var t=typeof e.lastChild.getAttribute("multiple");return t!=="boolean"&&t!=="string"}),Z=K(function(e){return e.innerHTML="",!e.getElementsByClassName||!e.getElementsByClassName("e").length?!1:(e.lastChild.className="e",e.getElementsByClassName("e").length===2)}),et=K(function(e){e.id=d+0,e.innerHTML="
",y.insertBefore(e,y.firstChild);var t=g.getElementsByName&&g.getElementsByName(d).length===2+g.getElementsByName(d+0).length;return r=!g.getElementById(d),y.removeChild(e),t});try{x.call(y.childNodes,0)[0].nodeType}catch(tt){x=function(e){var t,n=[];for(;t=this[e];e++)n.push(t);return n}}nt.matches=function(e,t){return nt(e,null,null,t)},nt.matchesSelector=function(e,t){return nt(t,null,null,[e]).length>0},s=nt.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(i===1||i===9||i===11){if(typeof e.textContent=="string")return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=s(e)}else if(i===3||i===4)return e.nodeValue}else for(;t=e[r];r++)n+=s(t);return n},o=nt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?t.nodeName!=="HTML":!1},u=nt.contains=y.contains?function(e,t){var n=e.nodeType===9?e.documentElement:e,r=t&&t.parentNode;return e===r||!!(r&&r.nodeType===1&&n.contains&&n.contains(r))}:y.compareDocumentPosition?function(e,t){return t&&!!(e.compareDocumentPosition(t)&16)}:function(e,t){while(t=t.parentNode)if(t===e)return!0;return!1},nt.attr=function(e,t){var n,r=o(e);return r||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):r||Y?e.getAttribute(t):(n=e.getAttributeNode(t),n?typeof e[t]=="boolean"?e[t]?t:null:n.specified?n.value:null:null)},i=nt.selectors={cacheLength:50,createPseudo:N,match:J,attrHandle:G?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},find:{ID:r?function(e,t,n){if(typeof t.getElementById!==p&&!n){var r=t.getElementById(e);return r&&r.parentNode?[r]:[]}}:function(e,n,r){if(typeof n.getElementById!==p&&!r){var i=n.getElementById(e);return i?i.id===e||typeof i.getAttributeNode!==p&&i.getAttributeNode("id").value===e?[i]:t:[]}},TAG:Q?function(e,t){if(typeof t.getElementsByTagName!==p)return t.getElementsByTagName(e)}:function(e,t){var n=t.getElementsByTagName(e);if(e==="*"){var r,i=[],s=0;for(;r=n[s];s++)r.nodeType===1&&i.push(r);return i}return n},NAME:et&&function(e,t){if(typeof t.getElementsByName!==p)return t.getElementsByName(name)},CLASS:Z&&function(e,t,n){if(typeof t.getElementsByClassName!==p&&!n)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,""),e[3]=(e[4]||e[5]||"").replace($,""),e[2]==="~="&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),e[1]==="nth"?(e[2]||nt.error(e[0]),e[3]=+(e[3]?e[4]+(e[5]||1):2*(e[2]==="even"||e[2]==="odd")),e[4]=+(e[6]+e[7]||e[2]==="odd")):e[2]&&nt.error(e[0]),e},PSEUDO:function(e){var t,n;if(J.CHILD.test(e[0]))return null;if(e[3])e[2]=e[3];else if(t=e[4])q.test(t)&&(n=ut(t,!0))&&(n=t.indexOf(")",t.length-n)-t.length)&&(t=t.slice(0,n),e[0]=e[0].slice(0,n)),e[2]=t;return e.slice(0,3)}},filter:{ID:r?function(e){return e=e.replace($,""),function(t){return t.getAttribute("id")===e}}:function(e){return e=e.replace($,""),function(t){var n=typeof t.getAttributeNode!==p&&t.getAttributeNode("id");return n&&n.value===e}},TAG:function(e){return e==="*"?function(){return!0}:(e=e.replace($,"").toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[d][e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==p&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r,i){var s=nt.attr(r,e);return s==null?t==="!=":t?(s+="",t==="="?s===n:t==="!="?s!==n:t==="^="?n&&s.indexOf(n)===0:t==="*="?n&&s.indexOf(n)>-1:t==="$="?n&&s.substr(s.length-n.length)===n:t==="~="?(" "+s+" ").indexOf(n)>-1:t==="|="?s===n||s.substr(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r){return e==="nth"?function(e){var t,i,s=e.parentNode;if(n===1&&r===0)return!0;if(s){i=0;for(t=s.firstChild;t;t=t.nextSibling)if(t.nodeType===1){i++;if(e===t)break}}return i-=r,i===n||i%n===0&&i/n>=0}:function(t){var n=t;switch(e){case"only":case"first":while(n=n.previousSibling)if(n.nodeType===1)return!1;if(e==="first")return!0;n=t;case"last":while(n=n.nextSibling)if(n.nodeType===1)return!1;return!0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||nt.error("unsupported pseudo: "+e);return r[d]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?N(function(e,n){var i,s=r(e,t),o=s.length;while(o--)i=T.call(e,s[o]),e[i]=!(n[i]=s[o])}):function(e){return r(e,0,n)}):r}},pseudos:{not:N(function(e){var t=[],n=[],r=a(e.replace(j,"$1"));return r[d]?N(function(e,t,n,i){var s,o=r(e,null,i,[]),u=e.length;while(u--)if(s=o[u])e[u]=!(t[u]=s)}):function(e,i,s){return t[0]=e,r(t,null,s,n),!n.pop()}}),has:N(function(e){return function(t){return nt(e,t).length>0}}),contains:N(function(e){return function(t){return(t.textContent||t.innerText||s(t)).indexOf(e)>-1}}),enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&!!e.checked||t==="option"&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},parent:function(e){return!i.pseudos.empty(e)},empty:function(e){var t;e=e.firstChild;while(e){if(e.nodeName>"@"||(t=e.nodeType)===3||t===4)return!1;e=e.nextSibling}return!0},header:function(e){return X.test(e.nodeName)},text:function(e){var t,n;return e.nodeName.toLowerCase()==="input"&&(t=e.type)==="text"&&((n=e.getAttribute("type"))==null||n.toLowerCase()===t)},radio:rt("radio"),checkbox:rt("checkbox"),file:rt("file"),password:rt("password"),image:rt("image"),submit:it("submit"),reset:it("reset"),button:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&e.type==="button"||t==="button"},input:function(e){return V.test(e.nodeName)},focus:function(e){var t=e.ownerDocument;return e===t.activeElement&&(!t.hasFocus||t.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},active:function(e){return e===e.ownerDocument.activeElement},first:st(function(){return[0]}),last:st(function(e,t){return[t-1]}),eq:st(function(e,t,n){return[n<0?n+t:n]}),even:st(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:st(function(e,t,n){for(var r=n<0?n+t:n;++r",e.querySelectorAll("[selected]").length||i.push("\\["+O+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||i.push(":checked")}),K(function(e){e.innerHTML="

",e.querySelectorAll("[test^='']").length&&i.push("[*^$]="+O+"*(?:\"\"|'')"),e.innerHTML="",e.querySelectorAll(":enabled").length||i.push(":enabled",":disabled")}),i=new RegExp(i.join("|")),vt=function(e,r,s,o,u){if(!o&&!u&&!i.test(e)){var a,f,l=!0,c=d,h=r,p=r.nodeType===9&&e;if(r.nodeType===1&&r.nodeName.toLowerCase()!=="object"){a=ut(e),(l=r.getAttribute("id"))?c=l.replace(n,"\\$&"):r.setAttribute("id",c),c="[id='"+c+"'] ",f=a.length;while(f--)a[f]=c+a[f].join("");h=z.test(e)&&r.parentNode||r,p=a.join(",")}if(p)try{return S.apply(s,x.call(h.querySelectorAll(p),0)),s}catch(v){}finally{l||r.removeAttribute("id")}}return t(e,r,s,o,u)},u&&(K(function(t){e=u.call(t,"div");try{u.call(t,"[test!='']:sizzle"),s.push("!=",H)}catch(n){}}),s=new RegExp(s.join("|")),nt.matchesSelector=function(t,n){n=n.replace(r,"='$1']");if(!o(t)&&!s.test(n)&&!i.test(n))try{var a=u.call(t,n);if(a||e||t.document&&t.document.nodeType!==11)return a}catch(f){}return nt(n,null,null,[t]).length>0})}(),i.pseudos.nth=i.pseudos.eq,i.filters=mt.prototype=i.pseudos,i.setFilters=new mt,nt.attr=v.attr,v.find=nt,v.expr=nt.selectors,v.expr[":"]=v.expr.pseudos,v.unique=nt.uniqueSort,v.text=nt.getText,v.isXMLDoc=nt.isXML,v.contains=nt.contains}(e);var nt=/Until$/,rt=/^(?:parents|prev(?:Until|All))/,it=/^.[^:#\[\.,]*$/,st=v.expr.match.needsContext,ot={children:!0,contents:!0,next:!0,prev:!0};v.fn.extend({find:function(e){var t,n,r,i,s,o,u=this;if(typeof e!="string")return v(e).filter(function(){for(t=0,n=u.length;t0)for(i=r;i=0:v.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,s=[],o=st.test(e)||typeof e!="string"?v(e,t||this.context):0;for(;r-1:v.find.matchesSelector(n,e)){s.push(n);break}n=n.parentNode}}return s=s.length>1?v.unique(s):s,this.pushStack(s,"closest",e)},index:function(e){return e?typeof e=="string"?v.inArray(this[0],v(e)):v.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(e,t){var n=typeof e=="string"?v(e,t):v.makeArray(e&&e.nodeType?[e]:e),r=v.merge(this.get(),n);return this.pushStack(ut(n[0])||ut(r[0])?r:v.unique(r))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}}),v.fn.andSelf=v.fn.addBack,v.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return v.dir(e,"parentNode")},parentsUntil:function(e,t,n){return v.dir(e,"parentNode",n)},next:function(e){return at(e,"nextSibling")},prev:function(e){return at(e,"previousSibling")},nextAll:function(e){return v.dir(e,"nextSibling")},prevAll:function(e){return v.dir(e,"previousSibling")},nextUntil:function(e,t,n){return v.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return v.dir(e,"previousSibling",n)},siblings:function(e){return v.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return v.sibling(e.firstChild)},contents:function(e){return v.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:v.merge([],e.childNodes)}},function(e,t){v.fn[e]=function(n,r){var i=v.map(this,t,n);return nt.test(e)||(r=n),r&&typeof r=="string"&&(i=v.filter(r,i)),i=this.length>1&&!ot[e]?v.unique(i):i,this.length>1&&rt.test(e)&&(i=i.reverse()),this.pushStack(i,e,l.call(arguments).join(","))}}),v.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),t.length===1?v.find.matchesSelector(t[0],e)?[t[0]]:[]:v.find.matches(e,t)},dir:function(e,n,r){var i=[],s=e[n];while(s&&s.nodeType!==9&&(r===t||s.nodeType!==1||!v(s).is(r)))s.nodeType===1&&i.push(s),s=s[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n}});var ct="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",ht=/ jQuery\d+="(?:null|\d+)"/g,pt=/^\s+/,dt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,vt=/<([\w:]+)/,mt=/]","i"),Et=/^(?:checkbox|radio)$/,St=/checked\s*(?:[^=]|=\s*.checked.)/i,xt=/\/(java|ecma)script/i,Tt=/^\s*\s*$/g,Nt={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},Ct=lt(i),kt=Ct.appendChild(i.createElement("div"));Nt.optgroup=Nt.option,Nt.tbody=Nt.tfoot=Nt.colgroup=Nt.caption=Nt.thead,Nt.th=Nt.td,v.support.htmlSerialize||(Nt._default=[1,"X
","
"]),v.fn.extend({text:function(e){return v.access(this,function(e){return e===t?v.text(this):this.empty().append((this[0]&&this[0].ownerDocument||i).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(v.isFunction(e))return this.each(function(t){v(this).wrapAll(e.call(this,t))});if(this[0]){var t=v(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&e.firstChild.nodeType===1)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return v.isFunction(e)?this.each(function(t){v(this).wrapInner(e.call(this,t))}):this.each(function(){var t=v(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v.isFunction(e);return this.each(function(n){v(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){v.nodeName(this,"body")||v(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(e,this.firstChild)})},before:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(e,this),"before",this.selector)}},after:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this.nextSibling)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(this,e),"after",this.selector)}},remove:function(e,t){var n,r=0;for(;(n=this[r])!=null;r++)if(!e||v.filter(e,[n]).length)!t&&n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),v.cleanData([n])),n.parentNode&&n.parentNode.removeChild(n);return this},empty:function(){var e,t=0;for(;(e=this[t])!=null;t++){e.nodeType===1&&v.cleanData(e.getElementsByTagName("*"));while(e.firstChild)e.removeChild(e.firstChild)}return this},clone:function(e,t){return e=e==null?!1:e,t=t==null?e:t,this.map(function(){return v.clone(this,e,t)})},html:function(e){return v.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return n.nodeType===1?n.innerHTML.replace(ht,""):t;if(typeof e=="string"&&!yt.test(e)&&(v.support.htmlSerialize||!wt.test(e))&&(v.support.leadingWhitespace||!pt.test(e))&&!Nt[(vt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(dt,"<$1>");try{for(;r1&&typeof f=="string"&&St.test(f))return this.each(function(){v(this).domManip(e,n,r)});if(v.isFunction(f))return this.each(function(i){var s=v(this);e[0]=f.call(this,i,n?s.html():t),s.domManip(e,n,r)});if(this[0]){i=v.buildFragment(e,this,l),o=i.fragment,s=o.firstChild,o.childNodes.length===1&&(o=s);if(s){n=n&&v.nodeName(s,"tr");for(u=i.cacheable||c-1;a0?this.clone(!0):this).get(),v(o[i])[t](r),s=s.concat(r);return this.pushStack(s,e,o.selector)}}),v.extend({clone:function(e,t,n){var r,i,s,o;v.support.html5Clone||v.isXMLDoc(e)||!wt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(kt.innerHTML=e.outerHTML,kt.removeChild(o=kt.firstChild));if((!v.support.noCloneEvent||!v.support.noCloneChecked)&&(e.nodeType===1||e.nodeType===11)&&!v.isXMLDoc(e)){Ot(e,o),r=Mt(e),i=Mt(o);for(s=0;r[s];++s)i[s]&&Ot(r[s],i[s])}if(t){At(e,o);if(n){r=Mt(e),i=Mt(o);for(s=0;r[s];++s)At(r[s],i[s])}}return r=i=null,o},clean:function(e,t,n,r){var s,o,u,a,f,l,c,h,p,d,m,g,y=t===i&&Ct,b=[];if(!t||typeof t.createDocumentFragment=="undefined")t=i;for(s=0;(u=e[s])!=null;s++){typeof u=="number"&&(u+="");if(!u)continue;if(typeof u=="string")if(!gt.test(u))u=t.createTextNode(u);else{y=y||lt(t),c=t.createElement("div"),y.appendChild(c),u=u.replace(dt,"<$1>"),a=(vt.exec(u)||["",""])[1].toLowerCase(),f=Nt[a]||Nt._default,l=f[0],c.innerHTML=f[1]+u+f[2];while(l--)c=c.lastChild;if(!v.support.tbody){h=mt.test(u),p=a==="table"&&!h?c.firstChild&&c.firstChild.childNodes:f[1]===""&&!h?c.childNodes:[];for(o=p.length-1;o>=0;--o)v.nodeName(p[o],"tbody")&&!p[o].childNodes.length&&p[o].parentNode.removeChild(p[o])}!v.support.leadingWhitespace&&pt.test(u)&&c.insertBefore(t.createTextNode(pt.exec(u)[0]),c.firstChild),u=c.childNodes,c.parentNode.removeChild(c)}u.nodeType?b.push(u):v.merge(b,u)}c&&(u=c=y=null);if(!v.support.appendChecked)for(s=0;(u=b[s])!=null;s++)v.nodeName(u,"input")?_t(u):typeof u.getElementsByTagName!="undefined"&&v.grep(u.getElementsByTagName("input"),_t);if(n){m=function(e){if(!e.type||xt.test(e.type))return r?r.push(e.parentNode?e.parentNode.removeChild(e):e):n.appendChild(e)};for(s=0;(u=b[s])!=null;s++)if(!v.nodeName(u,"script")||!m(u))n.appendChild(u),typeof u.getElementsByTagName!="undefined"&&(g=v.grep(v.merge([],u.getElementsByTagName("script")),m),b.splice.apply(b,[s+1,0].concat(g)),s+=g.length)}return b},cleanData:function(e,t){var n,r,i,s,o=0,u=v.expando,a=v.cache,f=v.support.deleteExpando,l=v.event.special;for(;(i=e[o])!=null;o++)if(t||v.acceptData(i)){r=i[u],n=r&&a[r];if(n){if(n.events)for(s in n.events)l[s]?v.event.remove(i,s):v.removeEvent(i,s,n.handle);a[r]&&(delete a[r],f?delete i[u]:i.removeAttribute?i.removeAttribute(u):i[u]=null,v.deletedIds.push(r))}}}}),function(){var e,t;v.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e=v.uaMatch(o.userAgent),t={},e.browser&&(t[e.browser]=!0,t.version=e.version),t.chrome?t.webkit=!0:t.webkit&&(t.safari=!0),v.browser=t,v.sub=function(){function e(t,n){return new e.fn.init(t,n)}v.extend(!0,e,this),e.superclass=this,e.fn=e.prototype=this(),e.fn.constructor=e,e.sub=this.sub,e.fn.init=function(r,i){return i&&i instanceof v&&!(i instanceof e)&&(i=e(i)),v.fn.init.call(this,r,i,t)},e.fn.init.prototype=e.fn;var t=e(i);return e}}();var Dt,Pt,Ht,Bt=/alpha\([^)]*\)/i,jt=/opacity=([^)]*)/,Ft=/^(top|right|bottom|left)$/,It=/^(none|table(?!-c[ea]).+)/,qt=/^margin/,Rt=new RegExp("^("+m+")(.*)$","i"),Ut=new RegExp("^("+m+")(?!px)[a-z%]+$","i"),zt=new RegExp("^([-+])=("+m+")","i"),Wt={BODY:"block"},Xt={position:"absolute",visibility:"hidden",display:"block"},Vt={letterSpacing:0,fontWeight:400},$t=["Top","Right","Bottom","Left"],Jt=["Webkit","O","Moz","ms"],Kt=v.fn.toggle;v.fn.extend({css:function(e,n){return v.access(this,function(e,n,r){return r!==t?v.style(e,n,r):v.css(e,n)},e,n,arguments.length>1)},show:function(){return Yt(this,!0)},hide:function(){return Yt(this)},toggle:function(e,t){var n=typeof e=="boolean";return v.isFunction(e)&&v.isFunction(t)?Kt.apply(this,arguments):this.each(function(){(n?e:Gt(this))?v(this).show():v(this).hide()})}}),v.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Dt(e,"opacity");return n===""?"1":n}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":v.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(!e||e.nodeType===3||e.nodeType===8||!e.style)return;var s,o,u,a=v.camelCase(n),f=e.style;n=v.cssProps[a]||(v.cssProps[a]=Qt(f,a)),u=v.cssHooks[n]||v.cssHooks[a];if(r===t)return u&&"get"in u&&(s=u.get(e,!1,i))!==t?s:f[n];o=typeof r,o==="string"&&(s=zt.exec(r))&&(r=(s[1]+1)*s[2]+parseFloat(v.css(e,n)),o="number");if(r==null||o==="number"&&isNaN(r))return;o==="number"&&!v.cssNumber[a]&&(r+="px");if(!u||!("set"in u)||(r=u.set(e,r,i))!==t)try{f[n]=r}catch(l){}},css:function(e,n,r,i){var s,o,u,a=v.camelCase(n);return n=v.cssProps[a]||(v.cssProps[a]=Qt(e.style,a)),u=v.cssHooks[n]||v.cssHooks[a],u&&"get"in u&&(s=u.get(e,!0,i)),s===t&&(s=Dt(e,n)),s==="normal"&&n in Vt&&(s=Vt[n]),r||i!==t?(o=parseFloat(s),r||v.isNumeric(o)?o||0:s):s},swap:function(e,t,n){var r,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];r=n.call(e);for(i in t)e.style[i]=s[i];return r}}),e.getComputedStyle?Dt=function(t,n){var r,i,s,o,u=e.getComputedStyle(t,null),a=t.style;return u&&(r=u.getPropertyValue(n)||u[n],r===""&&!v.contains(t.ownerDocument,t)&&(r=v.style(t,n)),Ut.test(r)&&qt.test(n)&&(i=a.width,s=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=r,r=u.width,a.width=i,a.minWidth=s,a.maxWidth=o)),r}:i.documentElement.currentStyle&&(Dt=function(e,t){var n,r,i=e.currentStyle&&e.currentStyle[t],s=e.style;return i==null&&s&&s[t]&&(i=s[t]),Ut.test(i)&&!Ft.test(t)&&(n=s.left,r=e.runtimeStyle&&e.runtimeStyle.left,r&&(e.runtimeStyle.left=e.currentStyle.left),s.left=t==="fontSize"?"1em":i,i=s.pixelLeft+"px",s.left=n,r&&(e.runtimeStyle.left=r)),i===""?"auto":i}),v.each(["height","width"],function(e,t){v.cssHooks[t]={get:function(e,n,r){if(n)return e.offsetWidth===0&&It.test(Dt(e,"display"))?v.swap(e,Xt,function(){return tn(e,t,r)}):tn(e,t,r)},set:function(e,n,r){return Zt(e,n,r?en(e,t,r,v.support.boxSizing&&v.css(e,"boxSizing")==="border-box"):0)}}}),v.support.opacity||(v.cssHooks.opacity={get:function(e,t){return jt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=v.isNumeric(t)?"alpha(opacity="+t*100+")":"",s=r&&r.filter||n.filter||"";n.zoom=1;if(t>=1&&v.trim(s.replace(Bt,""))===""&&n.removeAttribute){n.removeAttribute("filter");if(r&&!r.filter)return}n.filter=Bt.test(s)?s.replace(Bt,i):s+" "+i}}),v(function(){v.support.reliableMarginRight||(v.cssHooks.marginRight={get:function(e,t){return v.swap(e,{display:"inline-block"},function(){if(t)return Dt(e,"marginRight")})}}),!v.support.pixelPosition&&v.fn.position&&v.each(["top","left"],function(e,t){v.cssHooks[t]={get:function(e,n){if(n){var r=Dt(e,t);return Ut.test(r)?v(e).position()[t]+"px":r}}}})}),v.expr&&v.expr.filters&&(v.expr.filters.hidden=function(e){return e.offsetWidth===0&&e.offsetHeight===0||!v.support.reliableHiddenOffsets&&(e.style&&e.style.display||Dt(e,"display"))==="none"},v.expr.filters.visible=function(e){return!v.expr.filters.hidden(e)}),v.each({margin:"",padding:"",border:"Width"},function(e,t){v.cssHooks[e+t]={expand:function(n){var r,i=typeof n=="string"?n.split(" "):[n],s={};for(r=0;r<4;r++)s[e+$t[r]+t]=i[r]||i[r-2]||i[0];return s}},qt.test(e)||(v.cssHooks[e+t].set=Zt)});var rn=/%20/g,sn=/\[\]$/,on=/\r?\n/g,un=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,an=/^(?:select|textarea)/i;v.fn.extend({serialize:function(){return v.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?v.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||an.test(this.nodeName)||un.test(this.type))}).map(function(e,t){var n=v(this).val();return n==null?null:v.isArray(n)?v.map(n,function(e,n){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),v.param=function(e,n){var r,i=[],s=function(e,t){t=v.isFunction(t)?t():t==null?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};n===t&&(n=v.ajaxSettings&&v.ajaxSettings.traditional);if(v.isArray(e)||e.jquery&&!v.isPlainObject(e))v.each(e,function(){s(this.name,this.value)});else for(r in e)fn(r,e[r],n,s);return i.join("&").replace(rn,"+")};var ln,cn,hn=/#.*$/,pn=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,dn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,vn=/^(?:GET|HEAD)$/,mn=/^\/\//,gn=/\?/,yn=/)<[^<]*)*<\/script>/gi,bn=/([?&])_=[^&]*/,wn=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,En=v.fn.load,Sn={},xn={},Tn=["*/"]+["*"];try{cn=s.href}catch(Nn){cn=i.createElement("a"),cn.href="",cn=cn.href}ln=wn.exec(cn.toLowerCase())||[],v.fn.load=function(e,n,r){if(typeof e!="string"&&En)return En.apply(this,arguments);if(!this.length)return this;var i,s,o,u=this,a=e.indexOf(" ");return a>=0&&(i=e.slice(a,e.length),e=e.slice(0,a)),v.isFunction(n)?(r=n,n=t):n&&typeof n=="object"&&(s="POST"),v.ajax({url:e,type:s,dataType:"html",data:n,complete:function(e,t){r&&u.each(r,o||[e.responseText,t,e])}}).done(function(e){o=arguments,u.html(i?v("
").append(e.replace(yn,"")).find(i):e)}),this},v.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(e,t){v.fn[t]=function(e){return this.on(t,e)}}),v.each(["get","post"],function(e,n){v[n]=function(e,r,i,s){return v.isFunction(r)&&(s=s||i,i=r,r=t),v.ajax({type:n,url:e,data:r,success:i,dataType:s})}}),v.extend({getScript:function(e,n){return v.get(e,t,n,"script")},getJSON:function(e,t,n){return v.get(e,t,n,"json")},ajaxSetup:function(e,t){return t?Ln(e,v.ajaxSettings):(t=e,e=v.ajaxSettings),Ln(e,t),e},ajaxSettings:{url:cn,isLocal:dn.test(ln[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":Tn},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":v.parseJSON,"text xml":v.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:Cn(Sn),ajaxTransport:Cn(xn),ajax:function(e,n){function T(e,n,s,a){var l,y,b,w,S,T=n;if(E===2)return;E=2,u&&clearTimeout(u),o=t,i=a||"",x.readyState=e>0?4:0,s&&(w=An(c,x,s));if(e>=200&&e<300||e===304)c.ifModified&&(S=x.getResponseHeader("Last-Modified"),S&&(v.lastModified[r]=S),S=x.getResponseHeader("Etag"),S&&(v.etag[r]=S)),e===304?(T="notmodified",l=!0):(l=On(c,w),T=l.state,y=l.data,b=l.error,l=!b);else{b=T;if(!T||e)T="error",e<0&&(e=0)}x.status=e,x.statusText=(n||T)+"",l?d.resolveWith(h,[y,T,x]):d.rejectWith(h,[x,T,b]),x.statusCode(g),g=t,f&&p.trigger("ajax"+(l?"Success":"Error"),[x,c,l?y:b]),m.fireWith(h,[x,T]),f&&(p.trigger("ajaxComplete",[x,c]),--v.active||v.event.trigger("ajaxStop"))}typeof e=="object"&&(n=e,e=t),n=n||{};var r,i,s,o,u,a,f,l,c=v.ajaxSetup({},n),h=c.context||c,p=h!==c&&(h.nodeType||h instanceof v)?v(h):v.event,d=v.Deferred(),m=v.Callbacks("once memory"),g=c.statusCode||{},b={},w={},E=0,S="canceled",x={readyState:0,setRequestHeader:function(e,t){if(!E){var n=e.toLowerCase();e=w[n]=w[n]||e,b[e]=t}return this},getAllResponseHeaders:function(){return E===2?i:null},getResponseHeader:function(e){var n;if(E===2){if(!s){s={};while(n=pn.exec(i))s[n[1].toLowerCase()]=n[2]}n=s[e.toLowerCase()]}return n===t?null:n},overrideMimeType:function(e){return E||(c.mimeType=e),this},abort:function(e){return e=e||S,o&&o.abort(e),T(0,e),this}};d.promise(x),x.success=x.done,x.error=x.fail,x.complete=m.add,x.statusCode=function(e){if(e){var t;if(E<2)for(t in e)g[t]=[g[t],e[t]];else t=e[x.status],x.always(t)}return this},c.url=((e||c.url)+"").replace(hn,"").replace(mn,ln[1]+"//"),c.dataTypes=v.trim(c.dataType||"*").toLowerCase().split(y),c.crossDomain==null&&(a=wn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===ln[1]&&a[2]===ln[2]&&(a[3]||(a[1]==="http:"?80:443))==(ln[3]||(ln[1]==="http:"?80:443)))),c.data&&c.processData&&typeof c.data!="string"&&(c.data=v.param(c.data,c.traditional)),kn(Sn,c,n,x);if(E===2)return x;f=c.global,c.type=c.type.toUpperCase(),c.hasContent=!vn.test(c.type),f&&v.active++===0&&v.event.trigger("ajaxStart");if(!c.hasContent){c.data&&(c.url+=(gn.test(c.url)?"&":"?")+c.data,delete c.data),r=c.url;if(c.cache===!1){var N=v.now(),C=c.url.replace(bn,"$1_="+N);c.url=C+(C===c.url?(gn.test(c.url)?"&":"?")+"_="+N:"")}}(c.data&&c.hasContent&&c.contentType!==!1||n.contentType)&&x.setRequestHeader("Content-Type",c.contentType),c.ifModified&&(r=r||c.url,v.lastModified[r]&&x.setRequestHeader("If-Modified-Since",v.lastModified[r]),v.etag[r]&&x.setRequestHeader("If-None-Match",v.etag[r])),x.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+(c.dataTypes[0]!=="*"?", "+Tn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)x.setRequestHeader(l,c.headers[l]);if(!c.beforeSend||c.beforeSend.call(h,x,c)!==!1&&E!==2){S="abort";for(l in{success:1,error:1,complete:1})x[l](c[l]);o=kn(xn,c,n,x);if(!o)T(-1,"No Transport");else{x.readyState=1,f&&p.trigger("ajaxSend",[x,c]),c.async&&c.timeout>0&&(u=setTimeout(function(){x.abort("timeout")},c.timeout));try{E=1,o.send(b,T)}catch(k){if(!(E<2))throw k;T(-1,k)}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var Mn=[],_n=/\?/,Dn=/(=)\?(?=&|$)|\?\?/,Pn=v.now();v.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mn.pop()||v.expando+"_"+Pn++;return this[e]=!0,e}}),v.ajaxPrefilter("json jsonp",function(n,r,i){var s,o,u,a=n.data,f=n.url,l=n.jsonp!==!1,c=l&&Dn.test(f),h=l&&!c&&typeof a=="string"&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Dn.test(a);if(n.dataTypes[0]==="jsonp"||c||h)return s=n.jsonpCallback=v.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,o=e[s],c?n.url=f.replace(Dn,"$1"+s):h?n.data=a.replace(Dn,"$1"+s):l&&(n.url+=(_n.test(f)?"&":"?")+n.jsonp+"="+s),n.converters["script json"]=function(){return u||v.error(s+" was not called"),u[0]},n.dataTypes[0]="json",e[s]=function(){u=arguments},i.always(function(){e[s]=o,n[s]&&(n.jsonpCallback=r.jsonpCallback,Mn.push(s)),u&&v.isFunction(o)&&o(u[0]),u=o=t}),"script"}),v.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(e){return v.globalEval(e),e}}}),v.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),v.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=i.head||i.getElementsByTagName("head")[0]||i.documentElement;return{send:function(s,o){n=i.createElement("script"),n.async="async",e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,i){if(i||!n.readyState||/loaded|complete/.test(n.readyState))n.onload=n.onreadystatechange=null,r&&n.parentNode&&r.removeChild(n),n=t,i||o(200,"success")},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(0,1)}}}});var Hn,Bn=e.ActiveXObject?function(){for(var e in Hn)Hn[e](0,1)}:!1,jn=0;v.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&Fn()||In()}:Fn,function(e){v.extend(v.support,{ajax:!!e,cors:!!e&&"withCredentials"in e})}(v.ajaxSettings.xhr()),v.support.ajax&&v.ajaxTransport(function(n){if(!n.crossDomain||v.support.cors){var r;return{send:function(i,s){var o,u,a=n.xhr();n.username?a.open(n.type,n.url,n.async,n.username,n.password):a.open(n.type,n.url,n.async);if(n.xhrFields)for(u in n.xhrFields)a[u]=n.xhrFields[u];n.mimeType&&a.overrideMimeType&&a.overrideMimeType(n.mimeType),!n.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");try{for(u in i)a.setRequestHeader(u,i[u])}catch(f){}a.send(n.hasContent&&n.data||null),r=function(e,i){var u,f,l,c,h;try{if(r&&(i||a.readyState===4)){r=t,o&&(a.onreadystatechange=v.noop,Bn&&delete Hn[o]);if(i)a.readyState!==4&&a.abort();else{u=a.status,l=a.getAllResponseHeaders(),c={},h=a.responseXML,h&&h.documentElement&&(c.xml=h);try{c.text=a.responseText}catch(p){}try{f=a.statusText}catch(p){f=""}!u&&n.isLocal&&!n.crossDomain?u=c.text?200:404:u===1223&&(u=204)}}}catch(d){i||s(-1,d)}c&&s(u,f,c,l)},n.async?a.readyState===4?setTimeout(r,0):(o=++jn,Bn&&(Hn||(Hn={},v(e).unload(Bn)),Hn[o]=r),a.onreadystatechange=r):r()},abort:function(){r&&r(0,1)}}}});var qn,Rn,Un=/^(?:toggle|show|hide)$/,zn=new RegExp("^(?:([-+])=|)("+m+")([a-z%]*)$","i"),Wn=/queueHooks$/,Xn=[Gn],Vn={"*":[function(e,t){var n,r,i=this.createTween(e,t),s=zn.exec(t),o=i.cur(),u=+o||0,a=1,f=20;if(s){n=+s[2],r=s[3]||(v.cssNumber[e]?"":"px");if(r!=="px"&&u){u=v.css(i.elem,e,!0)||n||1;do a=a||".5",u/=a,v.style(i.elem,e,u+r);while(a!==(a=i.cur()/o)&&a!==1&&--f)}i.unit=r,i.start=u,i.end=s[1]?u+(s[1]+1)*n:n}return i}]};v.Animation=v.extend(Kn,{tweener:function(e,t){v.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;r-1,f={},l={},c,h;a?(l=i.position(),c=l.top,h=l.left):(c=parseFloat(o)||0,h=parseFloat(u)||0),v.isFunction(t)&&(t=t.call(e,n,s)),t.top!=null&&(f.top=t.top-s.top+c),t.left!=null&&(f.left=t.left-s.left+h),"using"in t?t.using.call(e,f):i.css(f)}},v.fn.extend({position:function(){if(!this[0])return;var e=this[0],t=this.offsetParent(),n=this.offset(),r=er.test(t[0].nodeName)?{top:0,left:0}:t.offset();return n.top-=parseFloat(v.css(e,"marginTop"))||0,n.left-=parseFloat(v.css(e,"marginLeft"))||0,r.top+=parseFloat(v.css(t[0],"borderTopWidth"))||0,r.left+=parseFloat(v.css(t[0],"borderLeftWidth"))||0,{top:n.top-r.top,left:n.left-r.left}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||i.body;while(e&&!er.test(e.nodeName)&&v.css(e,"position")==="static")e=e.offsetParent;return e||i.body})}}),v.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);v.fn[e]=function(i){return v.access(this,function(e,i,s){var o=tr(e);if(s===t)return o?n in o?o[n]:o.document.documentElement[i]:e[i];o?o.scrollTo(r?v(o).scrollLeft():s,r?s:v(o).scrollTop()):e[i]=s},e,i,arguments.length,null)}}),v.each({Height:"height",Width:"width"},function(e,n){v.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){v.fn[i]=function(i,s){var o=arguments.length&&(r||typeof i!="boolean"),u=r||(i===!0||s===!0?"margin":"border");return v.access(this,function(n,r,i){var s;return v.isWindow(n)?n.document.documentElement["client"+e]:n.nodeType===9?(s=n.documentElement,Math.max(n.body["scroll"+e],s["scroll"+e],n.body["offset"+e],s["offset"+e],s["client"+e])):i===t?v.css(n,r,i,u):v.style(n,r,i,u)},n,o?i:t,o,null)}})}),e.jQuery=e.$=v,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return v})})(window); \ No newline at end of file diff --git a/_static/js/theme.js b/_static/js/theme.js new file mode 100644 index 0000000..60520cc --- /dev/null +++ b/_static/js/theme.js @@ -0,0 +1,47 @@ +$( document ).ready(function() { + // Shift nav in mobile when clicking the menu. + $(document).on('click', "[data-toggle='wy-nav-top']", function() { + $("[data-toggle='wy-nav-shift']").toggleClass("shift"); + $("[data-toggle='rst-versions']").toggleClass("shift"); + }); + // Close menu when you click a link. + $(document).on('click', ".wy-menu-vertical .current ul li a", function() { + $("[data-toggle='wy-nav-shift']").removeClass("shift"); + $("[data-toggle='rst-versions']").toggleClass("shift"); + }); + $(document).on('click', "[data-toggle='rst-current-version']", function() { + $("[data-toggle='rst-versions']").toggleClass("shift-up"); + }); + // Make tables responsive + $("table.docutils:not(.field-list)").wrap("
"); +}); + +window.SphinxRtdTheme = (function (jquery) { + var stickyNav = (function () { + var navBar, + win, + stickyNavCssClass = 'stickynav', + applyStickNav = function () { + if (navBar.height() <= win.height()) { + navBar.addClass(stickyNavCssClass); + } else { + navBar.removeClass(stickyNavCssClass); + } + }, + enable = function () { + applyStickNav(); + win.on('resize', applyStickNav); + }, + init = function () { + navBar = jquery('nav.wy-nav-side:first'); + win = jquery(window); + }; + jquery(init); + return { + enable : enable + }; + }()); + return { + StickyNav : stickyNav + }; +}($)); diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 0000000..da1c562 Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 0000000..b3cb374 Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 0000000..8213e90 --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,65 @@ +.highlight .hll { background-color: #ffffcc } +.highlight { background: #eeffcc; } +.highlight .c { color: #408090; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #007020; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #007020 } /* Comment.Preproc */ +.highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #333333 } /* Generic.Output */ +.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #007020 } /* Keyword.Pseudo */ +.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #902000 } /* Keyword.Type */ +.highlight .m { color: #208050 } /* Literal.Number */ +.highlight .s { color: #4070a0 } /* Literal.String */ +.highlight .na { color: #4070a0 } /* Name.Attribute */ +.highlight .nb { color: #007020 } /* Name.Builtin */ +.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ +.highlight .no { color: #60add5 } /* Name.Constant */ +.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #007020 } /* Name.Exception */ +.highlight .nf { color: #06287e } /* Name.Function */ +.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ +.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #bb60d5 } /* Name.Variable */ +.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #208050 } /* Literal.Number.Bin */ +.highlight .mf { color: #208050 } /* Literal.Number.Float */ +.highlight .mh { color: #208050 } /* Literal.Number.Hex */ +.highlight .mi { color: #208050 } /* Literal.Number.Integer */ +.highlight .mo { color: #208050 } /* Literal.Number.Oct */ +.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ +.highlight .sc { color: #4070a0 } /* Literal.String.Char */ +.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4070a0 } /* Literal.String.Double */ +.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ +.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ +.highlight .sx { color: #c65d09 } /* Literal.String.Other */ +.highlight .sr { color: #235388 } /* Literal.String.Regex */ +.highlight .s1 { color: #4070a0 } /* Literal.String.Single */ +.highlight .ss { color: #517918 } /* Literal.String.Symbol */ +.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ +.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ +.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ +.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/searchtools.js b/_static/searchtools.js new file mode 100644 index 0000000..6e1f06b --- /dev/null +++ b/_static/searchtools.js @@ -0,0 +1,622 @@ +/* + * searchtools.js_t + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilties for the full-text search. + * + * :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + + + +/** + * Simple result scoring code. + */ +var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [filename, title, anchor, descr, score] + // and returns the new score. + /* + score: function(result) { + return result[4]; + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: {0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5}, // used to be unimportantResults + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + // query found in terms + term: 5 +}; + + +/** + * Search Module + */ +var Search = { + + _index : null, + _queued_query : null, + _pulse_status : -1, + + init : function() { + var params = $.getQueryParameters(); + if (params.q) { + var query = params.q[0]; + $('input[name="q"]')[0].value = query; + this.performSearch(query); + } + }, + + loadIndex : function(url) { + $.ajax({type: "GET", url: url, data: null, + dataType: "script", cache: true, + complete: function(jqxhr, textstatus) { + if (textstatus != "success") { + document.getElementById("searchindexloader").src = url; + } + }}); + }, + + setIndex : function(index) { + var q; + this._index = index; + if ((q = this._queued_query) !== null) { + this._queued_query = null; + Search.query(q); + } + }, + + hasIndex : function() { + return this._index !== null; + }, + + deferQuery : function(query) { + this._queued_query = query; + }, + + stopPulse : function() { + this._pulse_status = 0; + }, + + startPulse : function() { + if (this._pulse_status >= 0) + return; + function pulse() { + var i; + Search._pulse_status = (Search._pulse_status + 1) % 4; + var dotString = ''; + for (i = 0; i < Search._pulse_status; i++) + dotString += '.'; + Search.dots.text(dotString); + if (Search._pulse_status > -1) + window.setTimeout(pulse, 500); + } + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch : function(query) { + // create the required interface elements + this.out = $('#search-results'); + this.title = $('

' + _('Searching') + '

').appendTo(this.out); + this.dots = $('').appendTo(this.title); + this.status = $('

').appendTo(this.out); + this.output = $('
'); + } + // Prettify the comment rating. + comment.pretty_rating = comment.rating + ' point' + + (comment.rating == 1 ? '' : 's'); + // Make a class (for displaying not yet moderated comments differently) + comment.css_class = comment.displayed ? '' : ' moderate'; + // Create a div for this comment. + var context = $.extend({}, opts, comment); + var div = $(renderTemplate(commentTemplate, context)); + + // If the user has voted on this comment, highlight the correct arrow. + if (comment.vote) { + var direction = (comment.vote == 1) ? 'u' : 'd'; + div.find('#' + direction + 'v' + comment.id).hide(); + div.find('#' + direction + 'u' + comment.id).show(); + } + + if (opts.moderator || comment.text != '[deleted]') { + div.find('a.reply').show(); + if (comment.proposal_diff) + div.find('#sp' + comment.id).show(); + if (opts.moderator && !comment.displayed) + div.find('#cm' + comment.id).show(); + if (opts.moderator || (opts.username == comment.username)) + div.find('#dc' + comment.id).show(); + } + return div; + } + + /** + * A simple template renderer. Placeholders such as <%id%> are replaced + * by context['id'] with items being escaped. Placeholders such as <#id#> + * are not escaped. + */ + function renderTemplate(template, context) { + var esc = $(document.createElement('div')); + + function handle(ph, escape) { + var cur = context; + $.each(ph.split('.'), function() { + cur = cur[this]; + }); + return escape ? esc.text(cur || "").html() : cur; + } + + return template.replace(/<([%#])([\w\.]*)\1>/g, function() { + return handle(arguments[2], arguments[1] == '%' ? true : false); + }); + } + + /** Flash an error message briefly. */ + function showError(message) { + $(document.createElement('div')).attr({'class': 'popup-error'}) + .append($(document.createElement('div')) + .attr({'class': 'error-message'}).text(message)) + .appendTo('body') + .fadeIn("slow") + .delay(2000) + .fadeOut("slow"); + } + + /** Add a link the user uses to open the comments popup. */ + $.fn.comment = function() { + return this.each(function() { + var id = $(this).attr('id').substring(1); + var count = COMMENT_METADATA[id]; + var title = count + ' comment' + (count == 1 ? '' : 's'); + var image = count > 0 ? opts.commentBrightImage : opts.commentImage; + var addcls = count == 0 ? ' nocomment' : ''; + $(this) + .append( + $(document.createElement('a')).attr({ + href: '#', + 'class': 'sphinx-comment-open' + addcls, + id: 'ao' + id + }) + .append($(document.createElement('img')).attr({ + src: image, + alt: 'comment', + title: title + })) + .click(function(event) { + event.preventDefault(); + show($(this).attr('id').substring(2)); + }) + ) + .append( + $(document.createElement('a')).attr({ + href: '#', + 'class': 'sphinx-comment-close hidden', + id: 'ah' + id + }) + .append($(document.createElement('img')).attr({ + src: opts.closeCommentImage, + alt: 'close', + title: 'close' + })) + .click(function(event) { + event.preventDefault(); + hide($(this).attr('id').substring(2)); + }) + ); + }); + }; + + var opts = { + processVoteURL: '/_process_vote', + addCommentURL: '/_add_comment', + getCommentsURL: '/_get_comments', + acceptCommentURL: '/_accept_comment', + deleteCommentURL: '/_delete_comment', + commentImage: '/static/_static/comment.png', + closeCommentImage: '/static/_static/comment-close.png', + loadingImage: '/static/_static/ajax-loader.gif', + commentBrightImage: '/static/_static/comment-bright.png', + upArrow: '/static/_static/up.png', + downArrow: '/static/_static/down.png', + upArrowPressed: '/static/_static/up-pressed.png', + downArrowPressed: '/static/_static/down-pressed.png', + voting: false, + moderator: false + }; + + if (typeof COMMENT_OPTIONS != "undefined") { + opts = jQuery.extend(opts, COMMENT_OPTIONS); + } + + var popupTemplate = '\ +
\ +

\ + Sort by:\ + best rated\ + newest\ + oldest\ +

\ +
Comments
\ +
\ + loading comments...
\ +
    \ +
    \ +

    Add a comment\ + (markup):

    \ +
    \ + reStructured text markup: *emph*, **strong**, \ + ``code``, \ + code blocks: :: and an indented block after blank line
    \ +
    \ + \ +

    \ + \ + Propose a change ▹\ + \ + \ + Propose a change ▿\ + \ +

    \ + \ + \ + \ + \ + \ +
    \ +
    '; + + var commentTemplate = '\ +
    \ +
    \ +
    \ + \ + \ + \ + \ + \ + \ +
    \ +
    \ + \ + \ + \ + \ + \ + \ +
    \ +
    \ +
    \ +

    \ + <%username%>\ + <%pretty_rating%>\ + <%time.delta%>\ +

    \ +
    <#text#>
    \ +

    \ + \ + reply ▿\ + proposal ▹\ + proposal ▿\ + \ + \ +

    \ +
    \
    +<#proposal_diff#>\
    +        
    \ +
      \ +
      \ +
      \ +
      \ + '; + + var replyTemplate = '\ +
    • \ +
      \ +
      \ + \ + \ + \ + \ + \ + \ +
      \ +
    • '; + + $(document).ready(function() { + init(); + }); +})(jQuery); + +$(document).ready(function() { + // add comment anchors for all paragraphs that are commentable + $('.sphinx-has-comment').comment(); + + // highlight search words in search results + $("div.context").each(function() { + var params = $.getQueryParameters(); + var terms = (params.q) ? params.q[0].split(/\s+/) : []; + var result = $(this); + $.each(terms, function() { + result.highlightText(this.toLowerCase(), 'highlighted'); + }); + }); + + // directly open comment window if requested + var anchor = document.location.hash; + if (anchor.substring(0, 9) == '#comment-') { + $('#ao' + anchor.substring(9)).click(); + document.location.hash = '#s' + anchor.substring(9); + } +}); diff --git a/syncano/__init__.py b/build/lib/syncano/__init__.py similarity index 52% rename from syncano/__init__.py rename to build/lib/syncano/__init__.py index a50f4be..31df386 100644 --- a/syncano/__init__.py +++ b/build/lib/syncano/__init__.py @@ -2,14 +2,10 @@ import os __title__ = 'Syncano Python' -__version__ = '5.4.6' -__author__ = "Daniel Kopka, Michal Kobus and Sebastian Opalczynski" -__credits__ = ["Daniel Kopka", - "Michal Kobus", - "Sebastian Opalczynski", - "Robert Kopaczewski"] -__copyright__ = 'Copyright 2016 Syncano' +__version__ = '4.0.0' +__author__ = 'Daniel Kopka' __license__ = 'MIT' +__copyright__ = 'Copyright 2015 Syncano' env_loglevel = os.getenv('SYNCANO_LOGLEVEL', 'INFO') loglevel = getattr(logging, env_loglevel.upper(), None) @@ -28,12 +24,11 @@ # Few global env variables VERSION = __version__ DEBUG = env_loglevel.lower() == 'debug' -API_ROOT = os.getenv('SYNCANO_APIROOT', 'https://api.syncano.io/') +API_ROOT = os.getenv('SYNCANO_APIROOT', 'https://v4.hydraengine.com/') EMAIL = os.getenv('SYNCANO_EMAIL') 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): @@ -47,16 +42,7 @@ def connect(*args, **kwargs): :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: Instance user name - - :type user_key: string - :param user_key: Instance user key - - :type instance_name: string - :param instance_name: Your Syncano instance_name + :param api_key: Your Syncano account key :type verify_ssl: boolean :param verify_ssl: Verify SSL certificate @@ -66,25 +52,45 @@ def connect(*args, **kwargs): Usage:: - # Admin login 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='') - # OR - connection = syncano.connect(user_key='', api_key='', instance_name='') """ - from syncano.connection import DefaultConnection + from syncano.connection import default_connection from syncano.models import registry - registry.set_default_connection(DefaultConnection()) - registry.connection.open(*args, **kwargs) - instance = kwargs.get('instance_name', INSTANCE) - - if instance is not None: - registry.set_used_instance(instance) + default_connection.open(*args, **kwargs) + if INSTANCE: + 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 + + :type verify_ssl: boolean + :param verify_ssl: Verify SSL certificate + + :rtype: :class:`syncano.models.base.Instance` + :return: Instance object + + Usage:: + + my_instance = syncano.connect_instance('my_instance_name', email='', password='') + my_instance = syncano.connect_instance('my_instance_name', api_key='') + """ + name = name or INSTANCE + connection = connect(*args, **kwargs) + return connection.Instance.please.get(name) diff --git a/build/lib/syncano/connection.py b/build/lib/syncano/connection.py new file mode 100644 index 0000000..ffc3f70 --- /dev/null +++ b/build/lib/syncano/connection.py @@ -0,0 +1,273 @@ +import json +from urlparse import urljoin +from copy import deepcopy + +import requests +import six + +import syncano +from syncano.exceptions import SyncanoValueError, SyncanoRequestError + + +__all__ = ['default_connection', 'Connection', 'ConnectionMixin'] + + +def is_success(code): + """Checks if response code is successful.""" + return 200 <= code <= 299 + + +def is_client_error(code): + """Checks if response code has client error.""" + return 400 <= code <= 499 + + +def is_server_error(code): + """Checks if response code has server error.""" + return 500 <= code <= 599 + + +class DefaultConnection(object): + """Singleton class which holds default connection.""" + + def __init__(self): + self._connection = None + + def __call__(self): + if not self._connection: + raise SyncanoValueError('Please open new connection.') + return self._connection + + def open(self, *args, **kwargs): + connection = Connection(*args, **kwargs) + if not self._connection: + self._connection = connection + return connection + + +default_connection = DefaultConnection() + + +class Connection(object): + """Base connection class. + + :ivar host: Syncano API host + :ivar email: Your Syncano email address + :ivar password: Your Syncano password + :ivar api_key: Your Syncano ``Account Key`` + :ivar logger: Python logger instance + :ivar timeout: Default request timeout + :ivar verify_ssl: Verify SSL certificate + """ + + AUTH_SUFFIX = 'v1/account/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 + 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 build_params(self, params): + """ + :type params: dict + :param params: Params which will be passed to request + + :rtype: dict + :return: Request params + """ + params = deepcopy(params) + params['timeout'] = params.get('timeout') or self.timeout + params['headers'] = params.get('headers') or {} + + if 'content-type' not in params['headers']: + params['headers']['content-type'] = self.CONTENT_TYPE + + if 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 + if syncano.DEBUG or not self.verify_ssl: + params['verify'] = False + + return params + + def build_url(self, path): + """Ensures proper format for provided path. + + :type path: string + :param path: Request path + + :rtype: string + :return: Request URL + """ + if not isinstance(path, six.string_types): + raise SyncanoValueError('"path" should be a string.') + + query = None + + if path.startswith(self.host): + return path + + if '?' in path: + path, query = path.split('?', 1) + + if not path.endswith('/'): + path += '/' + + if path.startswith('/'): + path = path[1:] + + if query: + path = '{0}?{1}'.format(path, query) + + return urljoin(self.host, path) + + def request(self, method_name, path, **kwargs): + """Simple wrapper around :func:`~syncano.connection.Connection.make_request` which + will ensure that request is authenticated. + + :type method_name: string + :param method_name: HTTP request method e.g: GET + + :type path: string + :param path: Request path or full URL + + :rtype: dict + :return: JSON response + """ + + if not self.is_authenticated(): + self.authenticate() + + return self.make_request(method_name, path, **kwargs) + + def make_request(self, method_name, path, **kwargs): + """ + :type method_name: string + :param method_name: HTTP request method e.g: GET + + :type path: string + :param path: Request path or full URL + + :rtype: dict + :return: JSON response + + :raises SyncanoValueError: if invalid request method was chosen + :raises SyncanoRequestError: if something went wrong during the request + """ + params = self.build_params(kwargs) + method = getattr(self.session, method_name.lower(), None) + + # JSON dump can be expensive + if syncano.DEBUG: + formatted_params = json.dumps( + params, + sort_keys=True, + indent=2, + separators=(',', ': ') + ) + self.logger.debug('Request: %s %s\n%s', method_name, path, formatted_params) + + if method is None: + raise SyncanoValueError('Invalid request method: {0}.'.format(method_name)) + + # 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) + + try: + content = response.json() + except ValueError: + content = response.text + + if is_server_error(response.status_code): + raise SyncanoRequestError(response.status_code, 'Server error.') + + # Validation error + if is_client_error(response.status_code): + raise SyncanoRequestError(response.status_code, content) + + # Other errors + if not is_success(response.status_code): + self.logger.debug('Request Error: %s', url) + self.logger.debug('Status code: %d', response.status_code) + self.logger.debug('Response: %s', content) + raise SyncanoRequestError(response.status_code, content) + + return content + + def is_authenticated(self): + """Checks if current session is authenticated. + + :rtype: boolean + :return: Session authentication state + """ + + return self.api_key is not None + + def authenticate(self, email=None, password=None): + """ + :type email: string + :param email: Your Syncano account email address + + :type password: string + :param password: Your Syncano password + + :rtype: string + :return: Your ``Account Key`` + """ + + 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 not email: + raise SyncanoValueError('"email" is required.') + + if not password: + raise SyncanoValueError('"password" is required.') + + 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 + + +class ConnectionMixin(object): + """Injects connection attribute with support of basic validation.""" + + def __init__(self, *args, **kwargs): + self._connection = None + super(ConnectionMixin, self).__init__(*args, **kwargs) + + @property + def connection(self): + # Sometimes someone will not use super + return getattr(self, '_connection', None) or default_connection() + + @connection.setter + def connection(self, value): + if not isinstance(value, Connection): + raise SyncanoValueError('"connection" needs to be a Syncano Connection instance.') + self._connection = value + + @connection.deleter + def connection(self): + self._connection = None diff --git a/syncano/exceptions.py b/build/lib/syncano/exceptions.py similarity index 83% rename from syncano/exceptions.py rename to build/lib/syncano/exceptions.py index 1e2e7d8..0e38ec4 100644 --- a/syncano/exceptions.py +++ b/build/lib/syncano/exceptions.py @@ -32,9 +32,7 @@ def __init__(self, status_code, reason, *args): self.status_code = status_code if isinstance(reason, dict): - joined_details = (''.join(reason.get(k, '')) for k in ['detail', 'error', '__all__']) - message = ''.join(joined_details) - + message = reason.get('detail', '') or reason.get('error', '') if not message: for name, value in six.iteritems(reason): if isinstance(value, (list, dict)): @@ -71,11 +69,3 @@ def __str__(self): class SyncanoDoesNotExist(SyncanoException): """Syncano object doesn't exist error occurred.""" - - -class RevisionMismatchException(SyncanoRequestError): - """Revision do not match with expected one""" - - -class UserNotFound(SyncanoRequestError): - """Special error to handle user not found case.""" diff --git a/build/lib/syncano/models/__init__.py b/build/lib/syncano/models/__init__.py new file mode 100644 index 0000000..4d05d6a --- /dev/null +++ b/build/lib/syncano/models/__init__.py @@ -0,0 +1,2 @@ +from .base import * # NOQA +from .fields import * # NOQA diff --git a/syncano/models/accounts.py b/build/lib/syncano/models/accounts.py similarity index 58% rename from syncano/models/accounts.py rename to build/lib/syncano/models/accounts.py index 1e79eca..eff8014 100644 --- a/syncano/models/accounts.py +++ b/build/lib/syncano/models/accounts.py @@ -1,15 +1,13 @@ -from syncano.exceptions import SyncanoRequestError, SyncanoValueError, UserNotFound +from __future__ import unicode_literals from . import fields from .base import Model -from .classes import Class, DataObjectMixin, Object from .instances import Instance -from .manager import ObjectManager class Admin(Model): """ - OO wrapper around instance admins `link `_. + OO wrapper around instance admins `endpoint `_. :ivar first_name: :class:`~syncano.models.fields.StringField` :ivar last_name: :class:`~syncano.models.fields.StringField` @@ -17,6 +15,9 @@ 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'}, @@ -27,7 +28,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.LinksField() + links = fields.HyperlinkedField(links=LINKS) class Meta: parent = Instance @@ -37,23 +38,23 @@ class Meta: 'path': '/admins/{id}/', }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], 'path': '/admins/', } } -class Profile(DataObjectMixin, Object): +class Profile(Model): """ """ - - PREDEFINED_CLASS_NAME = 'user_profile' + 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'}, + {'display_name': 'Create users', 'value': 'create_users'}, ) owner = fields.IntegerField(label='owner id', required=False, read_only=True) @@ -64,29 +65,27 @@ class Profile(DataObjectMixin, Object): channel = fields.StringField(required=False) channel_room = fields.StringField(required=False, max_length=64) - links = fields.LinksField() + 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}/', + 'methods': ['delete', 'patch', 'put', 'get'], + 'path': '/user_profile/objects/{id}/', }, 'list': { - 'methods': ['get', 'post'], - 'path': '/objects/', + 'methods': ['get'], + 'path': '/user_profile/objects/', } } - please = ObjectManager() - class User(Model): """ - OO wrapper around users `link `_. + OO wrapper around users `endpoint `_. :ivar username: :class:`~syncano.models.fields.StringField` :ivar password: :class:`~syncano.models.fields.StringField` @@ -95,15 +94,17 @@ 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) user_key = fields.StringField(read_only=True, required=False) - profile = fields.ModelField('Profile', read_only=False, default={}, - just_pk=False, is_data_object_mixin=True) + profile = fields.ModelField('Profile') - links = fields.LinksField() + links = fields.HyperlinkedField(links=LINKS) created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) @@ -118,81 +119,40 @@ class Meta: 'methods': ['post'], 'path': '/users/{id}/reset_key/', }, - 'auth': { - 'methods': ['post'], - 'path': '/user/auth/', - }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], 'path': '/users/', }, 'groups': { - 'methods': ['get', 'post', 'delete'], + 'methods': ['get', 'post'], 'path': '/users/{id}/groups/', } } def reset_key(self): properties = self.get_endpoint_data() - http_method = 'POST' - endpoint = self._meta.resolve_endpoint('reset_key', properties, http_method) + endpoint = self._meta.resolve_endpoint('reset_key', properties) connection = self._get_connection() - return connection.request(http_method, endpoint) - - def auth(self, username=None, password=None): - properties = self.get_endpoint_data() - http_method = 'POST' - endpoint = self._meta.resolve_endpoint('auth', properties, http_method) - connection = self._get_connection() - - if not (username and password): - raise SyncanoValueError('You need provide username and password.') + return connection.request('POST', endpoint) - data = { - 'username': username, - 'password': password - } - - return connection.request(http_method, endpoint, data=data) - - 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, method) - - if group_id is not None and method != 'POST': + endpoint = self._meta.resolve_endpoint('groups', properties) + if group_id is not None: endpoint += '{}/'.format(group_id) connection = self._get_connection() - - 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') + return connection.request(method, endpoint) 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) - - def remove_from_group(self, group_id): - return self._user_groups_method(group_id, method='DELETE') + return self.user_groups_method(group_id) class Group(Model): """ - OO wrapper around groups `link `_. + OO wrapper around groups `endpoint `_. :ivar label: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` @@ -200,11 +160,14 @@ 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.LinksField() + links = fields.HyperlinkedField(links=LINKS) created_at = fields.DateTimeField(read_only=True, required=False) updated_at = fields.DateTimeField(read_only=True, required=False) @@ -216,7 +179,7 @@ class Meta: 'path': '/groups/{id}/', }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], 'path': '/groups/', }, 'users': { @@ -225,40 +188,22 @@ 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, method) - if user_id is not None and method != 'POST': + endpoint = self._meta.resolve_endpoint('users', properties) + if user_id is not None: endpoint += '{}/'.format(user_id) connection = self._get_connection() - - data = {} - if method == 'POST': - data = {'user': user_id} - - try: - 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 - - if method == 'DELETE': - return - - if 'objects' in response: - return [User(**user_response['user']) for user_response in response['objects']] - - return User(**response['user']) + 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') diff --git a/syncano/models/archetypes.py b/build/lib/syncano/models/archetypes.py similarity index 71% rename from syncano/models/archetypes.py rename to build/lib/syncano/models/archetypes.py index 236da8b..1654897 100644 --- a/syncano/models/archetypes.py +++ b/build/lib/syncano/models/archetypes.py @@ -1,4 +1,4 @@ - +from __future__ import unicode_literals import inspect @@ -18,7 +18,6 @@ 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 getattr(b.Meta, 'abstract', None)] if not parents: return super_new(cls, name, bases, attrs) @@ -38,11 +37,6 @@ 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 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) - if not meta.pk: pk_field = fields.IntegerField(primary_key=True, read_only=True, required=False) @@ -83,9 +77,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 = {} self.to_python(kwargs) @@ -134,35 +126,13 @@ def save(self, **kwargs): if 'put' in methods: method = 'PUT' - endpoint = self._meta.resolve_endpoint(endpoint_name, properties, method) - if 'expected_revision' in kwargs: - data.update({'expected_revision': kwargs['expected_revision']}) + endpoint = self._meta.resolve_endpoint(endpoint_name, properties) request = {'data': data} - if not self.is_lazy: - response = connection.request(method, endpoint, **request) - self.to_python(response) - return self - - return self.batch_object(method=method, path=endpoint, body=request['data'], properties=data) - - @classmethod - def batch_object(cls, method, path, body, properties=None): - properties = properties if properties else {} - return { - 'body': { - 'method': method, - 'path': path, - 'body': body, - }, - 'meta': { - 'model': cls, - 'properties': properties - } - } - - def mark_for_batch(self): - self.is_lazy = True + response = connection.request(method, endpoint, **request) + + self.to_python(response) + return self def delete(self, **kwargs): """Removes the current instance. @@ -171,12 +141,9 @@ def delete(self, **kwargs): raise SyncanoValidationError('Method allowed only on existing model.') properties = self.get_endpoint_data() - http_method = 'DELETE' - endpoint = self._meta.resolve_endpoint('detail', properties, http_method) + endpoint = self._meta.resolve_endpoint('detail', properties) connection = self._get_connection(**kwargs) - connection.request(http_method, endpoint) - if self.__class__.__name__ == 'Instance': # avoid circular import; - registry.clear_used_instance() + connection.request('DELETE', endpoint) self._raw_data = {} def reload(self, **kwargs): @@ -186,10 +153,9 @@ def reload(self, **kwargs): raise SyncanoValidationError('Method allowed only on existing model.') properties = self.get_endpoint_data() - http_method = 'GET' - endpoint = self._meta.resolve_endpoint('detail', properties, http_method) + endpoint = self._meta.resolve_endpoint('detail', properties) connection = self._get_connection(**kwargs) - response = connection.request(http_method, endpoint) + response = connection.request('GET', endpoint) self.to_python(response) def validate(self): @@ -227,28 +193,16 @@ def to_python(self, data): :type data: dict :param data: Raw data """ - for field in self._meta.fields: field_name = field.name - # 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 and self.is_new(): + 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) - 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. @@ -257,18 +211,14 @@ 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 value is None and field.blank: + 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) - 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) + data[param_name] = field.to_native(value) return data def get_endpoint_data(self): diff --git a/syncano/models/backups.py b/build/lib/syncano/models/backups.py similarity index 100% rename from syncano/models/backups.py rename to build/lib/syncano/models/backups.py diff --git a/build/lib/syncano/models/base.py b/build/lib/syncano/models/base.py new file mode 100644 index 0000000..2539e43 --- /dev/null +++ b/build/lib/syncano/models/base.py @@ -0,0 +1,813 @@ +from __future__ import unicode_literals + +import inspect +import json + +import six + +from syncano.exceptions import SyncanoValidationError, SyncanoDoesNotExist +from . import fields +from .options import Options +from .manager import Manager, WebhookManager, ObjectManager, CodeBoxManager +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 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: + 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 + data[field.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 + + +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': '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', 'post', 'patch', '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 links: :class:`~syncano.models.fields.HyperlinkedField` + """ + LINKS = [ + {'type': 'detail', 'name': 'self'}, + ] + + api_key = fields.StringField(read_only=True, required=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` + + .. 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'}, + ] + + 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) + + class Meta: + parent = Instance + plural_name = 'Classes' + endpoints = { + 'detail': { + 'methods': ['delete', 'post', 'patch', 'get'], + 'path': '/classes/{name}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/classes/', + } + } + + +class CodeBox(Model): + """ + OO wrapper around codeboxes `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 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, 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(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'}, + ) + + description = fields.StringField(required=False) + links = fields.HyperlinkedField(links=LINKS) + source = fields.StringField() + runtime_name = fields.ChoiceField(choices=RUNTIME_CHOICES) + config = fields.Field(required=False) + name = fields.StringField(max_length=80) + 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): + 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) + } + } + return connection.request('POST', endpoint, **request) + + +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 created_at: :class:`~syncano.models.fields.DateTimeField` + :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` + """ + + LINKS = [ + {'type': 'detail', 'name': 'self'}, + {'type': 'list', 'name': 'traces'}, + {'type': 'list', 'name': 'codebox'}, + ] + + 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': ['get', 'delete'], + 'path': '/schedules/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/schedules/', + } + } + + +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'}, + ) + 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` + + .. note:: + This model is special because each instance will be **dynamically populated** + with fields defined in related :class:`~syncano.models.base.Class` schema attribute. + """ + + 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) + + please = ObjectManager() + + class Meta: + parent = Class + endpoints = { + 'detail': { + 'methods': ['delete', 'post', 'patch', 'get'], + 'path': '/objects/{id}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/objects/', + } + } + + @classmethod + def create_subclass(cls, name, schema): + attrs = {'Meta': cls._meta} + + 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), (cls, ), 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 + + +class Trigger(Model): + """ + OO wrapper around triggers `endpoint `_. + + :ivar codebox: :class:`~syncano.models.fields.IntegerField` + :ivar klass: :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': 'klass'}, + {'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'}, + ) + + codebox = fields.IntegerField(label='codebox id') + klass = fields.StringField(label='class name') + 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 Webhook(Model): + """ + OO wrapper around webhooks `endpoint `_. + + :ivar slug: :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') + + or via instance:: + + >>> wh = Webhook.please.get('instance-name', 'webhook-slug') + >>> wh.run() + """ + + LINKS = ( + {'type': 'detail', 'name': 'self'}, + {'type': 'detail', 'name': 'codebox'}, + ) + + slug = fields.SlugField(max_length=50, primary_key=True) + codebox = fields.IntegerField(label='codebox id') + links = fields.HyperlinkedField(links=LINKS) + + please = WebhookManager() + + class Meta: + parent = Instance + endpoints = { + 'detail': { + 'methods': ['put', 'get', 'patch', 'delete'], + 'path': '/webhooks/{slug}/', + }, + 'list': { + 'methods': ['post', 'get'], + 'path': '/webhooks/', + }, + 'run': { + 'methods': ['get'], + 'path': '/webhooks/{slug}/run/', + } + } + + def run(self, **kwargs): + 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(**kwargs) + return connection.request('GET', endpoint) diff --git a/syncano/models/billing.py b/build/lib/syncano/models/billing.py similarity index 75% rename from syncano/models/billing.py rename to build/lib/syncano/models/billing.py index 89ee8ab..5fd9fd2 100644 --- a/syncano/models/billing.py +++ b/build/lib/syncano/models/billing.py @@ -1,4 +1,4 @@ - +from __future__ import unicode_literals from . import fields from .base import Model @@ -6,7 +6,7 @@ class Coupon(Model): """ - OO wrapper around coupons `link `_. + OO wrapper around coupons `endpoint `_. :ivar name: :class:`~syncano.models.fields.StringField` :ivar redeem_by: :class:`~syncano.models.fields.DateField` @@ -17,13 +17,17 @@ 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.LinksField() + links = fields.HyperlinkedField(links=LINKS) percent_off = fields.IntegerField(required=False) amount_off = fields.FloatField(required=False) currency = fields.ChoiceField(choices=CURRENCY_CHOICES) @@ -33,18 +37,18 @@ class Meta: endpoints = { 'detail': { 'methods': ['get', 'delete'], - 'path': '/v1.1/billing/coupons/{name}/', + 'path': '/v1/billing/coupons/{name}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/v1.1/billing/coupons/', + 'path': '/v1/billing/coupons/', } } class Discount(Model): """ - OO wrapper around discounts `link `_. + OO wrapper around discounts `endpoint `_. :ivar instance: :class:`~syncano.models.fields.ModelField` :ivar coupon: :class:`~syncano.models.fields.ModelField` @@ -53,20 +57,24 @@ 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.LinksField() + links = fields.HyperlinkedField(links=LINKS) class Meta: endpoints = { 'detail': { 'methods': ['get'], - 'path': '/v1.1/billing/discounts/{id}/', + 'path': '/v1/billing/discounts/{id}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/v1.1/billing/discounts/', + 'path': '/v1/billing/discounts/', } } diff --git a/syncano/models/bulk.py b/build/lib/syncano/models/bulk.py similarity index 97% rename from syncano/models/bulk.py rename to build/lib/syncano/models/bulk.py index 4786ff1..ba95abf 100644 --- a/syncano/models/bulk.py +++ b/build/lib/syncano/models/bulk.py @@ -1,17 +1,18 @@ # -*- coding: utf-8 -*- from abc import ABCMeta, abstractmethod -import six from syncano.exceptions import SyncanoValidationError, SyncanoValueError -class BaseBulkCreate(six.with_metaclass(ABCMeta)): +class BaseBulkCreate(object): """ Helper class for making bulk create; Usage: instances = ObjectBulkCreate(objects, manager).process() """ + __metaclass__ = ABCMeta + MAX_BATCH_SIZE = 50 @abstractmethod diff --git a/syncano/models/channels.py b/build/lib/syncano/models/channels.py similarity index 86% rename from syncano/models/channels.py rename to build/lib/syncano/models/channels.py index 65bdbd3..9c5a653 100644 --- a/syncano/models/channels.py +++ b/build/lib/syncano/models/channels.py @@ -2,11 +2,10 @@ import six from requests import Timeout + from syncano import logger -from . import fields -from .base import Model -from .instances import Instance +from .base import Instance, Model, fields class PollThread(Thread): @@ -66,7 +65,7 @@ class Channel(Model): """ .. _long polling: http://en.wikipedia.org/wiki/Push_technology#Long_polling - OO wrapper around channels `link http://docs.syncano.io/docs/realtime-communication`_. + OO wrapper around channels `endpoint `_. :ivar name: :class:`~syncano.models.fields.StringField` :ivar type: :class:`~syncano.models.fields.ChoiceField` @@ -93,23 +92,22 @@ class Channel(Model): """ TYPE_CHOICES = ( - {'display_name': 'Default', 'value': 'default'}, - {'display_name': 'Separate rooms', 'value': 'separate_rooms'}, + {'display_name': 'Default', 'value': 0}, + {'display_name': 'Separate rooms', 'value': 1}, ) PERMISSIONS_CHOICES = ( - {'display_name': 'None', 'value': 'none'}, - {'display_name': 'Subscribe', 'value': 'subscribe'}, - {'display_name': 'Publish', 'value': 'publish'}, + {'display_name': 'none', 'value': 0}, + {'display_name': 'subscribe', 'value': 1}, + {'display_name': 'publish', 'value': 2}, ) name = fields.StringField(max_length=64, primary_key=True) - type = fields.ChoiceField(choices=TYPE_CHOICES, required=False, default='default') + type = fields.ChoiceField(choices=TYPE_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') - custom_publish = fields.BooleanField(default=False, required=False) - links = fields.LinksField() + group_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default=0) + other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, default=0) + custom_publish = fields.BooleanField(default=False) class Meta: parent = Instance @@ -138,7 +136,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, http_method='GET') + endpoint = self._meta.resolve_endpoint('poll', properties) connection = self._get_connection() thread = PollThread(connection, endpoint, callback, error, timeout=timeout, @@ -148,17 +146,16 @@ 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() - http_method = 'POST' - endpoint = self._meta.resolve_endpoint('publish', properties, http_method) + endpoint = self._meta.resolve_endpoint('publish', properties) connection = self._get_connection() request = {'data': Message(payload=payload, room=room).to_native()} - response = connection.request(http_method, endpoint, **request) + response = connection.request('POST', endpoint, **request) return Message(**response) class Message(Model): """ - OO wrapper around channel hisotry `link http://docs.syncano.io/docs/realtime-communication`_. + OO wrapper around channel hisotry `endpoint `_. :ivar room: :class:`~syncano.models.fields.StringField` :ivar action: :class:`~syncano.models.fields.ChoiceField` @@ -190,7 +187,7 @@ class Meta: 'path': '/history/{pk}/', }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], 'path': '/history/', }, } diff --git a/syncano/models/classes.py b/build/lib/syncano/models/classes.py similarity index 67% rename from syncano/models/classes.py rename to build/lib/syncano/models/classes.py index 0027c04..38b78cd 100644 --- a/syncano/models/classes.py +++ b/build/lib/syncano/models/classes.py @@ -1,4 +1,4 @@ - +from __future__ import unicode_literals from copy import deepcopy @@ -14,7 +14,7 @@ class Class(Model): """ - OO wrapper around instance classes `link `_. + OO wrapper around instance classes `endpoint `_. :ivar name: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` @@ -30,13 +30,17 @@ 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 **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'}, @@ -47,8 +51,8 @@ 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) - links = fields.LinksField() + schema = fields.SchemaField(read_only=False, required=True) + links = fields.HyperlinkedField(links=LINKS) status = fields.Field() metadata = fields.JSONField(read_only=False, required=False) @@ -61,8 +65,6 @@ 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' @@ -77,15 +79,10 @@ class Meta: } } - def save(self, **kwargs): - if self.schema: # do not allow add empty schema to registry; - registry.set_schema(self.name, self.schema.schema) # update the registry schema here; - return super(Class, self).save(**kwargs) - class Object(Model): """ - OO wrapper around data objects `link `_. + OO wrapper around data objects `endpoint `_. :ivar revision: :class:`~syncano.models.fields.IntegerField` :ivar created_at: :class:`~syncano.models.fields.DateTimeField` @@ -102,7 +99,6 @@ 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'}, @@ -114,11 +110,11 @@ 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) - owner_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, 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, required=False) - other_permissions = fields.ChoiceField(choices=PERMISSIONS_CHOICES, 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) @@ -139,8 +135,9 @@ class Meta: @staticmethod def __new__(cls, **kwargs): - instance_name = cls._get_instance_name(kwargs) - class_name = cls._get_class_name(kwargs) + instance_name = kwargs.get('instance_name') + class_name = kwargs.get('class_name') + if not instance_name: raise SyncanoValidationError('Field "instance_name" is required.') @@ -150,44 +147,21 @@ def __new__(cls, **kwargs): model = cls.get_subclass_model(instance_name, class_name) return model(**kwargs) - @classmethod - def _set_up_object_class(cls, model): - pass - - @classmethod - def _get_instance_name(cls, kwargs): - return kwargs.get('instance_name') or registry.instance_name - - @classmethod - def _get_class_name(cls, kwargs): - return kwargs.get('class_name') - @classmethod def create_subclass(cls, name, schema): - meta = deepcopy(Object._meta) attrs = { - 'Meta': meta, + 'Meta': deepcopy(Object._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) - field_class(required=False, read_only=False, query_allowed=query_allowed).contribute_to_class( - model, field.get('name') - ) + attrs[field['name']] = field_class(required=False, read_only=False, + query_allowed=query_allowed) - for field in meta.fields: - if field.primary_key: - setattr(model, 'pk', field) - setattr(model, field.name, field) - - cls._set_up_object_class(model) - return model + return type(str(name), (Object, ), attrs) @classmethod def get_or_create_subclass(cls, name, schema): @@ -196,6 +170,7 @@ def get_or_create_subclass(cls, name, schema): except LookupError: subclass = cls.create_subclass(name, schema) registry.add(name, subclass) + return subclass @classmethod @@ -204,13 +179,9 @@ def get_subclass_name(cls, instance_name, class_name): @classmethod def get_class_schema(cls, instance_name, class_name): - schema = registry.get_schema(class_name) - if not schema: - 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 + 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): @@ -226,45 +197,8 @@ def get_subclass_model(cls, instance_name, class_name, **kwargs): try: model = registry.get_model_by_name(model_name) except LookupError: - parent = cls._meta.parent - schema = parent.please.get(instance_name, class_name).schema + 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) - - 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 - return model - - -class DataObjectMixin(object): - - @classmethod - def _get_instance_name(cls, kwargs): - return cls.please.properties.get('instance_name') or kwargs.get('instance_name') - - @classmethod - def _get_class_name(cls, kwargs): - return cls.PREDEFINED_CLASS_NAME - - @classmethod - def get_class_object(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_object', cls.get_class_object) - setattr(model, '_get_instance_name', cls._get_instance_name) - setattr(model, '_get_class_name', cls._get_class_name) diff --git a/syncano/models/custom_response.py b/build/lib/syncano/models/custom_response.py similarity index 74% rename from syncano/models/custom_response.py rename to build/lib/syncano/models/custom_response.py index 3881b6d..7e07327 100644 --- a/syncano/models/custom_response.py +++ b/build/lib/syncano/models/custom_response.py @@ -8,38 +8,37 @@ class CustomResponseHandler(object): A helper class which allows to define and maintain custom response handlers. Consider an example: - Script code:: + CodeBox 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 CodeBoxTrace is used: - trace = ScriptTrace.please.get(id=, script=) + >> 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:: + So this is possible: - trace.content['one'] + >> trace.content['one'] - And the trace.content is equal to:: + And the trace.content is equal to: + >> {'one': 1} - {'one': 1} + The handler can be easily overwrite: - The handler can be easily overwrite:: + def custom_handler(response): + return json.loads(response['response']['content'])['one'] - def custom_handler(response): - return json.loads(response['response']['content'])['one'] + trace.response_handler.overwrite_handler('application/json', custom_handler) - trace.response_handler.overwrite_handler('application/json', custom_handler) + or globally: - 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 + Then trace.content is equal to: + >> 1 Currently supported content_types (but any handler can be defined): * application/json @@ -97,10 +96,10 @@ def plain_handler(response): class CustomResponseMixin(object): """ - 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; + 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. diff --git a/syncano/models/custom_sockets.py b/build/lib/syncano/models/custom_sockets.py similarity index 95% rename from syncano/models/custom_sockets.py rename to build/lib/syncano/models/custom_sockets.py index 3e791ec..441d59c 100644 --- a/syncano/models/custom_sockets.py +++ b/build/lib/syncano/models/custom_sockets.py @@ -25,7 +25,6 @@ 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) @@ -59,18 +58,13 @@ 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, config=None): + 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() - config = config or {} - response = connection.request('POST', install_path, data={ - 'name': self.name, - 'install_url': url, - 'config': config - }) + response = connection.request('POST', install_path, data={'name': self.name, 'install_url': url}) return response diff --git a/syncano/models/custom_sockets_utils.py b/build/lib/syncano/models/custom_sockets_utils.py similarity index 84% rename from syncano/models/custom_sockets_utils.py rename to build/lib/syncano/models/custom_sockets_utils.py index 45d1196..96eaf6e 100644 --- a/syncano/models/custom_sockets_utils.py +++ b/build/lib/syncano/models/custom_sockets_utils.py @@ -2,7 +2,6 @@ import six from syncano.exceptions import SyncanoValueError -from .classes import Class from .incentives import Script, ScriptEndpoint @@ -18,7 +17,6 @@ class DependencyType(object): The type of the dependency object used in the custom socket; """ SCRIPT = 'script' - CLASS = 'class' class BaseCall(object): @@ -111,9 +109,7 @@ def to_dependency_data(self): return dependency_data def get_name(self): - if self.name is not None: - return {'name': self.name} - return {'name': self.dependency_object.name} + raise NotImplementedError() def get_dependency_data(self): raise NotImplementedError() @@ -121,9 +117,6 @@ 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): """ @@ -154,6 +147,11 @@ 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): @@ -163,7 +161,9 @@ def get_dependency_data(self): script = self.dependency_object dependency_data = self.get_name() - dependency_data.update(self._build_dict(script)) + dependency_data.update({ + field_name: getattr(script, field_name) for field_name in self.fields + }) return dependency_data @classmethod @@ -174,41 +174,6 @@ 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. @@ -274,8 +239,6 @@ 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/syncano/models/data_views.py b/build/lib/syncano/models/data_views.py similarity index 51% rename from syncano/models/data_views.py rename to build/lib/syncano/models/data_views.py index 69dd488..22bd543 100644 --- a/syncano/models/data_views.py +++ b/build/lib/syncano/models/data_views.py @@ -1,15 +1,11 @@ -import json - -import six -from syncano.exceptions import SyncanoValueError -from syncano.models.incentives import ResponseTemplate +from __future__ import unicode_literals from . import fields -from .base import Model, Object +from .base import Model from .instances import Instance -class DataEndpoint(Model): +class DataView(Model): """ :ivar name: :class:`~syncano.models.fields.StringField` :ivar description: :class:`~syncano.models.fields.StringField` @@ -22,6 +18,11 @@ class DataEndpoint(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'}, @@ -32,7 +33,7 @@ class DataEndpoint(Model): name = fields.StringField(max_length=64, primary_key=True) description = fields.StringField(required=False) - query = fields.JSONField(read_only=False, required=False) + query = fields.JSONField(read_only=False, required=True) class_name = fields.StringField(label='class name', mapping='class') @@ -41,92 +42,54 @@ class DataEndpoint(Model): order_by = fields.StringField(required=False) page_size = fields.IntegerField(required=False) - links = fields.LinksField() + links = fields.HyperlinkedField(links=LINKS) class Meta: parent = Instance + plural_name = 'DataViews' endpoints = { 'detail': { 'methods': ['get', 'put', 'patch', 'delete'], - 'path': '/endpoints/data/{name}/', + 'path': '/api/objects/{name}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/endpoints/data/', + 'path': '/api/objects/', }, 'get': { 'methods': ['get'], - 'path': '/endpoints/data/{name}/get/', + 'path': '/api/objects/{name}/get/', }, 'rename': { 'methods': ['post'], - 'path': '/endpoints/data/{name}/rename/', + 'path': '/api/objects/{name}/rename/', }, 'clear_cache': { 'methods': ['post'], - 'path': '/endpoints/data/{name}/clear_cache/', + 'path': '/api/objects/{name}/clear_cache/', } } def rename(self, new_name): properties = self.get_endpoint_data() - http_method = 'POST' - endpoint = self._meta.resolve_endpoint('rename', properties, http_method) + endpoint = self._meta.resolve_endpoint('rename', properties) connection = self._get_connection() - return connection.request(http_method, + return connection.request('POST', endpoint, data={'new_name': new_name}) def clear_cache(self): properties = self.get_endpoint_data() - http_method = 'POST' - endpoint = self._meta.resolve_endpoint('clear_cache', properties, http_method) + endpoint = self._meta.resolve_endpoint('clear_cache', properties) connection = self._get_connection() - return connection.request(http_method, endpoint) + return connection.request('POST', endpoint) - def get(self, cache_key=None, response_template=None, **kwargs): - connection = self._get_connection() + def get(self): properties = self.get_endpoint_data() - query = Object.please._build_query(query_data=kwargs, class_name=self.class_name) - - http_method = 'GET' - endpoint = self._meta.resolve_endpoint('get', properties, http_method) - - kwargs = {} - params = {} - params.update({'query': json.dumps(query)}) - - if cache_key is not None: - params = {'cache_key': cache_key} - - if params: - kwargs = {'params': params} - - if response_template: - template_name = self._get_response_template_name(response_template) - kwargs['headers'] = { - 'X-TEMPLATE-RESPONSE': template_name - } - + endpoint = self._meta.resolve_endpoint('get', properties) + connection = self._get_connection() while endpoint is not None: - response = connection.request(http_method, endpoint, **kwargs) - 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 - - def add_object(self, **kwargs): - return Object(instance_name=self.instance_name, class_name=self.class_name, **kwargs).save() + response = connection.request('GET', endpoint) + endpoint = response.get('next') + for obj in response['objects']: + yield obj diff --git a/syncano/models/fields.py b/build/lib/syncano/models/fields.py similarity index 54% rename from syncano/models/fields.py rename to build/lib/syncano/models/fields.py index 967fd64..36fe6ba 100644 --- a/syncano/models/fields.py +++ b/build/lib/syncano/models/fields.py @@ -4,28 +4,12 @@ import six import validictory -from syncano import PUSH_ENV, logger + +from syncano import logger from syncano.exceptions import SyncanoFieldError, SyncanoValueError from syncano.utils import force_text - -from .geo import Distance, GeoPoint -from .manager import SchemaManager +from .manager import RelatedManagerDescriptor, SchemaManager from .registry import registry -from .relations import RelationManager, RelationValidatorMixin - - -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): @@ -41,10 +25,8 @@ class Field(object): has_endpoint_data = False query_allowed = True - allow_increment = False creation_counter = 0 - field_lookups = [] def __init__(self, name=None, **kwargs): self.name = name @@ -54,7 +36,6 @@ 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) @@ -78,8 +59,6 @@ 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 @@ -94,13 +73,12 @@ def __unicode__(self): return six.u(repr(self)) def __get__(self, instance, owner): - if instance is not None: - return instance._raw_data.get(self.name, self.default) + 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): - logger.debug('Field "{0}"" is read only, ' - 'your changes will not be saved.'.format(self.name)) + logger.warning('Field "{0}"" is read only, ' + 'your changes will not be saved.'.format(self.name)) instance._raw_data[self.name] = self.to_python(value) @@ -128,9 +106,6 @@ def to_python(self, value): """ Returns field's value prepared for usage in Python. """ - if isinstance(value, dict) and 'type' in value and 'value' in value: - return value['value'] - return value def to_native(self, value): @@ -139,7 +114,7 @@ def to_native(self, value): """ return value - def to_query(self, value, lookup_type, **kwargs): + def to_query(self, value, lookup_type): """ Returns field's value prepared for usage in HTTP request query. """ @@ -178,27 +153,6 @@ def contribute_to_class(self, cls, name): setattr(self, 'ValidationError', error_class) -class RelatedManagerField(Field): - - def __init__(self, model_name, endpoint='list', *args, **kwargs): - super(RelatedManagerField, self).__init__(*args, **kwargs) - self.model_name = model_name - self.endpoint = endpoint - - 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 @@ -215,58 +169,28 @@ 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) - if isinstance(value, six.string_types) or value is None: return value return force_text(value) class IntegerField(WritableField): - allow_increment = True def to_python(self, value): - value = super(IntegerField, self).to_python(value) - if value is None: - return + return value try: return int(value) except (TypeError, ValueError): raise self.ValidationError('Invalid value. Value should be an integer.') -class ReferenceField(IntegerField): - - def to_python(self, value): - if isinstance(value, int): - return value - - if hasattr(value, 'pk') and isinstance(value.pk, int): - value = value.pk - - return super(ReferenceField, self).to_python(value) - - class FloatField(WritableField): - allow_increment = True def to_python(self, value): - value = super(FloatField, self).to_python(value) - if value is None: - return + return value try: return float(value) except (TypeError, ValueError): @@ -276,15 +200,13 @@ def to_python(self, value): 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 (True, 't', 'true', 'True', '1'): + if value in ('t', 'True', '1'): return True - if value in (False, 'f', 'false', 'False', '0'): + if value in ('f', 'False', '0'): return False raise self.ValidationError('Invalid value. Value should be a boolean.') @@ -329,7 +251,7 @@ def __init__(self, *args, **kwargs): def validate(self, value, model_instance): super(ChoiceField, self).validate(value, model_instance) - if self.choices and value is not None and value not in self.allowed_values: + if self.choices and value not in self.allowed_values: raise self.ValidationError("Value '{0}' is not a valid choice.".format(value)) @@ -339,10 +261,8 @@ class DateField(WritableField): ) def to_python(self, value): - value = super(DateField, self).to_python(value) - if value is None: - return + return value if isinstance(value, datetime): return value.date() @@ -381,10 +301,7 @@ class DateTimeField(DateField): def to_python(self, value): if value is None: - return - - if isinstance(value, dict) and 'type' in value and 'value' in value: - value = value['value'] + return value if isinstance(value, datetime): return value @@ -427,53 +344,32 @@ def parse_from_date(self, value): def to_native(self, value): if value is None: - return - ret = value.strftime(self.FORMAT) + return value + ret = value.isoformat() if ret.endswith('+00:00'): ret = ret[:-6] + 'Z' - - if not ret.endswith('Z'): - ret = ret + 'Z' - return ret -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: - value = self.links_dict.get(item) - if not value: - item = item.replace('_', '-') - value = self.links_dict.get(item) - - if not value: - raise - - return value - - def to_native(self): - return self.links_dict - - -class LinksField(Field): +class HyperlinkedField(Field): query_allowed = False IGNORED_LINKS = ('self', ) def __init__(self, *args, **kwargs): - super(LinksField, self).__init__(*args, **kwargs) + self.links = kwargs.pop('links', []) + super(HyperlinkedField, self).__init__(*args, **kwargs) - def to_python(self, value): - return LinksWrapper(value, self.IGNORED_LINKS) + def contribute_to_class(self, cls, name): + super(HyperlinkedField, self).contribute_to_class(cls, name) - def to_native(self, value): - return value.to_native() + for link in self.links: + name = link['name'] + endpoint = link['type'] + + if name in self.IGNORED_LINKS: + continue + + setattr(cls, name, RelatedManagerDescriptor(self, name, endpoint)) class ModelField(Field): @@ -481,7 +377,6 @@ 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): @@ -505,17 +400,14 @@ def validate(self, value, model_instance): super(ModelField, self).validate(value, model_instance) if not isinstance(value, (self.rel, dict)): - 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__)) + raise self.ValidationError('Value needs to be a {0} instance.'.format(self.rel.__name__)) - if (self.required and isinstance(value, self.rel)) or \ - (self.is_data_object_mixin and hasattr(value, 'validate')): + if self.required and isinstance(value, self.rel): value.validate() def to_python(self, value): - if value is None: - return + return value if isinstance(value, self.rel): return value @@ -527,7 +419,7 @@ def to_python(self, value): def to_native(self, value): if value is None: - return + return value if isinstance(value, self.rel): if not self.just_pk: @@ -537,22 +429,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 and not self.just_pk and hasattr(value, 'to_native'): - return value.to_native() - return value -class FileField(WritableField): - param_name = 'files' - - def to_native(self, value): - if isinstance(value, six.string_types): - return None - return {self.name: value} - - -class JSONField(JSONToPythonMixin, WritableField): +class JSONField(WritableField): query_allowed = False schema = None @@ -568,64 +448,24 @@ def validate(self, value, model_instance): except ValueError as e: raise self.ValidationError(e) + def to_python(self, value): + if value is None: + return value + + if isinstance(value, six.string_types): + value = json.loads(value) + return value + def to_native(self, value): if value is None: - return + return value if not isinstance(value, six.string_types): value = json.dumps(value) return value -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) - except (ValueError, TypeError): - raise SyncanoValueError('Expected an array') - - 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') - - 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') - - -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) - except (ValueError, TypeError): - raise SyncanoValueError('Expected an object') - - if not isinstance(value, dict): - raise SyncanoValueError('Expected an object') - - class SchemaField(JSONField): - required = False query_allowed = False not_indexable_types = ['text', 'file'] schema = { @@ -648,11 +488,7 @@ class SchemaField(JSONField): 'boolean', 'datetime', 'file', - 'reference', - 'relation', - 'array', - 'object', - 'geopoint', + 'reference' ], }, 'order_index': { @@ -672,9 +508,6 @@ class SchemaField(JSONField): } def validate(self, value, model_instance): - if value is None: - return - if isinstance(value, SchemaManager): value = value.schema @@ -704,209 +537,16 @@ 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): - if 'environment' not in value: - value.update({ - 'environment': PUSH_ENV, - }) - value = json.dumps(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'] - - def validate(self, value, model_instance): - super(GeoPointField, 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, GeoPoint): - raise SyncanoValueError('Expected a GeoPoint') - - def to_native(self, value): - if value is None: - return - - if isinstance(value, bool): - return value # exists lookup - - if isinstance(value, dict): - value = GeoPoint(latitude=value['latitude'], longitude=value['longitude']) - - 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 - - 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, **kwargs) - - if lookup_type not in self.field_lookups: - 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(latitude=value.pop('latitude'), longitude=value.pop('longitude')), - Distance(**value) - ) - - 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))') - - 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: - 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 - - if isinstance(value, dict): - latitude = value.get('latitude') - longitude = value.get('longitude') - elif isinstance(value, (tuple, list)): - try: - latitude = value[0] - longitude = value[1] - except IndexError: - raise SyncanoValueError('Can not parse the geo point.') - - return latitude, longitude - - -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) - - 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'] - - if isinstance(value, dict) and ('_add' in value or '_remove' in value): - return value - - if not isinstance(value, (list, tuple)): - return [value] - - return value - - 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 self.field_lookups: - raise SyncanoValueError('Lookup {} not supported for relation field.'.format(lookup_type)) - - query_dict = {} - - if lookup_type == 'contains': - if self._check_relation_value(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: - 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._check_relation_value(value): - value = [obj.id for obj in value] - return value - - MAPPING = { 'string': StringField, 'text': StringField, - 'file': FileField, + 'file': StringField, 'ref': StringField, - 'reference': ReferenceField, - 'relation': RelationField, + 'reference': IntegerField, 'integer': IntegerField, 'float': FloatField, 'boolean': BooleanField, - 'name': SlugField, + 'slug': SlugField, 'email': EmailField, 'choice': ChoiceField, 'date': DateField, @@ -914,11 +554,8 @@ def to_native(self, value): 'field': Field, 'writable': WritableField, 'endpoint': EndpointField, - 'links': LinksField, + 'links': HyperlinkedField, 'model': ModelField, 'json': JSONField, 'schema': SchemaField, - 'array': ArrayField, - 'object': ObjectField, - 'geopoint': GeoPointField, } diff --git a/syncano/models/geo.py b/build/lib/syncano/models/geo.py similarity index 100% rename from syncano/models/geo.py rename to build/lib/syncano/models/geo.py diff --git a/build/lib/syncano/models/hosting.py b/build/lib/syncano/models/hosting.py new file mode 100644 index 0000000..39c32e7 --- /dev/null +++ b/build/lib/syncano/models/hosting.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +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.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) + + 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 = connection.session.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']] + + def set_default(self): + default_path = self.links.set_default + connection = self._get_connection() + + response = connection.make_request('POST', default_path) + self.to_python(response) + return self diff --git a/build/lib/syncano/models/incentives.py b/build/lib/syncano/models/incentives.py new file mode 100644 index 0000000..6be08e0 --- /dev/null +++ b/build/lib/syncano/models/incentives.py @@ -0,0 +1,294 @@ +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': 'list', '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) + """ + from .traces import CodeBoxTrace + + 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 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': 'detail', 'name': 'codebox'}, + {'type': 'list', 'name': 'traces'}, + ] + + 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 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': 'list', '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 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'}, + {'type': 'list', 'name': 'traces'}, + ) + + 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) + """ + from .traces import WebhookTrace + + 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) diff --git a/build/lib/syncano/models/instances.py b/build/lib/syncano/models/instances.py new file mode 100644 index 0000000..782f7d5 --- /dev/null +++ b/build/lib/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 .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) + 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/build/lib/syncano/models/manager.py b/build/lib/syncano/models/manager.py new file mode 100644 index 0000000..9239794 --- /dev/null +++ b/build/lib/syncano/models/manager.py @@ -0,0 +1,739 @@ +import json +from copy import deepcopy +from functools import wraps + +import six + +from syncano.connection import ConnectionMixin +from syncano.exceptions import SyncanoValueError, SyncanoRequestError +from syncano.utils import get_class_name +from .registry import registry + + +# The maximum number of items to display in a Manager.__repr__ +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): + self.manager = manager + + def __get__(self, instance, owner=None): + if instance is not None: + raise AttributeError("Manager isn't accessible via {0} instances.".format(owner.__name__)) + return self.manager.all() + + +class RelatedManagerDescriptor(object): + + def __init__(self, field, name, endpoint): + self.field = field + self.name = name + self.endpoint = endpoint + + 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) + path = links[self.name] + Model = registry.get_model_by_path(path) + 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) + + +class Manager(ConnectionMixin): + """Base class responsible for all ORM (``please``) actions.""" + + def __init__(self): + self.name = None + self.model = None + + self.endpoint = None + self.properties = {} + + self.method = None + self.query = {} + self.data = {} + + self._limit = None + self._serialize = True + self._connection = None + + def __repr__(self): # pragma: no cover + data = list(self[:REPR_OUTPUT_SIZE + 1]) + if len(data) > REPR_OUTPUT_SIZE: + data[-1] = '...(remaining elements truncated)...' + return repr(data) + + def __str__(self): # pragma: no cover + return ''.format(self.model.__name__) + + def __unicode__(self): # pragma: no cover + return six.u(str(self)) + + def __len__(self): # pragma: no cover + return self.iterator() + + def __iter__(self): # pragma: no cover + return iter(self.iterator()) + + def __bool__(self): # pragma: no cover + return bool(self.iterator()) + + 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. + """ + if not isinstance(k, (slice,) + six.integer_types): + raise TypeError + assert ((not isinstance(k, slice) and (k >= 0)) or + (isinstance(k, slice) and (k.start is None or k.start >= 0) and + (k.stop is None or k.stop >= 0))), \ + "Negative indexing is not supported." + + manager = self._clone() + + if isinstance(k, slice): + if k.stop is not None: + manager.limit(int(k.stop) + 1) + return list(manager)[k.start:k.stop:k.step] + + manager.limit(k + 1) + return list(manager)[k] + + # Object actions + + def create(self, **kwargs): + """ + A convenience method for creating an object and saving it all in one step. Thus:: + + instance = Instance.please.create(name='test-one', description='description') + + and:: + + instance = Instance(name='test-one', description='description') + instance.save() + + are equivalent. + """ + attrs = kwargs.copy() + attrs.update(self.properties) + + instance = self.model(**attrs) + instance.save() + + return instance + + def bulk_create(self, *objects): + """ + Creates many new instances based on provided list of objects. + + Usage:: + + objects = [{'name': 'test-one'}, {'name': 'test-two'}] + instances = Instance.please.bulk_create(objects) + + .. warning:: + This method is not meant to be used with large data sets. + """ + return [self.create(**o) for o in objects] + + @clone + def get(self, *args, **kwargs): + """ + Returns the object matching the given lookup parameters. + + Usage:: + + instance = Instance.please.get('test-one') + instance = Instance.please.get(name='test-one') + """ + self.method = 'GET' + self.endpoint = 'detail' + self._filter(*args, **kwargs) + return self.request() + + def detail(self, *args, **kwargs): + """ + Wrapper around ``get`` method. + + Usage:: + + instance = Instance.please.detail('test-one') + instance = Instance.please.detail(name='test-one') + """ + return self.get(*args, **kwargs) + + def get_or_create(self, **kwargs): + """ + A convenience method for looking up an object with the given + lookup parameters, creating one if necessary. + + Returns a tuple of **(object, created)**, where **object** is the retrieved or + **created** object and created is a boolean specifying whether a new object was created. + + This is meant as a shortcut to boilerplatish code. For example:: + + try: + instance = Instance.please.get(name='test-one') + except Instance.DoesNotExist: + instance = Instance(name='test-one', description='test') + instance.save() + + The above example can be rewritten using **get_or_create()** like so:: + + instance, created = Instance.please.get_or_create(name='test-one', defaults={'description': 'test'}) + """ + defaults = deepcopy(kwargs.pop('defaults', {})) + try: + instance = self.get(**kwargs) + except self.model.DoesNotExist: + defaults.update(kwargs) + instance = self.create(**defaults) + created = True + else: + created = False + return instance, created + + @clone + def delete(self, *args, **kwargs): + """ + Removes single instance based on provided arguments. + + Usage:: + + instance = Instance.please.delete('test-one') + instance = Instance.please.delete(name='test-one') + """ + self.method = 'DELETE' + self.endpoint = 'detail' + self._filter(*args, **kwargs) + return self.request() + + @clone + def update(self, *args, **kwargs): + """ + Updates single instance based on provided arguments. + The **data** is a dictionary of (field, value) pairs used to update the object. + + Usage:: + + instance = Instance.please.update('test-one', data={'description': 'new one'}) + instance = Instance.please.update(name='test-one', data={'description': 'new one'}) + """ + self.method = 'PUT' + self.endpoint = 'detail' + self.data = kwargs.pop('data') + self._filter(*args, **kwargs) + return self.request() + + 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. + The ``defaults`` is a dictionary of (field, value) pairs used to update the object. + + Returns a tuple of **(object, created)**, where object is the created or updated object and created + is a boolean specifying whether a new object was created. + + The **update_or_create** method tries to fetch an object from Syncano API based on the given kwargs. + If a match is found, it updates the fields passed in the defaults dictionary. + + This is meant as a shortcut to boilerplatish code. For example:: + + try: + instance = Instance.please.update(name='test-one', data=updated_values) + except Instance.DoesNotExist: + updated_values.update({'name': 'test-one'}) + instance = Instance(**updated_values) + instance.save() + + This pattern gets quite unwieldy as the number of fields in a model goes up. + The above example can be rewritten using **update_or_create()** like so:: + + instance, created = Instance.please.update_or_create(name='test-one', + defaults=updated_values) + """ + defaults = deepcopy(defaults or {}) + try: + instance = self.update(**kwargs) + except self.model.DoesNotExist: + defaults.update(kwargs) + instance = self.create(**defaults) + created = True + else: + created = False + return instance, created + + # List actions + + @clone + def all(self, *args, **kwargs): + """ + Returns a copy of the current ``Manager`` with limit removed. + + Usage:: + + instances = Instance.please.all() + """ + self._limit = None + return self.list(*args, **kwargs) + + @clone + def list(self, *args, **kwargs): + """ + Returns a copy of the current ``Manager`` containing objects that match the given lookup parameters. + + Usage:: + instance = Instance.please.list() + classes = Class.please.list(instance_name='test-one') + """ + self.method = 'GET' + self.endpoint = 'list' + self._filter(*args, **kwargs) + return self + + @clone + def first(self, *args, **kwargs): + """ + Returns the first object matched by the lookup parameters or None, if there is no matching object. + + Usage:: + + instance = Instance.please.first() + classes = Class.please.first(instance_name='test-one') + """ + try: + self._limit = 1 + return self.list(*args, **kwargs)[0] + except KeyError: + return None + + @clone + def page_size(self, value): + """ + Sets page size. + + Usage:: + + instances = Instance.please.page_size(20).all() + """ + if not value or not isinstance(value, six.integer_types): + raise SyncanoValueError('page_size value needs to be an int.') + + self.query['page_size'] = value + return self + + @clone + def limit(self, value): + """ + Sets limit of returned objects. + + Usage:: + + instances = Instance.please.list().limit(10) + classes = Class.please.list(instance_name='test-one').limit(10) + """ + if not value or not isinstance(value, six.integer_types): + raise SyncanoValueError('Limit value needs to be an int.') + + self._limit = value + return self + + @clone + def order_by(self, field): + """ + Sets order 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 + + @clone + def raw(self): + """ + Disables serialization. ``request`` method will return raw Python types. + + Usage:: + + >>> instances = Instance.please.list().raw() + >>> instances + [{'description': 'new one', 'name': 'test-one'...}...] + """ + self._serialize = False + return self + + @clone + def using(self, connection): + """ + Connection juggling. + """ + # ConnectionMixin will validate this + self.connection = connection + return self + + # Other stuff + + def contribute_to_class(self, model, name): # pragma: no cover + setattr(model, name, ManagerDescriptor(self)) + + self.model = model + + if not self.name: + self.name = name + + def _filter(self, *args, **kwargs): + if args and self.endpoint: + properties = self.model._meta.get_endpoint_properties(self.endpoint) + mapped_args = {k: v for k, v in zip(properties, args)} + self.properties.update(mapped_args) + self.properties.update(kwargs) + + def _clone(self): + # Maybe deepcopy ? + manager = self.__class__() + manager.name = self.name + manager.model = self.model + manager._connection = self._connection + manager.endpoint = self.endpoint + manager.properties = deepcopy(self.properties) + manager._limit = self._limit + manager.method = self.method + manager.query = deepcopy(self.query) + manager.data = deepcopy(self.data) + manager._serialize = self._serialize + + return manager + + def serialize(self, data, model=None): + """Serializes passed data to related :class:`~syncano.models.base.Model` class.""" + model = model or self.model + + if isinstance(data, model): + return data + + if not isinstance(data, dict): + raise SyncanoValueError('Unsupported data type.') + + properties = deepcopy(self.properties) + properties.update(data) + + return model(**properties) if self._serialize else data + + def request(self, method=None, path=None, **request): + """Internal method, which calls Syncano API and returns serialized data.""" + meta = self.model._meta + method = method or self.method + path = path or meta.resolve_endpoint(self.endpoint, self.properties) + + if 'params' not in request and self.query: + request['params'] = self.query + + if 'data' not in request and self.data: + request['data'] = self.data + + try: + response = self.connection.request(method, path, **request) + except SyncanoRequestError as e: + if e.status_code == 404: + raise self.model.DoesNotExist + raise + + if 'next' not in response: + return self.serialize(response) + + return response + + def iterator(self): + """Pagination handler""" + + response = self.request() + results = 0 + while True: + objects = response.get('objects') + next_url = response.get('next') + + for o in objects: + if self._limit and results >= self._limit: + break + + results += 1 + yield self.serialize(o) + + if not objects or not next_url or (self._limit and results >= self._limit): + break + + response = self.request(path=next_url) + + +class CodeBoxManager(Manager): + """ + Custom :class:`~syncano.models.manager.Manager` + class for :class:`~syncano.models.base.CodeBox` model. + """ + + @clone + def run(self, *args, **kwargs): + payload = kwargs.pop('payload', {}) + + if not isinstance(payload, six.string_types): + payload = json.dumps(payload) + + self.method = 'POST' + self.endpoint = 'run' + self.data['payload'] = payload + self._filter(*args, **kwargs) + self._serialize = False + return self.request() + + +class WebhookManager(Manager): + """ + Custom :class:`~syncano.models.manager.Manager` + class for :class:`~syncano.models.base.Webhook` model. + """ + + @clone + def run(self, *args, **kwargs): + self.method = 'GET' + self.endpoint = 'run' + self._filter(*args, **kwargs) + self._serialize = False + return self.request() + + +class ObjectManager(Manager): + """ + Custom :class:`~syncano.models.manager.Manager` + class for :class:`~syncano.models.base.Object` model. + """ + LOOKUP_SEPARATOR = '__' + ALLOWED_LOOKUPS = [ + 'gt', 'gte', 'lt', 'lte', + 'eq', 'neq', 'exists', 'in', + ] + + def create(self, **kwargs): + attrs = kwargs.copy() + attrs.update(self.properties) + + model = self.get_class_model(kwargs) + instance = model(**attrs) + instance.save() + + return instance + + def serialize(self, data, model=None): + model = self.get_class_model(self.properties) + return super(ObjectManager, self).serialize(data, model) + + def get_class_model(self, properties): + """Creates custom :class:`~syncano.models.base.Object` sub-class definition based on passed ``properties``.""" + instance_name = properties.get('instance_name', '') + class_name = properties.get('class_name', '') + model_name = get_class_name(instance_name, class_name, 'object') + + if self.model.__name__ == model_name: + return self.model + + try: + model = registry.get_model_by_name(model_name) + except LookupError: + schema = self.get_class_schema(properties) + model = self.model.create_subclass(model_name, schema) + registry.add(model_name, model) + + return model + + def get_class_schema(self, properties): + instance_name = properties.get('instance_name', '') + class_name = properties.get('class_name', '') + parent = self.model._meta.parent + class_ = parent.please.get(instance_name, class_name) + return class_.schema + + @clone + def filter(self, **kwargs): + """ + Special method just for data object :class:`~syncano.models.base.Object` model. + + Usage:: + + objects = Object.please.list('instance-name', 'class-name').filter(henryk__gte='hello') + """ + query = {} + model = self.get_class_model(self.properties) + + for field_name, value in six.iteritems(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)) + + 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.query['query'] = json.dumps(query) + self.method = 'GET' + self.endpoint = 'list' + return self + + +class SchemaManager(object): + """ + Custom :class:`~syncano.models.manager.Manager` + class for :class:`~syncano.models.fields.SchemaFiled`. + """ + + def __init__(self, schema=None): + self.schema = schema or [] + + def __eq__(self, other): + if isinstance(other, SchemaManager): + return self.schema == other.schema + return NotImplemented + + def __str__(self): # pragma: no cover + return str(self.schema) + + def __repr__(self): # pragma: no cover + return '' + + def __getitem__(self, key): + if isinstance(key, int): + return self.schema[key] + + if isinstance(key, six.string_types): + for v in self.schema: + if v['name'] == key: + return v + + raise KeyError + + def __setitem__(self, key, value): + value = deepcopy(value) + value['name'] = key + self.remove(key) + self.add(value) + + def __delitem__(self, key): + self.remove(key) + + def __iter__(self): + return iter(self.schema) + + def __contains__(self, item): + if not self.schema: + return False + return item in self.schema + + def set(self, value): + """Sets schema value.""" + self.schema = value + + def add(self, *objects): + """Adds multiple objects to schema.""" + self.schema.extend(objects) + + def remove(self, *names): + """Removes selected objects based on their names.""" + values = [v for v in self.schema if v['name'] not in names] + self.set(values) + + def clear(self): + """Sets empty schema.""" + self.set([]) + + def set_index(self, field, order=False, filter=False): + """Sets index on selected field. + + :type field: string + :param field: Name of schema field + + :type filter: bool + :param filter: Sets filter index on selected field + + :type order: bool + :param order: Sets order index on selected field + """ + if not order and not filter: + raise ValueError('Choose at least one index.') + + if order: + self[field]['order_index'] = True + + if filter: + self[field]['filter_index'] = True + + def set_order_index(self, field): + """Shortcut for ``set_index(field, order=True)``.""" + self.set_index(field, order=True) + + def set_filter_index(self, field): + """Shortcut for ``set_index(field, filter=True)``.""" + self.set_index(field, filter=True) + + def remove_index(self, field, order=False, filter=False): + """Removes index from selected field. + + :type field: string + :param field: Name of schema field + + :type filter: bool + :param filter: Removes filter index from selected field + + :type order: bool + :param order: Removes order index from selected field + """ + if not order and not filter: + raise ValueError('Choose at least one index.') + + if order and 'order_index' in self[field]: + del self[field]['order_index'] + + if filter and 'filter_index' in self[field]: + del self[field]['filter_index'] + + def remove_order_index(self, field): + """Shortcut for ``remove_index(field, order=True)``.""" + self.remove_index(field, order=True) + + def remove_filter_index(self, field): + """Shortcut for ``remove_index(field, filter=True)``.""" + self.remove_index(field, filter=True) diff --git a/build/lib/syncano/models/manager_mixins.py b/build/lib/syncano/models/manager_mixins.py new file mode 100644 index 0000000..7f0e0e7 --- /dev/null +++ b/build/lib/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/syncano/models/mixins.py b/build/lib/syncano/models/mixins.py similarity index 100% rename from syncano/models/mixins.py rename to build/lib/syncano/models/mixins.py diff --git a/syncano/models/options.py b/build/lib/syncano/models/options.py similarity index 84% rename from syncano/models/options.py rename to build/lib/syncano/models/options.py index 605194e..1a4e9fa 100644 --- a/syncano/models/options.py +++ b/build/lib/syncano/models/options.py @@ -1,17 +1,14 @@ import re from bisect import bisect +from urlparse import urljoin import six + from syncano.connection import ConnectionMixin -from syncano.exceptions import SyncanoValidationError, SyncanoValueError +from syncano.exceptions import SyncanoValueError from syncano.models.registry import registry from syncano.utils import camelcase_to_underscore -if six.PY3: - from urllib.parse import urljoin -else: - from urlparse import urljoin - class Options(ConnectionMixin): """Holds metadata related to model definition.""" @@ -133,23 +130,15 @@ def get_endpoint_methods(self, name): endpoint = self.get_endpoint(name) return endpoint['methods'] - def resolve_endpoint(self, endpoint_name, properties, http_method=None): - if http_method and not self.is_http_method_available(http_method, endpoint_name): - raise SyncanoValidationError( - 'HTTP method {0} not allowed for endpoint "{1}".'.format(http_method, endpoint_name) - ) - endpoint = self.get_endpoint(endpoint_name) + def resolve_endpoint(self, name, properties): + endpoint = self.get_endpoint(name) - for endpoint_name in endpoint['properties']: - if endpoint_name not in properties: - raise SyncanoValueError('Request property "{0}" is required.'.format(endpoint_name)) + for name in endpoint['properties']: + if name not in properties: + raise SyncanoValueError('Request property "{0}" is required.'.format(name)) return endpoint['path'].format(**properties) - 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 - 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} diff --git a/syncano/models/push_notification.py b/build/lib/syncano/models/push_notification.py similarity index 56% rename from syncano/models/push_notification.py rename to build/lib/syncano/models/push_notification.py index b8a7c86..0693f9d 100644 --- a/syncano/models/push_notification.py +++ b/build/lib/syncano/models/push_notification.py @@ -16,29 +16,16 @@ class DeviceBase(object): device_id = fields.StringField(required=False) is_active = fields.BooleanField(default=True) label = fields.StringField(max_length=80) - user = fields.IntegerField(required=False) + user_id = 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 send_message(self, content): - """ - A method which allows to send message directly to the device; - :param contet: Message content structure - object like; - :return: - """ - 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 + def is_new(self): + return self.created_at is None class GCMDevice(DeviceBase, Model): @@ -48,7 +35,8 @@ class GCMDevice(DeviceBase, Model): Usage:: - Create a new Device: + Create a new Device: + gcm_device = GCMDevice( label='example label', registration_id=86152312314401555, @@ -58,15 +46,17 @@ class GCMDevice(DeviceBase, Model): gcm_device.save() - Read: - gcm_device = GCMDevice.please.get(registration_id=86152312314401554) + Note:: + + another save on the same object will always fail (altering the Device data is currently not possible); + + Delete a Device: - Delete: gcm_device.delete() - Update: - gcm_device.label = 'some new label' - gcm_device.save() + Read a Device data: + + gcm_device = GCMDevice.please.get(registration_id=86152312314401554) """ @@ -74,11 +64,11 @@ class Meta: parent = Instance endpoints = { 'detail': { - 'methods': ['delete', 'get', 'put', 'patch'], + 'methods': ['delete', 'get'], 'path': '/push_notifications/gcm/devices/{registration_id}/', }, 'list': { - 'methods': ['post', 'get'], + 'methods': ['get'], 'path': '/push_notifications/gcm/devices/', } } @@ -91,7 +81,7 @@ class APNSDevice(DeviceBase, Model): Usage:: - Create a new Device: + Create apns_device = APNSDevice( label='example label', registration_id='4719084371920471208947120984731208947910827409128470912847120894', @@ -100,32 +90,30 @@ class APNSDevice(DeviceBase, Model): ) apns_device.save() - Read: - apns_device = - APNSDevice.please.get(registration_id='4719084371920471208947120984731208947910827409128470912847120894') + Note:: - Delete: - apns_device.delete() + 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() + Also note the different format (from GCM) of registration_id required by APNS; the device_id have different + format too. - .. note:: + Read + apns_device = + APNSDevice.please.get(registration_id='4719084371920471208947120984731208947910827409128470912847120894') - Also note the different format (from GCM) of registration_id required by APNS; the device_id have different - format too. + Delete + apns_device.delete() """ class Meta: parent = Instance endpoints = { 'detail': { - 'methods': ['delete', 'get', 'put', 'patch'], + 'methods': ['delete', 'get'], 'path': '/push_notifications/apns/devices/{registration_id}/', }, 'list': { - 'methods': ['post', 'get'], + 'methods': ['get'], 'path': '/push_notifications/apns/devices/', } } @@ -154,7 +142,8 @@ class GCMMessage(MessageBase, Model): Usage:: - Create a new Message: + Create + message = GCMMessage( content={ @@ -167,24 +156,20 @@ 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; - Read: + Note:: + Every save after initial one will raise an error; + 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; - - 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:: + Note:: The altering of existing Message is not possible. It also not possible to delete message. """ @@ -197,7 +182,7 @@ class Meta: 'path': '/push_notifications/gcm/messages/{id}/', }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], 'path': '/push_notifications/gcm/messages/', } } @@ -210,7 +195,7 @@ class APNSMessage(MessageBase, Model): Usage:: - Create new Message: + Create apns_message = APNSMessage( content={ 'registration_ids': [gcm_device.registration_id], @@ -220,21 +205,19 @@ class APNSMessage(MessageBase, Model): apns_message.save() - Read: + The 'aps' data is send 'as-it-is' to APNS, some validation is made on syncano CORE; + For more details read the APNS documentation; - apns_message = APNSMessage.please.get(id=1) + Note:: + Every save after initial one will raise an error; - Debugging: + 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; - 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 @@ -244,83 +227,7 @@ class Meta: 'path': '/push_notifications/apns/messages/{id}/', }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], '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', 'rb') - 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/build/lib/syncano/models/registry.py b/build/lib/syncano/models/registry.py new file mode 100644 index 0000000..93f1a90 --- /dev/null +++ b/build/lib/syncano/models/registry.py @@ -0,0 +1,81 @@ +from __future__ import unicode_literals + +import re + +import six + +from syncano import logger + + +class Registry(object): + """Models registry.""" + + def __init__(self, models=None): + self.models = models or {} + self.patterns = [] + self._pending_lookups = {} + + def __str__(self): + return 'Registry: {0}'.format(', '.join(self.models)) + + def __unicode__(self): + return unicode(str(self)) + + def __iter__(self): + for name, model in six.iteritems(self.models): + yield model + + def get_model_patterns(self, cls): + patterns = [] + for k, v in six.iteritems(cls._meta.endpoints): + pattern = '^{0}$'.format(v['path']) + for name in v.get('properties', []): + pattern = pattern.replace('{{{0}}}'.format(name), '([^/.]+)') + patterns.append((re.compile(pattern), cls)) + return patterns + + def get_model_by_path(self, path): + for pattern, cls in self.patterns: + if pattern.match(path): + return cls + raise LookupError('Invalid path: {0}'.format(path)) + + def get_model_by_name(self, name): + return self.models[name] + + def add(self, name, cls): + + 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()) + + logger.debug('New model: %s, %s', name, related_name) + + if name in self._pending_lookups: + lookups = self._pending_lookups.pop(name) + for callback, args, kwargs in lookups: + callback(*args, **kwargs) + + return self + + def set_default_property(self, name, value): + for model in self: + if name in model.__dict__: + + 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 + + def set_default_instance(self, value): + self.set_default_property('instance_name', value) + + +registry = Registry() diff --git a/syncano/models/relations.py b/build/lib/syncano/models/relations.py similarity index 100% rename from syncano/models/relations.py rename to build/lib/syncano/models/relations.py diff --git a/syncano/models/traces.py b/build/lib/syncano/models/traces.py similarity index 84% rename from syncano/models/traces.py rename to build/lib/syncano/models/traces.py index e99003a..b794796 100644 --- a/syncano/models/traces.py +++ b/build/lib/syncano/models/traces.py @@ -1,12 +1,12 @@ +from __future__ import unicode_literals from . import fields from .base import Model -from .custom_response import CustomResponseMixin -from .incentives import Schedule, Script, ScriptEndpoint, Trigger +from .incentives import CodeBox, Schedule, Trigger, Webhook -class ScriptTrace(CustomResponseMixin, Model): +class CodeBoxTrace(Model): """ :ivar status: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` @@ -18,25 +18,27 @@ class ScriptTrace(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 = ( + {'type': 'detail', 'name': 'self'}, + ) status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.LinksField() + links = fields.HyperlinkedField(links=LINKS) executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.JSONField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) class Meta: - parent = Script + parent = CodeBox endpoints = { 'detail': { 'methods': ['get'], 'path': '/traces/{id}/', }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], 'path': '/traces/', } } @@ -56,9 +58,12 @@ 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.LinksField() + 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) @@ -71,7 +76,7 @@ class Meta: 'path': '/traces/{id}/', }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], 'path': '/traces/', } } @@ -96,7 +101,7 @@ class TriggerTrace(Model): ) status = fields.ChoiceField(choices=STATUS_CHOICES, read_only=True, required=False) - links = fields.LinksField() + 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) @@ -109,13 +114,13 @@ class Meta: 'path': '/traces/{id}/', }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], 'path': '/traces/', } } -class ScriptEndpointTrace(CustomResponseMixin, Model): +class WebhookTrace(Model): """ :ivar status: :class:`~syncano.models.fields.ChoiceField` :ivar links: :class:`~syncano.models.fields.HyperlinkedField` @@ -129,22 +134,25 @@ 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.LinksField() + links = fields.HyperlinkedField(links=LINKS) executed_at = fields.DateTimeField(read_only=True, required=False) - result = fields.JSONField(read_only=True, required=False) + result = fields.StringField(read_only=True, required=False) duration = fields.IntegerField(read_only=True, required=False) class Meta: - parent = ScriptEndpoint + parent = Webhook endpoints = { 'detail': { 'methods': ['get'], 'path': '/traces/{id}/', }, 'list': { - 'methods': ['get', 'post'], + 'methods': ['get'], 'path': '/traces/', } } diff --git a/syncano/release_utils.py b/build/lib/syncano/release_utils.py similarity index 85% rename from syncano/release_utils.py rename to build/lib/syncano/release_utils.py index e75c17c..2358091 100644 --- a/syncano/release_utils.py +++ b/build/lib/syncano/release_utils.py @@ -21,7 +21,7 @@ def new_func(*args, **kwargs): self.removed_in_version ), category=DeprecationWarning, - filename=original_func.__code__.co_filename, - lineno=original_func.__code__.co_firstlineno + self.lineno) + filename=original_func.func_code.co_filename, + lineno=original_func.func_code.co_firstlineno + self.lineno) return original_func(*args, **kwargs) return new_func diff --git a/syncano/utils.py b/build/lib/syncano/utils.py similarity index 100% rename from syncano/utils.py rename to build/lib/syncano/utils.py index fd4d273..00c91c8 100644 --- a/syncano/utils.py +++ b/build/lib/syncano/utils.py @@ -1,5 +1,5 @@ -import datetime import re +import datetime from decimal import Decimal import six diff --git a/build/lib/tests/__init__.py b/build/lib/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build/lib/tests/__main__.py b/build/lib/tests/__main__.py new file mode 100644 index 0000000..7735842 --- /dev/null +++ b/build/lib/tests/__main__.py @@ -0,0 +1,11 @@ +import os +import unittest +import xmlrunner + + +if __name__ == '__main__': + output = os.path.join(os.getenv('CIRCLE_TEST_REPORTS', ''), 'junit') + suite = unittest.TestLoader().discover('.') + runner = xmlrunner.XMLTestRunner(output=output, verbosity=2, + failfast=False, buffer=False) + runner.run(suite) diff --git a/tests/test_connection.py b/build/lib/tests/test_connection.py similarity index 62% rename from tests/test_connection.py rename to build/lib/tests/test_connection.py index ded59b5..4351cf3 100644 --- a/tests/test_connection.py +++ b/build/lib/tests/test_connection.py @@ -1,47 +1,82 @@ -import json -import tempfile import unittest - -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 +from urlparse import urljoin try: from unittest import mock except ImportError: import mock +from syncano import connect, connect_instance +from syncano.connection import Connection, ConnectionMixin, default_connection +from syncano.exceptions import SyncanoValueError, SyncanoRequestError + class ConnectTestCase(unittest.TestCase): - @mock.patch('syncano.connection.DefaultConnection.open') - def test_connect(self, open_mock): + @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) 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) + self.assertEqual(connection, registry_mock) - @mock.patch('syncano.models.registry.connection.open') + @mock.patch('syncano.connection.default_connection.open') @mock.patch('syncano.models.registry') @mock.patch('syncano.INSTANCE') def test_env_instance(self, instance_mock, registry_mock, *args): - instance_mock.return_value = 'test_instance' - self.assertFalse(registry_mock.set_used_instance.called) + self.assertFalse(registry_mock.set_default_instance.called) connect(1, 2, 3, a=1, b=2, c=3) - self.assertTrue(registry_mock.set_used_instance.called) - registry_mock.set_used_instance.assert_called_once_with(instance_mock) + self.assertTrue(registry_mock.set_default_instance.called) + 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) + 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) + get_mock.assert_called_once_with(instance_mock) + self.assertEqual(instance, get_mock) class ConnectionTestCase(unittest.TestCase): @@ -73,7 +108,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( - {'files': [], 'headers': {'content-type': 'application/json'}, 'timeout': 30, 'verify': False}, + {'headers': {'content-type': 'application/json'}, 'timeout': 30, 'verify': False}, sort_keys=True, indent=2, separators=(',', ': ')) @mock.patch('requests.Session.post') @@ -125,7 +160,7 @@ def test_build_params(self): self.assertTrue('headers' in params) self.assertTrue('Authorization' in params['headers']) - self.assertEqual(params['headers']['Authorization'], 'token {0}'.format(self.connection.api_key)) + self.assertEqual(params['headers']['Authorization'], 'ApiKey {0}'.format(self.connection.api_key)) self.assertTrue('content-type' in params['headers']) self.assertEqual(params['headers']['content-type'], self.connection.CONTENT_TYPE) @@ -181,26 +216,6 @@ def test_invalid_method_name(self): with self.assertRaises(SyncanoValueError): self.connection.make_request('INVALID', 'test') - @mock.patch('syncano.connection.Connection.get_response_content') - @mock.patch('requests.Session.patch') - def test_make_request_for_creating_object_with_file(self, patch_mock, get_response_mock): - kwargs = { - 'data': { - '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') - @mock.patch('requests.Session.patch') - def test_make_request_for_updating_object_with_file(self, patch_mock, get_reponse_mock): - kwargs = { - 'data': {'filename': tempfile.TemporaryFile(mode='w')} - } - # 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') @@ -258,15 +273,13 @@ 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']) - 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"}) + 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 + ) @mock.patch('syncano.connection.Connection.make_request') def test_successful_authentication(self, make_request): @@ -280,98 +293,11 @@ 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 = {'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) - 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 = {'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) - 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 as 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: - self.assertIsNone(self.connection.api_key) - self.assertFalse(make_request.called) - - @mock.patch('syncano.connection.Connection.make_request') - def test_get_user_info(self, make_request_mock): - info = {'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 = {'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 = {'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): def setUp(self): - self.connection = registry.connection + self.connection = default_connection self.connection._connection = None def test_call(self): @@ -397,7 +323,7 @@ class ConnectionMixinTestCase(unittest.TestCase): def setUp(self): self.mixin = ConnectionMixin() - @mock.patch('syncano.models.registry._default_connection') + @mock.patch('syncano.connection.default_connection') def test_getter(self, default_connection_mock): default_connection_mock.return_value = default_connection_mock diff --git a/tests/test_fields.py b/build/lib/tests/test_fields.py similarity index 86% rename from tests/test_fields.py rename to build/lib/tests/test_fields.py index 694a9bd..58ef1f6 100644 --- a/tests/test_fields.py +++ b/build/lib/tests/test_fields.py @@ -1,10 +1,8 @@ -import json import unittest from datetime import datetime 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 @@ -57,23 +55,20 @@ class AllFieldsModel(models.Model): choice_field = models.ChoiceField(choices=CHOICES) date_field = models.DateField() datetime_field = models.DateTimeField() - hyperlinked_field = models.LinksField() + hyperlinked_field = models.HyperlinkedField() model_field = models.ModelField('Instance') json_field = models.JSONField(schema=SCHEMA) schema_field = models.SchemaField() - array_field = models.ArrayField() - object_field = models.ObjectField() - geo_field = models.GeoPointField() class Meta: endpoints = { 'detail': { 'methods': ['delete', 'post', 'patch', 'get'], - 'path': '/v1.1/dummy/{dynamic_field}/', + 'path': '/v1/dummy/{dynamic_field}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/v1.1/dummy/', + 'path': '/v1/dummy/', } } @@ -110,11 +105,11 @@ def test_field_str(self): @skip_base_class def test_field_unicode(self): - expected = six.u('<{0}: {1}>').format( + expected = u'<{0}: {1}>'.format( self.field.__class__.__name__, self.field_name ) - out = str(self.field) + out = unicode(self.field) self.assertEqual(out, expected) @skip_base_class @@ -253,11 +248,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), '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]") + 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]") class IntegerFieldTestCase(BaseTestCase): @@ -416,7 +411,7 @@ def test_to_python(self): def test_to_native(self): now = datetime.now() self.assertEqual(self.field.to_native(None), None) - self.assertEqual(self.field.to_native(now), '%sZ' % now.isoformat()) + self.assertEqual(self.field.to_native(now), now.isoformat()) class HyperlinkedFieldTestCase(BaseTestCase): @@ -529,65 +524,5 @@ def test_to_native(self): schema = SchemaManager(value) 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}') - - -class GeoPointTestCase(BaseTestCase): - field_name = 'geo_field' - - def test_validate(self): - - with self.assertRaises(SyncanoValueError): - self.field.validate(12, self.instance) - - self.field.validate(models.GeoPoint(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}) - self.field.to_python(models.GeoPoint(52.12, 12.02)) + self.assertEqual(self.field.to_native(schema), '[{"type": "string", "name": "username"}]') + self.assertEqual(self.field.to_native(value), '[{"type": "string", "name": "username"}]') diff --git a/tests/test_manager.py b/build/lib/tests/test_manager.py similarity index 58% rename from tests/test_manager.py rename to build/lib/tests/test_manager.py index 4466c72..4fd9e15 100644 --- a/tests/test_manager.py +++ b/build/lib/tests/test_manager.py @@ -1,15 +1,16 @@ -import json import unittest -from datetime import datetime - -from syncano.exceptions import SyncanoDoesNotExist, SyncanoRequestError, SyncanoValueError -from syncano.models import Instance, Object, Script, ScriptEndpoint, ScriptEndpointTrace, ScriptTrace, User, registry try: from unittest import mock except ImportError: import mock +from syncano.exceptions import ( + SyncanoValueError, SyncanoRequestError, + SyncanoDoesNotExist +) +from syncano.models.base import Instance, CodeBox, Webhook, Object + class CloneTestCase(unittest.TestCase): pass @@ -29,22 +30,6 @@ def setUp(self): self.model = Instance 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 - registry.clear_used_instance() - - 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 @@ -58,56 +43,15 @@ 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, is_lazy=False) + model_mock.assert_called_once_with(a=1, b=2) model_mock.save.assert_called_once_with() - @mock.patch('syncano.models.bulk.ModelBulkCreate.make_batch_request') + @mock.patch('syncano.models.manager.Manager.create') def test_bulk_create(self, create_mock): self.assertFalse(create_mock.called) - self.manager.bulk_create( - User(instance_name='A', username='a', password='a'), - User(instance_name='A', username='b', password='b') - ) + self.manager.bulk_create({'a': 1}, {'a': 2}) 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) + self.assertEqual(create_mock.call_count, 2) @mock.patch('syncano.models.manager.Manager.request') @mock.patch('syncano.models.manager.Manager._filter') @@ -216,51 +160,9 @@ 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, 'PATCH') + self.assertEqual(self.manager.method, 'PUT') self.assertEqual(self.manager.endpoint, 'detail') - 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) - - 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}) - - @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) - - self.manager.filter(name=2).update(created_at=1, updated_at=2, links=1) - - 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}) + self.assertEqual(self.manager.data, {'x': 1, 'y': 2}) @mock.patch('syncano.models.manager.Manager.update') @mock.patch('syncano.models.manager.Manager.create') @@ -329,19 +231,6 @@ 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 = 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_original'}) - @mock.patch('syncano.models.manager.Manager.list') def test_first(self, list_mock): list_mock.__getitem__.return_value = 1 @@ -383,22 +272,22 @@ def test_limit(self, clone_mock): self.manager.limit('invalid value') @mock.patch('syncano.models.manager.Manager._clone') - def test_raw(self, clone_mock): + def test_order_by(self, clone_mock): clone_mock.return_value = self.manager - self.assertTrue(self.manager._serialize) - self.manager.raw() - self.assertFalse(self.manager._serialize) + 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_template(self, clone_mock): + def test_raw(self, clone_mock): clone_mock.return_value = self.manager self.assertTrue(self.manager._serialize) - self.assertIsNone(self.manager._template) - self.manager.template('test') + self.manager.raw() self.assertFalse(self.manager._serialize) - self.assertEqual(self.manager._template, 'test') def test_serialize(self): model = mock.Mock() @@ -431,9 +320,8 @@ def test_request(self, connection_mock): request_mock.assert_called_once_with( 'GET', - '/v1.1/instances/', + u'/v1/instances/', data={'b': 2}, - headers={}, params={'a': 1} ) @@ -453,10 +341,6 @@ def test_request(self, connection_mock): with self.assertRaises(SyncanoValueError): self.manager.request() - self.manager.method = 'dummy' - with self.assertRaises(SyncanoValueError): - self.manager.request() - @mock.patch('syncano.models.manager.Manager.request') def test_iterator(self, request_mock): request_mock.side_effect = [ @@ -480,38 +364,25 @@ def test_iterator(self, request_mock): self.assertEqual(request_mock.call_count, 2) request_mock.assert_called_with(path='next_url') - 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') - self.assertEqual(result, 'DELETE') - - with self.assertRaises(SyncanoValueError): - self.manager.get_allowed_method('dummy') - -class ScriptManagerTestCase(unittest.TestCase): +class CodeBoxManagerTestCase(unittest.TestCase): def setUp(self): - self.model = Script - self.manager = Script.please + self.model = CodeBox + self.manager = CodeBox.please - @mock.patch('syncano.models.manager.ScriptManager.request') - @mock.patch('syncano.models.manager.ScriptManager._filter') - @mock.patch('syncano.models.manager.ScriptManager._clone') + @mock.patch('syncano.models.manager.CodeBoxManager.request') + @mock.patch('syncano.models.manager.CodeBoxManager._filter') + @mock.patch('syncano.models.manager.CodeBoxManager._clone') def test_run(self, clone_mock, filter_mock, request_mock): clone_mock.return_value = self.manager - request_mock.return_value = {'id': 10} + request_mock.return_value = request_mock self.assertFalse(filter_mock.called) self.assertFalse(request_mock.called) result = self.manager.run(1, 2, a=1, b=2, payload={'x': 1, 'y': 2}) - self.assertIsInstance(result, ScriptTrace) + self.assertEqual(request_mock, result) self.assertTrue(filter_mock.called) self.assertTrue(request_mock.called) @@ -521,36 +392,27 @@ def test_run(self, clone_mock, filter_mock, request_mock): self.assertEqual(self.manager.method, 'POST') self.assertEqual(self.manager.endpoint, 'run') - self.assertDictEqual(json.loads(self.manager.data['payload']), {"y": 2, "x": 1}) + self.assertEqual(self.manager.data['payload'], '{"y": 2, "x": 1}') -class ScriptEndpointManagerTestCase(unittest.TestCase): +class WebhookManagerTestCase(unittest.TestCase): def setUp(self): - self.model = ScriptEndpoint - self.manager = ScriptEndpoint.please + self.model = Webhook + self.manager = Webhook.please - @mock.patch('syncano.models.manager.ScriptEndpointManager.request') - @mock.patch('syncano.models.manager.ScriptEndpointManager._filter') - @mock.patch('syncano.models.manager.ScriptEndpointManager._clone') + @mock.patch('syncano.models.manager.WebhookManager.request') + @mock.patch('syncano.models.manager.WebhookManager._filter') + @mock.patch('syncano.models.manager.WebhookManager._clone') def test_run(self, clone_mock, filter_mock, request_mock): clone_mock.return_value = self.manager - request_mock.return_value = { - 'status': 'success', - 'duration': 937, - 'result': 1, - 'executed_at': '2015-03-16T11:52:14.172830Z' - } + request_mock.return_value = request_mock self.assertFalse(filter_mock.called) self.assertFalse(request_mock.called) - result = self.manager.run(1, 2, a=1, b=2, payload={'x': 1, 'y': 2}) - self.assertIsInstance(result, ScriptEndpointTrace) - self.assertEqual(result.status, 'success') - self.assertEqual(result.duration, 937) - self.assertEqual(result.result, 1) - self.assertIsInstance(result.executed_at, datetime) + result = self.manager.run(1, 2, a=1, b=2) + self.assertEqual(request_mock, result) self.assertTrue(filter_mock.called) self.assertTrue(request_mock.called) @@ -558,9 +420,8 @@ def test_run(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, 'POST') + self.assertEqual(self.manager.method, 'GET') self.assertEqual(self.manager.endpoint, 'run') - self.assertDictEqual(json.loads(self.manager.data['payload']), {"y": 2, "x": 1}) class ObjectManagerTestCase(unittest.TestCase): @@ -569,39 +430,65 @@ def setUp(self): self.model = Object self.manager = Object.please - @mock.patch('syncano.models.Object.get_subclass_model') - def test_create(self, get_subclass_model_mock): + @mock.patch('syncano.models.manager.ObjectManager.get_class_model') + def test_create(self, get_model_mock): model_mock = mock.MagicMock() model_mock.return_value = model_mock - get_subclass_model_mock.return_value = model_mock + get_model_mock.return_value = model_mock self.assertFalse(model_mock.called) - self.assertFalse(get_subclass_model_mock.called) + self.assertFalse(get_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.assertTrue(get_model_mock.called) self.assertEqual(instance, model_mock) - model_mock.assert_called_once_with(a=1, b=2, is_lazy=False) + model_mock.assert_called_once_with(a=1, b=2) model_mock.save.assert_called_once_with() - get_subclass_model_mock.assert_called_once_with(a=1, b=2, is_lazy=False) + get_model_mock.assert_called_once_with({'a': 1, 'b': 2}) - @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' - self.manager.properties['class_name'] = 'test' + @mock.patch('syncano.models.manager.ObjectManager.get_class_model') + def test_serialize(self, get_model_mock): + get_model_mock.return_value = mock.Mock - self.assertFalse(get_subclass_model_mock.called) + self.assertFalse(get_model_mock.called) self.manager.serialize({}) - self.assertTrue(get_subclass_model_mock.called) - get_subclass_model_mock.assert_called_once_with(instance_name='test', class_name='test') + self.assertTrue(get_model_mock.called) + + @mock.patch('syncano.models.base.Object.create_subclass') + @mock.patch('syncano.models.manager.ObjectManager.get_class_schema') + @mock.patch('syncano.models.manager.registry.get_model_by_name') + @mock.patch('syncano.models.manager.get_class_name') + def test_get_class_model(self, get_class_name_mock, get_model_by_name_mock, + get_class_schema_mock, create_subclass_mock): + + create_subclass_mock.return_value = create_subclass_mock + get_class_name_mock.side_effect = [ + 'Object', + 'DummyObject', + 'DummyObject', + ] + + get_model_by_name_mock.side_effect = [ + self.manager.model, + LookupError + ] + + result = self.manager.get_class_model({}) + self.assertEqual(self.manager.model, result) + + result = self.manager.get_class_model({}) + self.assertEqual(self.manager.model, result) + + result = self.manager.get_class_model({}) + self.assertEqual(create_subclass_mock, result) @mock.patch('syncano.models.manager.ObjectManager._clone') - @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 + @mock.patch('syncano.models.manager.ObjectManager.get_class_model') + def test_filter(self, get_class_model_mock, clone_mock): + get_class_model_mock.return_value = Instance clone_mock.return_value = self.manager self.manager.filter(name='test') @@ -614,10 +501,7 @@ 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.assertDictEqual( - json.loads(self.manager.query['query']), - {"description": {"_eq": "test"}, "name": {"_gt": "test"}} - ) + self.assertEqual(self.manager.query['query'], '{"description": {"_eq": "test"}, "name": {"_gt": "test"}}') with self.assertRaises(SyncanoValueError): self.manager.filter(dummy_field=4) @@ -625,48 +509,6 @@ 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) - - @mock.patch('syncano.models.manager.Manager.request') - @mock.patch('syncano.models.manager.ObjectManager.serialize') - @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.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( - {'id': 20, 'fielda': 1, 'fieldb': None}, - 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): diff --git a/tests/test_models.py b/build/lib/tests/test_models.py similarity index 55% rename from tests/test_models.py rename to build/lib/tests/test_models.py index ae63899..04e306b 100644 --- a/tests/test_models.py +++ b/build/lib/tests/test_models.py @@ -1,23 +1,22 @@ import unittest -from syncano.exceptions import SyncanoValidationError -from syncano.models import Instance, registry - try: from unittest import mock except ImportError: import mock +from syncano.exceptions import SyncanoValidationError +from syncano.models import Instance, Webhook, CodeBox, Object + class ModelTestCase(unittest.TestCase): def setUp(self): self.model = Instance() - registry.connection.open() def test_init(self): self.assertTrue(hasattr(self.model, '_raw_data')) - self.assertEqual(self.model._raw_data, {}) + self.assertEquals(self.model._raw_data, {}) model = Instance(name='test', dummy_field='dummy') self.assertTrue('name' in model._raw_data) @@ -40,11 +39,11 @@ def test_str(self): self.assertEqual(out, expected) def test_unicode(self): - expected = '<{0}: {1}>'.format( + expected = u'<{0}: {1}>'.format( self.model.__class__.__name__, self.model.pk ) - out = str(self.model) + out = unicode(self.model) self.assertEqual(out, expected) def test_eq(self): @@ -74,7 +73,7 @@ def test_create(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.request.assert_called_with( 'POST', - '/v1.1/instances/', + '/v1/instances/', data={'name': 'test'} ) @@ -91,8 +90,8 @@ def test_update(self, connection_mock): self.assertTrue(connection_mock.called) self.assertTrue(connection_mock.request.called) connection_mock.request.assert_called_with( - 'PUT', - '/v1.1/instances/test/', + 'POST', + '/v1/instances/test/', data={'name': 'test'} ) @@ -103,13 +102,13 @@ def test_update(self, connection_mock): self.assertTrue(connection_mock.request.called) connection_mock.request.assert_called_with( 'PUT', - '/v1.1/instances/test/', + '/v1/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.1/instances/test/'}) + model = Instance(name='test', links={'self': '/v1/instances/test/'}) connection_mock.return_value = connection_mock self.assertFalse(connection_mock.called) @@ -119,32 +118,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.1/instances/test/') - - model = Instance() - with self.assertRaises(SyncanoValidationError): - model.delete() - - @mock.patch('syncano.models.Instance._get_connection') - def test_reload(self, connection_mock): - model = Instance(name='test', links={'self': '/v1.1/instances/test/'}) - connection_mock.return_value = connection_mock - connection_mock.request.return_value = { - 'name': 'new_one', - 'description': 'dummy desc' - } - - self.assertFalse(connection_mock.called) - self.assertFalse(connection_mock.request.called) - self.assertIsNone(model.description) - model.reload() - self.assertTrue(connection_mock.called) - self.assertTrue(connection_mock.request.called) - self.assertEqual(model.name, 'new_one') - self.assertEqual(model.description, 'dummy desc') - - connection_mock.assert_called_once_with() - connection_mock.request.assert_called_once_with('GET', '/v1.1/instances/test/') + connection_mock.request.assert_called_once_with('DELETE', '/v1/instances/test/') model = Instance() with self.assertRaises(SyncanoValidationError): @@ -180,20 +154,130 @@ def test_to_native(self): 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): + +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 = {} self.assertFalse(connection_mock.called) self.assertFalse(connection_mock.request.called) + model.run(a=1, b=2) + self.assertTrue(connection_mock.called) + self.assertTrue(connection_mock.request.called) + + 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.model = Object() + 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' + } + ] + + 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.base.registry') + @mock.patch('syncano.models.base.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) + + +class WebhookTestCase(unittest.TestCase): + def setUp(self): + self.model = Webhook() - Instance(name='test').save(expected_revision=12) + @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/'}) + connection_mock.return_value = connection_mock + self.assertFalse(connection_mock.called) + self.assertFalse(connection_mock.request.called) + model.run() self.assertTrue(connection_mock.called) self.assertTrue(connection_mock.request.called) - connection_mock.request.assert_called_with( - 'POST', - '/v1.1/instances/', - data={'name': 'test', 'expected_revision': 12} - ) + + connection_mock.assert_called_once_with() + connection_mock.request.assert_called_once_with('GET', '/v1/instances/test/webhooks/slug/run/') + + model = Webhook() + with self.assertRaises(SyncanoValidationError): + model.run() diff --git a/tests/test_options.py b/build/lib/tests/test_options.py similarity index 85% rename from tests/test_options.py rename to build/lib/tests/test_options.py index 5bef5dd..438bc94 100644 --- a/tests/test_options.py +++ b/build/lib/tests/test_options.py @@ -1,7 +1,8 @@ import unittest -from syncano.exceptions import SyncanoValidationError, SyncanoValueError -from syncano.models import Field, Instance +from syncano.exceptions import SyncanoValueError +from syncano.models.base import Instance +from syncano.models.fields import Field from syncano.models.options import Options @@ -15,15 +16,15 @@ class Meta: endpoints = { 'detail': { 'methods': ['delete', 'post', 'patch', 'get'], - 'path': '/v1.1/dummy/{name}/', + 'path': '/v1/dummy/{name}/', }, 'list': { 'methods': ['post', 'get'], - 'path': '/v1.1/dummy/', + 'path': '/v1/dummy/', }, 'dummy': { 'methods': ['post', 'get'], - 'path': '/v1.1/dummy/{a}/{b}/', + 'path': '/v1/dummy/{a}/{b}/', 'properties': ['a', 'b'] } } @@ -138,7 +139,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.1/instances/test/v1.1/dummy/a/b/') + self.assertEqual(path, '/v1/instances/test/v1/dummy/a/b/') def test_get_endpoint_query_params(self): properties = {'instance_name': 'test', 'x': 'y'} @@ -149,13 +150,3 @@ 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/') diff --git a/circle.yml b/circle.yml deleted file mode 100644 index dcfc0a2..0000000 --- a/circle.yml +++ /dev/null @@ -1,31 +0,0 @@ -machine: - python: - version: 2.7.5 - -dependencies: - pre: - - pip install -U setuptools - - pip install -r requirements-test.txt - post: - - pyenv local 3.4.3 2.7.6 - -test: - override: - - tox - -general: - artifacts: - - coverage - branches: - ignore: - - gh-pages - -deployment: - production: - branch: master - commands: - - 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" - - ./release.sh diff --git a/custom_sockets.html b/custom_sockets.html new file mode 100644 index 0000000..c3544cb --- /dev/null +++ b/custom_sockets.html @@ -0,0 +1,488 @@ + + + + + + + + + + Custom Sockets in Syncano — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      Custom Sockets in 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. 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

      +

      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.
      +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')  # 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']))
      +
      +# What happened here:
      +# - 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 request, using any defined method to the following API route:
      +# <host>://<api_version>/instances/<instance_name>/endpoints/sockets/my_endpoint/
      +# - To get details for that endpoint, you need to send a GET request to following API route:
      +# <host>://<api_version>/instances/<instance_name>/sockets/my_custom_socket/endpoints/my_endpoint/
      +#
      +# 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 endpoint details (but not call them), use following API route:
      +# <host>://<api_version>/instances/<instance_name>/sockets/my_custom_socket/endpoints/<endpoint>/
      +# and to run your endpoints (e.g. execute Script connected to them), use following API route:
      +# <host>://<api_version>/instances/<instance_name>/endpoints/sockets/<endpoint>/
      +
      +# 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.
      +custom_socket.add_dependency(
      +    ScriptDependency(
      +        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',
      +    )
      +)
      +
      +# 4.3 Using an existing ScriptEndpoint.
      +script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name')
      +custom_socket.add_dependency(
      +    script_endpoint
      +)
      +
      +# 5. Install custom_socket.
      +custom_socket.install()  # this will make an API call and create a script;
      +
      +
      +

      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()
      +print(custom_socket.status)
      +# and
      +print(custom_socket.status_info)
      +
      +
      +
      +
      +

      Updating the custom Socket

      +

      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 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 custom Socket

      +

      To run a custom Socket use:

      +
      # 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

      +

      To get the all defined endpoints in a custom Socket run:

      +
      endpoints = custom_socket.get_endpoints()
      +
      +for endpoint in endpoints:
      +    print(endpoint.name)
      +    print(endpoint.calls)
      +
      +
      +

      To run a particular endpoint:

      +
      endpoint.run(method='GET')
      +# or:
      +endpoint.run(method='POST', data={'name': 'test_name'})
      +
      +
      +

      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 an endpoint, +choose one endpoint first, e.g.:

      +
      +
      endpoint = socket_endpoint_list[0]
      +

      and now run it:

      +
      endpoint.run(method='GET')
      +# or:
      +endpoint.run(method='POST', data={'custom_data': 1})
      +
      +
      +
      +
      +

      Custom Sockets endpoints

      +

      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:

      +
      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'])
      +
      +custom_socket.add_endpoint(my_endpoint)
      +
      +
      +
      +
      +

      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 object. +Currently the only supported dependency is a Script.

      +

      Using new Script

      +
      custom_socket.add_dependency(
      +    ScriptDependency(
      +        Script(
      +            runtime_name=RuntimeChoices.PYTHON_V5_0,
      +            source='print("custom_script")'
      +        ),
      +        name='custom_script'
      +    )
      +)
      +
      +
      +

      Using defined Script

      +
      another_custom_script = Script.please.get(id=2)
      +custom_socket.add_dependency(
      +    ScriptDependency(
      +        another_custom_script,
      +        name='another_custom_script'
      +    )
      +)
      +
      +
      +

      Using defined Script endpoint

      +
      script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name')
      +custom_socket.add_dependency(
      +    script_endpoint
      +)
      +
      +
      +

      You can overwrite the Script name in the following way:

      +
      script_endpoint = ScriptEndpoint.please.get(name='script_endpoint_name')
      +custom_socket.add_dependency(
      +    script_endpoint,
      +    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

      +

      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)
      +
      +
      +

      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 - 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

      +

      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',
      +    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 internal structure of the JSON file must be known by the developer.

      +
      +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index e9c7ee4..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,191 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build -GIT_BRANCH_NAME = $(shell git symbolic-ref --short HEAD) - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Syncano.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Syncano.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/Syncano" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Syncano" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." - -gh-pages: - make html - git checkout gh-pages - cd .. && rm -rf _modules _sources _static refs syncano - mv -fv build/html/* ../ - cd .. && git add -A - cd .. && git commit -am "Generated gh-pages." - cd .. && git push origin gh-pages - git checkout $(GIT_BRANCH_NAME) - -rst: - sphinx-apidoc -o source/refs ../ -T -e diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 0cf0ae8..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source -set I18NSPHINXOPTS=%SPHINXOPTS% source -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Syncano.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Syncano.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index a753817..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Syncano documentation build configuration file, created by -# sphinx-quickstart on Mon Feb 23 13:51:24 2015. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. -from __future__ import unicode_literals - -import sys -from os.path import abspath, dirname - -import sphinx_rtd_theme - -sys.path.insert(1, dirname(dirname(dirname(abspath(__file__))))) -from syncano.models.fields import RelatedManagerField - - -needs_sphinx = '1.0' -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', -] - -if 'spelling' in sys.argv: - extensions.append("sphinxcontrib.spelling") - -spelling_lang = 'en_US' -templates_path = ['_templates'] -source_suffix = '.rst' -# source_encoding = 'utf-8-sig' -master_doc = 'index' -project = 'Syncano' -copyright = 'Syncano Inc' -version = '4.0.0' -release = '4.0.0' -# language = None -# today = '' -# today_fmt = '%B %d, %Y' -exclude_patterns = ['_build', '**tests**', 'build', 'setup.py', 'run_it.py'] -# default_role = None -add_function_parentheses = True -add_module_names = False -# show_authors = False -pygments_style = 'sphinx' -# modindex_common_prefix = ['syncano.'] -# keep_warnings = False - -html_theme = 'sphinx_rtd_theme' -html_theme_options = {} -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] -# html_title = None -# html_short_title = None -# html_logo = None -# html_favicon = None -html_static_path = ['_static'] -# html_extra_path = [] -# html_last_updated_fmt = '%b %d, %Y' -# html_use_smartypants = True -# html_sidebars = {} -# html_additional_pages = {} -# html_domain_indices = True -# html_use_index = True -# html_split_index = False -# html_show_sourcelink = True -# html_show_sphinx = True -# html_show_copyright = True -# html_use_opensearch = '' -# html_file_suffix = None -htmlhelp_basename = 'Syncanodoc' - -latex_elements = {} -latex_documents = [( - 'index', 'Syncano.tex', - 'Syncano Documentation', - 'Syncano', 'manual' -)] -# latex_logo = None -# latex_use_parts = False -# latex_show_pagerefs = False -# latex_show_urls = False -# latex_appendices = [] -# latex_domain_indices = True - -man_pages = [ - ('index', 'syncano', 'Syncano Documentation', - ['Syncano'], 1) -] -# man_show_urls = False - -texinfo_documents = [( - 'index', 'Syncano', 'Syncano Documentation', - 'Syncano', 'Syncano', 'One line description of project.', - 'Miscellaneous' -)] - -# texinfo_appendices = [] -# texinfo_domain_indices = True -# texinfo_show_urls = 'footnote' -# texinfo_no_detailmenu = False - -autodoc_member_order = 'bysource' -highlight_language = 'python' - -RelatedManagerField.__get__ = lambda self, *args, **kwargs: self diff --git a/docs/source/refs/tests.rst b/docs/source/refs/tests.rst deleted file mode 100644 index 6e93559..0000000 --- a/docs/source/refs/tests.rst +++ /dev/null @@ -1,17 +0,0 @@ -tests package -============= - -Submodules ----------- - -.. toctree:: - - tests.test_connection - -Module contents ---------------- - -.. automodule:: tests - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/refs/tests.test_connection.rst b/docs/source/refs/tests.test_connection.rst deleted file mode 100644 index f0753b8..0000000 --- a/docs/source/refs/tests.test_connection.rst +++ /dev/null @@ -1,7 +0,0 @@ -tests.test_connection module -============================ - -.. automodule:: tests.test_connection - :members: - :undoc-members: - :show-inheritance: diff --git a/genindex.html b/genindex.html new file mode 100644 index 0000000..a785149 --- /dev/null +++ b/genindex.html @@ -0,0 +1,4145 @@ + + + + + + + + + + + Index — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + +
      + +
      abstract (DeviceBase.Meta attribute) +
      + +
      + +
      (MessageBase.Meta attribute) +
      + +
      + +
      ACCOUNT_SUFFIX (Connection attribute) +
      + + +
      action (Message attribute) +
      + + +
      ACTION_CHOICES (Message attribute) +
      + + +
      add() (Registry method) +
      + +
      + +
      (SchemaManager method) +
      + +
      + +
      add_call() (Endpoint method) +
      + + +
      add_dependency() (DependencyMetadataMixin method) +
      + + +
      add_endpoint() (EndpointMetadataMixin method) +
      + + +
      add_field() (Options method) +
      + + +
      add_object() (DataEndpoint method) +
      + + +
      add_to_class() (ModelMetaclass method) +
      + + +
      add_to_group() (User method) +
      + + +
      add_user() (Group method) +
      + + +
      Admin (class in syncano.models.accounts) +
      + + +
      admins (Instance attribute) +
      + + +
      all() (Manager method) +
      + + +
      allow_anonymous_read (ApiKey attribute) +
      + + +
      allow_increment (Field attribute) +
      + +
      + +
      (FloatField attribute) +
      + + +
      (IntegerField attribute) +
      + +
      + +
      allow_user_create (ApiKey attribute) +
      + + +
      ALLOWED_LOOKUPS (ObjectManager attribute) +
      + +
      + +
      allowed_methods (SocketEndpoint attribute) +
      + + +
      ALT_LOGIN_PARAMS (Connection attribute) +
      + + +
      amount_off (Coupon attribute) +
      + + +
      api_key (ApiKey attribute) +
      + + +
      api_keys (Instance attribute) +
      + + +
      ApiKey (class in syncano.models.instances) +
      + + +
      apns_devices (Instance attribute) +
      + + +
      apns_messages (Instance attribute) +
      + + +
      APNSConfig (class in syncano.models.push_notification) +
      + + +
      APNSDevice (class in syncano.models.push_notification) +
      + + +
      APNSMessage (class in syncano.models.push_notification) +
      + + +
      ArrayField (class in syncano.models.fields) +
      + + +
      as_batch() (Manager method) +
      + + +
      auth() (User method) +
      + + +
      auth_key (Connection attribute) +
      + + +
      AUTH_SUFFIX (Connection attribute) +
      + + +
      authenticate() (Connection method) +
      + + +
      authenticate_admin() (Connection method) +
      + + +
      authenticate_user() (Connection method) +
      + + +
      author (Message attribute) +
      + +
      + +

      B

      + + + +
      + +
      BaseCall (class in syncano.models.custom_sockets_utils) +
      + + +
      BaseDependency (class in syncano.models.custom_sockets_utils) +
      + + +
      batch() (Manager method) +
      + + +
      batch_object() (syncano.models.archetypes.Model class method) +
      + + +
      BATCH_URI (Manager attribute) +
      + + +
      blank (Field attribute) +
      + + +
      BooleanField (class in syncano.models.fields) +
      + +
      + +
      build_doc() (ModelMetaclass method) +
      + + +
      build_params() (Connection method) +
      + + +
      build_properties() (Options method) +
      + + +
      build_request() (Manager method) +
      + + +
      build_url() (Connection method) +
      + + +
      bulk_create() (Manager method) +
      + +
      + +
      (ObjectManager method) +
      + +
      +
      + +

      C

      + + + +
      + +
      call_type (BaseCall attribute) +
      + +
      + +
      (ScriptCall attribute) +
      + +
      + +
      CallType (class in syncano.models.custom_sockets_utils) +
      + + +
      camelcase_to_underscore() (in module syncano.utils) +
      + + +
      Channel (class in syncano.models.channels) +
      + + +
      channel (Object attribute) +
      + +
      + +
      (Profile attribute) +
      + +
      + +
      channel_name (Message attribute) +
      + + +
      channel_room (Object attribute) +
      + +
      + +
      (Profile attribute) +
      + +
      + +
      ChoiceField (class in syncano.models.fields) +
      + + +
      Class (class in syncano.models.classes) +
      + + +
      CLASS (DependencyType attribute) +
      + + +
      class_name (DataEndpoint attribute) +
      + +
      + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + + +
      (Trigger attribute) +
      + +
      + +
      ClassDependency (class in syncano.models.custom_sockets_utils) +
      + + +
      classes (Instance attribute) +
      + + +
      clear() (SchemaManager method) +
      + + +
      clear_cache() (DataEndpoint method) +
      + + +
      clear_schemas() (Registry method) +
      + + +
      clear_used_instance() (Registry method) +
      + + +
      config (CustomSocket attribute) +
      + +
      + +
      (Script attribute) +
      + +
      + +
      connect() (in module syncano) +
      + + +
      Connection (class in syncano.connection) +
      + + +
      connection (ConnectionMixin attribute) +
      + +
      + +
      (Registry attribute) +
      + +
      + +
      ConnectionMixin (class in syncano.connection) +
      + +
      + +
      content (APNSMessage attribute) +
      + +
      + +
      (CustomResponseMixin attribute) +
      + + +
      (GCMMessage attribute) +
      + + +
      (MessageBase attribute) +
      + + +
      (ResponseTemplate attribute) +
      + +
      + +
      CONTENT_TYPE (Connection attribute) +
      + + +
      content_type (CustomResponseMixin attribute) +
      + +
      + +
      (ResponseTemplate attribute) +
      + +
      + +
      context (ResponseTemplate attribute) +
      + + +
      contribute_to_class() (Field method) +
      + +
      + +
      (Manager method) +
      + + +
      (ModelField method) +
      + + +
      (Options method) +
      + + +
      (RelatedManagerField method) +
      + +
      + +
      count() (ObjectManager method) +
      + + +
      Coupon (class in syncano.models.billing) +
      + + +
      coupon (Discount attribute) +
      + + +
      create() (Manager method) +
      + + +
      create_error_class() (ModelMetaclass method) +
      + + +
      create_from_raw_data() (BaseDependency method) +
      + +
      + +
      (syncano.models.custom_sockets_utils.ClassDependency class method) +
      + + +
      (syncano.models.custom_sockets_utils.ScriptDependency class method) +
      + +
      + +
      create_subclass() (syncano.models.classes.Object class method) +
      + + +
      created_at (APNSDevice attribute) +
      + +
      + +
      (APNSMessage attribute) +
      + + +
      (Class attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (DeviceBase attribute) +
      + + +
      (GCMDevice attribute) +
      + + +
      (GCMMessage attribute) +
      + + +
      (Group attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (Instance attribute) +
      + + +
      (InstanceInvitation attribute) +
      + + +
      (Message attribute) +
      + + +
      (MessageBase attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + + +
      (ResponseTemplate attribute) +
      + + +
      (Schedule attribute) +
      + + +
      (Script attribute) +
      + + +
      (Trigger attribute) +
      + + +
      (User attribute) +
      + +
      + +
      creation_counter (Field attribute) +
      + + +
      crontab (Schedule attribute) +
      + + +
      currency (Coupon attribute) +
      + + +
      CURRENCY_CHOICES (Coupon attribute) +
      + + +
      custom_publish (Channel attribute) +
      + + +
      custom_socket_name (SocketEndpoint attribute) +
      + + +
      CustomResponseHandler (class in syncano.models.custom_response) +
      + + +
      CustomResponseMixin (class in syncano.models.custom_response) +
      + + +
      CustomSocket (class in syncano.models.custom_sockets) +
      + +
      + +

      D

      + + + +
      + +
      data_endpoints (Instance attribute) +
      + + +
      DataEndpoint (class in syncano.models.data_views) +
      + + +
      DataObjectMixin (class in syncano.models.classes) +
      + + +
      date_regex (DateField attribute) +
      + + +
      DateField (class in syncano.models.fields) +
      + + +
      DateTimeField (class in syncano.models.fields) +
      + + +
      default (Field attribute) +
      + + +
      delete() (Manager method) +
      + +
      + +
      (Model method) +
      + +
      + +
      delete_user() (Group method) +
      + + +
      dependencies (CustomSocket attribute) +
      + + +
      dependencies_data (DependencyMetadataMixin attribute) +
      + + +
      dependency_type (BaseDependency attribute) +
      + +
      + +
      (ClassDependency attribute) +
      + + +
      (ScriptDependency attribute) +
      + +
      + +
      DependencyMetadataMixin (class in syncano.models.custom_sockets_utils) +
      + + +
      DependencyType (class in syncano.models.custom_sockets_utils) +
      + + +
      description (ApiKey attribute) +
      + +
      + +
      (Class attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (DataEndpoint attribute) +
      + + +
      (Group attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (Instance attribute) +
      + + +
      (Script attribute) +
      + +
      +
      + +
      detail() (Manager method) +
      + + +
      development_api_key (GCMConfig attribute) +
      + + +
      development_bundle_identifier (APNSConfig attribute) +
      + + +
      development_certificate (APNSConfig attribute) +
      + + +
      development_certificate_name (APNSConfig attribute) +
      + + +
      development_expiration_date (APNSConfig attribute) +
      + + +
      device_id (APNSDevice attribute) +
      + +
      + +
      (DeviceBase attribute) +
      + + +
      (GCMDevice attribute) +
      + +
      + +
      DeviceBase (class in syncano.models.push_notification) +
      + + +
      DeviceBase.Meta (class in syncano.models.push_notification) +
      + + +
      Discount (class in syncano.models.billing) +
      + + +
      Distance (class in syncano.models.geo) +
      + + +
      DoesNotExist (Admin attribute) +
      + +
      + +
      (APNSConfig attribute) +
      + + +
      (APNSDevice attribute) +
      + + +
      (APNSMessage attribute) +
      + + +
      (ApiKey attribute) +
      + + +
      (Channel attribute) +
      + + +
      (Class attribute) +
      + + +
      (Coupon attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (DataEndpoint attribute) +
      + + +
      (Discount attribute) +
      + + +
      (GCMConfig attribute) +
      + + +
      (GCMDevice attribute) +
      + + +
      (GCMMessage attribute) +
      + + +
      (Group attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (HostingFile attribute) +
      + + +
      (Instance attribute) +
      + + +
      (InstanceInvitation attribute) +
      + + +
      (InstanceInvitation.Admin attribute) +
      + + +
      (Message attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + + +
      (ResponseTemplate attribute) +
      + + +
      (Schedule attribute) +
      + + +
      (ScheduleTrace attribute) +
      + + +
      (Script attribute) +
      + + +
      (ScriptEndpoint attribute) +
      + + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (SocketEndpoint attribute) +
      + + +
      (Trigger attribute) +
      + + +
      (TriggerTrace attribute) +
      + + +
      (User attribute) +
      + +
      + +
      domains (Hosting attribute) +
      + + +
      duration (Coupon attribute) +
      + +
      + +
      (ScheduleTrace attribute) +
      + + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (TriggerTrace attribute) +
      + +
      +
      + +

      E

      + + + +
      + +
      email (Admin attribute) +
      + +
      + +
      (InstanceInvitation attribute) +
      + + +
      (InstanceInvitation.Admin attribute) +
      + +
      + +
      EmailField (class in syncano.models.fields) +
      + + +
      end (Discount attribute) +
      + + +
      Endpoint (class in syncano.models.custom_sockets_utils) +
      + + +
      EndpointField (class in syncano.models.fields) +
      + + +
      EndpointMetadataMixin (class in syncano.models.custom_sockets_utils) +
      + + +
      endpoints (CustomSocket attribute) +
      + +
      + +
      endpoints_data (EndpointMetadataMixin attribute) +
      + + +
      error (CustomResponseMixin attribute) +
      + + +
      exclude() (ObjectManager method) +
      + + +
      excluded_fields (DataEndpoint attribute) +
      + + +
      executed_at (ScheduleTrace attribute) +
      + +
      + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (TriggerTrace attribute) +
      + +
      + +
      expand (DataEndpoint attribute) +
      + + +
      expected_revision (Class attribute) +
      + +
      + +

      F

      + + + +
      + +
      Field (class in syncano.models.fields) +
      + + +
      field_lookups (Field attribute) +
      + +
      + +
      (GeoPointField attribute) +
      + + +
      (RelationField attribute) +
      + + +
      (StringField attribute) +
      + +
      + +
      field_name (SyncanoFieldError attribute) +
      + + +
      fields (BaseDependency attribute) +
      + +
      + +
      (ClassDependency attribute) +
      + + +
      (ScriptDependency attribute) +
      + +
      + +
      fields() (ObjectManager method) +
      + + +
      file (HostingFile attribute) +
      + + +
      FileField (class in syncano.models.fields) +
      + +
      + +
      filter() (Manager method) +
      + +
      + +
      (ObjectManager method) +
      + +
      + +
      first() (Manager method) +
      + + +
      first_name (Admin attribute) +
      + +
      + +
      (InstanceInvitation.Admin attribute) +
      + +
      + +
      FloatField (class in syncano.models.fields) +
      + + +
      force_text() (in module syncano.utils) +
      + + +
      FORMAT (DateTimeField attribute) +
      + +
      + +

      G

      + + + +
      + +
      gcm_devices (Instance attribute) +
      + + +
      gcm_messages (Instance attribute) +
      + + +
      GCMConfig (class in syncano.models.push_notification) +
      + + +
      GCMDevice (class in syncano.models.push_notification) +
      + + +
      GCMMessage (class in syncano.models.push_notification) +
      + + +
      GeoPoint (class in syncano.models.geo) +
      + + +
      GeoPointField (class in syncano.models.fields) +
      + + +
      get() (DataEndpoint method) +
      + +
      + +
      (Manager method) +
      + +
      + +
      get_account_info() (Connection method) +
      + + +
      get_all_endpoints() (syncano.models.custom_sockets.SocketEndpoint class method) +
      + + +
      get_allowed_method() (Manager method) +
      + + +
      get_class_name() (in module syncano.utils) +
      + + +
      get_class_object() (syncano.models.classes.DataObjectMixin class method) +
      + + +
      get_class_schema() (syncano.models.classes.Object class method) +
      + + +
      get_config() (Instance method) +
      + + +
      get_dependency_data() (BaseDependency method) +
      + +
      + +
      (ClassDependency method) +
      + + +
      (ScriptDependency method) +
      + +
      + +
      get_endpoint() (Options method) +
      + + +
      get_endpoint_data() (Model method) +
      + + +
      get_endpoint_methods() (Options method) +
      + + +
      get_endpoint_path() (Options method) +
      + + +
      get_endpoint_properties() (Options method) +
      + + +
      get_endpoint_query_params() (Options method) +
      + +
      + +
      get_endpoints() (CustomSocket method) +
      + + +
      get_field() (Options method) +
      + + +
      get_model_by_name() (Registry method) +
      + + +
      get_model_by_path() (Registry method) +
      + + +
      get_model_patterns() (Registry method) +
      + + +
      get_name() (BaseDependency method) +
      + + +
      get_or_create() (Manager method) +
      + + +
      get_or_create_subclass() (syncano.models.classes.Object class method) +
      + + +
      get_path_properties() (Options method) +
      + + +
      get_response_content() (Connection method) +
      + + +
      get_schema() (Registry method) +
      + + +
      get_subclass_model() (syncano.models.classes.Object class method) +
      + + +
      get_subclass_name() (syncano.models.classes.Object class method) +
      + + +
      get_user_info() (Connection method) +
      + + +
      GOLANG (RuntimeChoices attribute) +
      + + +
      group (Channel attribute) +
      + +
      + +
      (Class attribute) +
      + +
      + +
      Group (class in syncano.models.accounts) +
      + + +
      group (Object attribute) +
      + +
      + +
      (Profile attribute) +
      + +
      + +
      group_details() (User method) +
      + + +
      group_permissions (Channel attribute) +
      + +
      + +
      (Class attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + +
      + +
      groups (Instance attribute) +
      + +
      + +

      H

      + + + +
      + +
      has_data (EndpointField attribute) +
      + +
      + +
      (Field attribute) +
      + +
      + +
      has_endpoint_data (EndpointField attribute) +
      + +
      + +
      (Field attribute) +
      + +
      + +
      Hosting (class in syncano.models.hosting) +
      + +
      + +
      hosting_id (HostingFile attribute) +
      + + +
      HostingFile (class in syncano.models.hosting) +
      + + +
      hostings (Instance attribute) +
      + +
      + +

      I

      + + + +
      + +
      id (Admin attribute) +
      + +
      + +
      (APNSConfig attribute) +
      + + +
      (APNSMessage attribute) +
      + + +
      (ApiKey attribute) +
      + + +
      (Discount attribute) +
      + + +
      (GCMConfig attribute) +
      + + +
      (GCMMessage attribute) +
      + + +
      (Group attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (HostingFile attribute) +
      + + +
      (InstanceInvitation attribute) +
      + + +
      (InstanceInvitation.Admin attribute) +
      + + +
      (Message attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + + +
      (ResponseTemplate attribute) +
      + + +
      (Schedule attribute) +
      + + +
      (ScheduleTrace attribute) +
      + + +
      (Script attribute) +
      + + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (Trigger attribute) +
      + + +
      (TriggerTrace attribute) +
      + + +
      (User attribute) +
      + +
      + +
      ignore_acl (ApiKey attribute) +
      + + +
      IGNORED_LINKS (LinksField attribute) +
      + + +
      in_bulk() (Manager method) +
      + + +
      install() (CustomSocket method) +
      + + +
      install_from_url() (CustomSocket method) +
      + + +
      Instance (class in syncano.models.instances) +
      + + +
      instance (Discount attribute) +
      + + +
      instance_name (Admin attribute) +
      + +
      + +
      (APNSConfig attribute) +
      + + +
      (APNSDevice attribute) +
      + + +
      (APNSMessage attribute) +
      + + +
      (ApiKey attribute) +
      + + +
      (Channel attribute) +
      + + +
      (Class attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (DataEndpoint attribute) +
      + + +
      (GCMConfig attribute) +
      + + +
      (GCMDevice attribute) +
      + + +
      (GCMMessage attribute) +
      + + +
      (Group attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (HostingFile attribute) +
      + + +
      (InstanceInvitation attribute) +
      + + +
      (InstanceInvitation.Admin attribute) +
      + + +
      (Message attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + + +
      (ResponseTemplate attribute) +
      + + +
      (Schedule attribute) +
      + + +
      (ScheduleTrace attribute) +
      + + +
      (Script attribute) +
      + + +
      (ScriptEndpoint attribute) +
      + + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (SocketEndpoint attribute) +
      + + +
      (Trigger attribute) +
      + + +
      (TriggerTrace attribute) +
      + + +
      (User attribute) +
      + +
      + +
      InstanceInvitation (class in syncano.models.instances) +
      + + +
      InstanceInvitation.Admin (class in syncano.models.instances) +
      + + +
      IntegerField (class in syncano.models.fields) +
      + +
      + +
      interval_sec (Schedule attribute) +
      + + +
      invitations (Instance attribute) +
      + + +
      is_active (APNSDevice attribute) +
      + +
      + +
      (DeviceBase attribute) +
      + + +
      (GCMDevice attribute) +
      + + +
      (Hosting attribute) +
      + +
      + +
      is_alt_login (Connection attribute) +
      + + +
      is_authenticated() (Connection method) +
      + + +
      is_default (Hosting attribute) +
      + + +
      is_http_method_available() (Options method) +
      + + +
      is_new() (APNSConfig method) +
      + +
      + +
      (GCMConfig method) +
      + + +
      (Model method) +
      + +
      + +
      is_social (Connection attribute) +
      + + +
      is_user (Connection attribute) +
      + + +
      is_valid() (Model method) +
      + + +
      iterator() (Manager method) +
      + +
      + +

      J

      + + + +
      + +
      json_handler() (CustomResponseHandler static method) +
      + + +
      JSONField (class in syncano.models.fields) +
      + +
      + +
      JSONToPythonMixin (class in syncano.models.fields) +
      + +
      + +

      K

      + + + +
      + +
      key (InstanceInvitation attribute) +
      + +
      + +
      KILOMETERS (Distance attribute) +
      + +
      + +

      L

      + + + +
      + +
      label (APNSDevice attribute) +
      + +
      + +
      (DeviceBase attribute) +
      + + +
      (GCMDevice attribute) +
      + + +
      (Group attribute) +
      + + +
      (Schedule attribute) +
      + + +
      (Script attribute) +
      + + +
      (Trigger attribute) +
      + +
      + +
      last_name (Admin attribute) +
      + +
      + +
      (InstanceInvitation.Admin attribute) +
      + +
      + +
      limit() (Manager method) +
      + + +
      links (Admin attribute) +
      + +
      + +
      (APNSDevice attribute) +
      + + +
      (ApiKey attribute) +
      + +
      + +
      LINKS (APNSDevice attribute) +
      + + +
      links (Channel attribute) +
      + +
      + +
      (Class attribute) +
      + + +
      (Coupon attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (DataEndpoint attribute) +
      + + +
      (DeviceBase attribute) +
      + +
      + +
      LINKS (DeviceBase attribute) +
      + + +
      links (Discount attribute) +
      + + +
      LINKS (GCMDevice attribute) +
      + + +
      links (GCMDevice attribute) +
      + +
      + +
      (Group attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (HostingFile attribute) +
      + + +
      (Instance attribute) +
      + + +
      (InstanceInvitation attribute) +
      + + +
      (InstanceInvitation.Admin attribute) +
      + + +
      (Profile attribute) +
      + + +
      (ResponseTemplate attribute) +
      + + +
      (Schedule attribute) +
      + + +
      (ScheduleTrace attribute) +
      + + +
      (Script attribute) +
      + + +
      (ScriptEndpoint attribute) +
      + + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (SocketEndpoint attribute) +
      + + +
      (Trigger attribute) +
      + +
      + +
      LINKS (TriggerTrace attribute) +
      + +
      + +
      links (TriggerTrace attribute) +
      + +
      + +
      (User attribute) +
      + +
      + +
      LinksField (class in syncano.models.fields) +
      + + +
      LinksWrapper (class in syncano.models.fields) +
      + + +
      list() (Manager method) +
      + + +
      list_files() (Hosting method) +
      + + +
      list_groups() (User method) +
      + + +
      list_users() (Group method) +
      + + +
      ListField (class in syncano.models.fields) +
      + + +
      LOGIN_PARAMS (Connection attribute) +
      + + +
      LOOKUP_SEPARATOR (ObjectManager attribute) +
      + +
      + +

      M

      + + + +
      + +
      make_request() (Connection method) +
      + + +
      Manager (class in syncano.models.manager) +
      + + +
      ManagerDescriptor (class in syncano.models.manager) +
      + + +
      mark_for_batch() (Model method) +
      + + +
      Message (class in syncano.models.channels) +
      + + +
      MessageBase (class in syncano.models.push_notification) +
      + +
      + +
      MessageBase.Meta (class in syncano.models.push_notification) +
      + + +
      metadata (Class attribute) +
      + +
      + +
      (CustomSocket attribute) +
      + + +
      (Instance attribute) +
      + + +
      (Message attribute) +
      + +
      + +
      MILES (Distance attribute) +
      + + +
      Model (class in syncano.models.archetypes) +
      + + +
      ModelField (class in syncano.models.fields) +
      + + +
      ModelMetaclass (class in syncano.models.archetypes) +
      + +
      + +

      N

      + + + +
      + +
      name (BaseDependency attribute) +
      + +
      + +
      (Channel attribute) +
      + + +
      (Class attribute) +
      + + +
      (Coupon attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (DataEndpoint attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (Instance attribute) +
      + + +
      (ResponseTemplate attribute) +
      + + +
      (ScriptEndpoint attribute) +
      + + +
      (SocketEndpoint attribute) +
      + +
      + +
      new_update() (Manager method) +
      + + +
      NODEJS (RuntimeChoices attribute) +
      + +
      + +
      NODEJS_V0_4 (RuntimeChoices attribute) +
      + + +
      NODEJS_V1_0 (RuntimeChoices attribute) +
      + + +
      not_indexable_types (SchemaField attribute) +
      + +
      + +

      O

      + + + +
      + +
      Object (class in syncano.models.classes) +
      + + +
      ObjectField (class in syncano.models.fields) +
      + + +
      ObjectManager (class in syncano.models.manager) +
      + + +
      objects (Class attribute) +
      + + +
      objects_count (Class attribute) +
      + + +
      old_update() (Manager method) +
      + + +
      Options (class in syncano.models.options) +
      + +
      + +
      order_by (DataEndpoint attribute) +
      + + +
      order_by() (ObjectManager method) +
      + + +
      ordering() (Manager method) +
      + +
      + +
      (ObjectManager method) +
      + +
      + +
      other_permissions (Channel attribute) +
      + +
      + +
      (Class attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + +
      + +
      overwrite_handler() (CustomResponseHandler method) +
      + + +
      owner (Instance attribute) +
      + +
      + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + +
      + +
      owner_permissions (Object attribute) +
      + +
      + +
      (Profile attribute) +
      + +
      +
      + +

      P

      + + + +
      + +
      page_size (DataEndpoint attribute) +
      + + +
      page_size() (Manager method) +
      + + +
      param_name (FileField attribute) +
      + + +
      parse_date() (DateField method) +
      + + +
      parse_from_date() (DateTimeField method) +
      + + +
      parse_from_string() (DateTimeField method) +
      + + +
      password (User attribute) +
      + + +
      path (HostingFile attribute) +
      + + +
      payload (Message attribute) +
      + +
      + +
      (Schedule attribute) +
      + +
      + +
      percent_off (Coupon attribute) +
      + + +
      PERMISSIONS_CHOICES (Channel attribute) +
      + +
      + +
      (Class attribute) +
      + + +
      (DataEndpoint attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + +
      + +
      PHP (RuntimeChoices attribute) +
      + + +
      pk (Admin attribute) +
      + +
      + +
      (APNSConfig attribute) +
      + + +
      (APNSDevice attribute) +
      + + +
      (APNSMessage attribute) +
      + + +
      (ApiKey attribute) +
      + + +
      (Channel attribute) +
      + + +
      (Class attribute) +
      + + +
      (Coupon attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (DataEndpoint attribute) +
      + + +
      (Discount attribute) +
      + + +
      (GCMConfig attribute) +
      + + +
      (GCMDevice attribute) +
      + + +
      (GCMMessage attribute) +
      + + +
      (Group attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (HostingFile attribute) +
      + + +
      (Instance attribute) +
      + + +
      (InstanceInvitation attribute) +
      + + +
      (InstanceInvitation.Admin attribute) +
      + + +
      (Message attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + + +
      (ResponseTemplate attribute) +
      + + +
      (Schedule attribute) +
      + + +
      (ScheduleTrace attribute) +
      + + +
      (Script attribute) +
      + + +
      (ScriptEndpoint attribute) +
      + + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (SocketEndpoint attribute) +
      + + +
      (Trigger attribute) +
      + + +
      (TriggerTrace attribute) +
      + + +
      (User attribute) +
      + +
      + +
      plain_handler() (CustomResponseHandler static method) +
      + + +
      please (Admin attribute) +
      + +
      + +
      (APNSConfig attribute) +
      + + +
      (APNSDevice attribute) +
      + + +
      (APNSMessage attribute) +
      + + +
      (ApiKey attribute) +
      + + +
      (Channel attribute) +
      + + +
      (Class attribute) +
      + + +
      (Coupon attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (DataEndpoint attribute) +
      + + +
      (Discount attribute) +
      + + +
      (GCMConfig attribute) +
      + + +
      (GCMDevice attribute) +
      + + +
      (GCMMessage attribute) +
      + + +
      (Group attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (HostingFile attribute) +
      + + +
      (Instance attribute) +
      + + +
      (InstanceInvitation attribute) +
      + + +
      (InstanceInvitation.Admin attribute) +
      + + +
      (Message attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + + +
      (ResponseTemplate attribute) +
      + + +
      (Schedule attribute) +
      + + +
      (ScheduleTrace attribute) +
      + + +
      (Script attribute) +
      + + +
      (ScriptEndpoint attribute) +
      + + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (SocketEndpoint attribute) +
      + + +
      (Trigger attribute) +
      + + +
      (TriggerTrace attribute) +
      + + +
      (User attribute) +
      + +
      + +
      poll() (Channel method) +
      + + +
      PollThread (class in syncano.models.channels) +
      + + +
      PREDEFINED_CLASS_NAME (Profile attribute) +
      + +
      + +
      primary_key (Field attribute) +
      + +
      + +
      (PrimaryKeyField attribute) +
      + +
      + +
      PrimaryKeyField (class in syncano.models.fields) +
      + + +
      process_response() (CustomResponseHandler method) +
      + + +
      production_api_key (GCMConfig attribute) +
      + + +
      production_bundle_identifier (APNSConfig attribute) +
      + + +
      production_certificate (APNSConfig attribute) +
      + + +
      production_certificate_name (APNSConfig attribute) +
      + + +
      production_expiration_date (APNSConfig attribute) +
      + + +
      Profile (class in syncano.models.accounts) +
      + + +
      profile (User attribute) +
      + + +
      public (ScriptEndpoint attribute) +
      + + +
      public_link (ScriptEndpoint attribute) +
      + + +
      publish() (Channel method) +
      + + +
      PushJSONField (class in syncano.models.fields) +
      + + +
      PYTHON (RuntimeChoices attribute) +
      + + +
      PYTHON_V4_2 (RuntimeChoices attribute) +
      + + +
      PYTHON_V5_0 (RuntimeChoices attribute) +
      + +
      + +

      Q

      + + + +
      + +
      query (DataEndpoint attribute) +
      + +
      + +
      query_allowed (Field attribute) +
      + +
      + +
      (JSONField attribute) +
      + + +
      (LinksField attribute) +
      + + +
      (RelationField attribute) +
      + + +
      (SchemaField attribute) +
      + +
      +
      + +

      R

      + + + +
      + +
      raw() (Manager method) +
      + + +
      re (DateField attribute) +
      + + +
      read_only (Field attribute) +
      + +
      + +
      (WritableField attribute) +
      + +
      + +
      recheck() (CustomSocket method) +
      + + +
      redeem_by (Coupon attribute) +
      + + +
      ReferenceField (class in syncano.models.fields) +
      + + +
      regex (EmailField attribute) +
      + +
      + +
      (SlugField attribute) +
      + +
      + +
      register() (Connection method) +
      + + +
      register_handler() (CustomResponseHandler method) +
      + + +
      REGISTER_SUFFIX (Connection attribute) +
      + + +
      registration_id (APNSDevice attribute) +
      + +
      + +
      (DeviceBase attribute) +
      + + +
      (GCMDevice attribute) +
      + +
      + +
      Registry (class in syncano.models.registry) +
      + + +
      RelatedManagerField (class in syncano.models.fields) +
      + + +
      RelationField (class in syncano.models.fields) +
      + + +
      reload() (Model method) +
      + + +
      remove() (SchemaManager method) +
      + + +
      remove_dependency() (DependencyMetadataMixin method) +
      + + +
      remove_endpoint() (EndpointMetadataMixin method) +
      + + +
      remove_filter_index() (SchemaManager method) +
      + + +
      remove_from_group() (User method) +
      + + +
      remove_index() (SchemaManager method) +
      + + +
      remove_order_index() (SchemaManager method) +
      + +
      + +
      rename() (DataEndpoint method) +
      + +
      + +
      (ResponseTemplate method) +
      + +
      + +
      render() (ResponseTemplate method) +
      + + +
      request() (Connection method) +
      + +
      + +
      (Manager method) +
      + + +
      (PollThread method) +
      + +
      + +
      required (Field attribute) +
      + +
      + +
      (SchemaField attribute) +
      + + +
      (WritableField attribute) +
      + +
      + +
      resend() (InstanceInvitation method) +
      + + +
      reset_key() (User method) +
      + + +
      reset_link() (ScriptEndpoint method) +
      + + +
      resolve_endpoint() (Options method) +
      + + +
      resolve_parent_data() (Options method) +
      + + +
      response_handler (CustomResponseMixin attribute) +
      + + +
      ResponseTemplate (class in syncano.models.incentives) +
      + + +
      result (APNSMessage attribute) +
      + +
      + +
      (GCMMessage attribute) +
      + + +
      (MessageBase attribute) +
      + + +
      (ScheduleTrace attribute) +
      + + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (TriggerTrace attribute) +
      + +
      + +
      revision (Class attribute) +
      + +
      + +
      (Object attribute) +
      + +
      + +
      RevisionMismatchException +
      + + +
      role (Admin attribute) +
      + +
      + +
      (Instance attribute) +
      + + +
      (InstanceInvitation attribute) +
      + + +
      (InstanceInvitation.Admin attribute) +
      + +
      + +
      ROLE_CHOICES (Admin attribute) +
      + +
      + +
      (InstanceInvitation.Admin attribute) +
      + +
      + +
      room (Message attribute) +
      + + +
      RUBY (RuntimeChoices attribute) +
      + + +
      run() (CustomSocket method) +
      + +
      + +
      (PollThread method) +
      + + +
      (Script method) +
      + + +
      (ScriptEndpoint method) +
      + + +
      (ScriptEndpointManager method) +
      + + +
      (ScriptManager method) +
      + + +
      (SocketEndpoint method) +
      + +
      + +
      runtime_name (Script attribute) +
      + + +
      RuntimeChoices (class in syncano.models.incentives) +
      + +
      + +

      S

      + + + +
      + +
      save() (Class method) +
      + +
      + +
      (Model method) +
      + +
      + +
      Schedule (class in syncano.models.incentives) +
      + + +
      schedule_id (ScheduleTrace attribute) +
      + + +
      scheduled_next (Schedule attribute) +
      + + +
      schedules (Instance attribute) +
      + + +
      ScheduleTrace (class in syncano.models.traces) +
      + + +
      schema (Class attribute) +
      + +
      + +
      (JSONField attribute) +
      + + +
      (SchemaField attribute) +
      + +
      + +
      SchemaField (class in syncano.models.fields) +
      + + +
      SchemaManager (class in syncano.models.manager) +
      + + +
      SCRIPT (CallType attribute) +
      + + +
      Script (class in syncano.models.incentives) +
      + + +
      SCRIPT (DependencyType attribute) +
      + + +
      script (Schedule attribute) +
      + +
      + +
      (ScriptEndpoint attribute) +
      + + +
      (Trigger attribute) +
      + +
      + +
      script_endpoint_name (ScriptEndpointTrace attribute) +
      + + +
      script_endpoints (Instance attribute) +
      + + +
      script_id (ScriptTrace attribute) +
      + + +
      ScriptCall (class in syncano.models.custom_sockets_utils) +
      + + +
      ScriptDependency (class in syncano.models.custom_sockets_utils) +
      + + +
      ScriptEndpoint (class in syncano.models.incentives) +
      + + +
      ScriptEndpointManager (class in syncano.models.manager) +
      + + +
      ScriptEndpointTrace (class in syncano.models.traces) +
      + + +
      ScriptManager (class in syncano.models.manager) +
      + + +
      scripts (Instance attribute) +
      + + +
      ScriptTrace (class in syncano.models.traces) +
      + + +
      send_message() (DeviceBase method) +
      + + +
      serialize() (Manager method) +
      + +
      + +
      (ObjectManager method) +
      + +
      + +
      set() (SchemaManager method) +
      + + +
      set_config() (Instance method) +
      + + +
      set_default() (Hosting method) +
      + + +
      set_default_connection() (Registry method) +
      + + +
      set_default_instance() (Registry method) +
      + + +
      set_default_property() (Registry method) +
      + + +
      set_filter_index() (SchemaManager method) +
      + + +
      set_index() (SchemaManager method) +
      + + +
      set_order_index() (SchemaManager method) +
      + + +
      set_schema() (Registry method) +
      + + +
      set_used_instance() (Registry method) +
      + + +
      signal (Trigger attribute) +
      + + +
      SIGNAL_CHOICES (Trigger attribute) +
      + + +
      SlugField (class in syncano.models.fields) +
      + + +
      SOCIAL_AUTH_SUFFIX (Connection attribute) +
      + + +
      SOCIAL_LOGIN_PARAMS (Connection attribute) +
      + +
      + +
      SocketEndpoint (class in syncano.models.custom_sockets) +
      + + +
      source (Script attribute) +
      + + +
      start (Discount attribute) +
      + + +
      state (InstanceInvitation attribute) +
      + + +
      status (APNSMessage attribute) +
      + +
      + +
      (Class attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (GCMMessage attribute) +
      + + +
      (MessageBase attribute) +
      + + +
      (ScheduleTrace attribute) +
      + + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (TriggerTrace attribute) +
      + +
      + +
      STATUS_CHOICES (ScheduleTrace attribute) +
      + +
      + +
      (ScriptEndpointTrace attribute) +
      + + +
      (ScriptTrace attribute) +
      + + +
      (TriggerTrace attribute) +
      + +
      + +
      status_code (CustomResponseMixin attribute) +
      + + +
      status_info (CustomSocket attribute) +
      + + +
      stop() (PollThread method) +
      + + +
      StringField (class in syncano.models.fields) +
      + + +
      SWIFT (RuntimeChoices attribute) +
      + + +
      syncano (module) +
      + + +
      syncano.connection (module) +
      + + +
      syncano.exceptions (module) +
      + + +
      syncano.models (module) +
      + + +
      syncano.models.accounts (module) +
      + + +
      syncano.models.archetypes (module) +
      + + +
      syncano.models.billing (module) +
      + + +
      syncano.models.channels (module) +
      + + +
      syncano.models.classes (module) +
      + + +
      syncano.models.custom_response (module) +
      + + +
      syncano.models.custom_sockets (module) +
      + + +
      syncano.models.custom_sockets_utils (module) +
      + + +
      syncano.models.data_views (module) +
      + + +
      syncano.models.fields (module) +
      + + +
      syncano.models.geo (module) +
      + + +
      syncano.models.hosting (module) +
      + + +
      syncano.models.incentives (module) +
      + + +
      syncano.models.instances (module) +
      + + +
      syncano.models.manager (module) +
      + + +
      syncano.models.options (module) +
      + + +
      syncano.models.push_notification (module) +
      + + +
      syncano.models.registry (module) +
      + + +
      syncano.models.traces (module) +
      + + +
      syncano.utils (module) +
      + + +
      SyncanoDoesNotExist +
      + + +
      SyncanoException +
      + + +
      SyncanoFieldError +
      + + +
      SyncanoRequestError +
      + + +
      SyncanoValidationError +
      + + +
      SyncanoValueError +
      + +
      + +

      T

      + + + +
      + +
      template() (Manager method) +
      + + +
      templates (Instance attribute) +
      + + +
      timezone (Schedule attribute) +
      + + +
      to_dependency_data() (BaseDependency method) +
      + + +
      to_dict() (BaseCall method) +
      + + +
      to_endpoint_data() (Endpoint method) +
      + + +
      to_native() (DateField method) +
      + +
      + +
      (DateTimeField method) +
      + + +
      (Distance method) +
      + + +
      (Field method) +
      + + +
      (FileField method) +
      + + +
      (GeoPoint method) +
      + + +
      (GeoPointField method) +
      + + +
      (JSONField method) +
      + + +
      (LinksField method) +
      + + +
      (LinksWrapper method) +
      + + +
      (Model method) +
      + + +
      (ModelField method) +
      + + +
      (PushJSONField method) +
      + + +
      (RelationField method) +
      + + +
      (SchemaField method) +
      + +
      + +
      to_python() (BooleanField method) +
      + +
      + +
      (DateField method) +
      + + +
      (DateTimeField method) +
      + + +
      (Field method) +
      + + +
      (FloatField method) +
      + + +
      (GeoPointField method) +
      + + +
      (IntegerField method) +
      + + +
      (JSONToPythonMixin method) +
      + + +
      (LinksField method) +
      + + +
      (Model method) +
      + + +
      (ModelField method) +
      + + +
      (ReferenceField method) +
      + + +
      (RelationField method) +
      + + +
      (SchemaField method) +
      + + +
      (StringField method) +
      + +
      +
      + +
      to_query() (Field method) +
      + +
      + +
      (GeoPointField method) +
      + + +
      (RelationField method) +
      + +
      + +
      traces (Schedule attribute) +
      + +
      + +
      (Script attribute) +
      + + +
      (ScriptEndpoint attribute) +
      + + +
      (Trigger attribute) +
      + +
      + +
      Trigger (class in syncano.models.incentives) +
      + + +
      trigger_id (TriggerTrace attribute) +
      + + +
      triggers (Instance attribute) +
      + + +
      TriggerTrace (class in syncano.models.traces) +
      + + +
      type (Channel attribute) +
      + + +
      TYPE_CHOICES (Channel attribute) +
      + +
      + +

      U

      + + + +
      + +
      underscore_to_camelcase() (in module syncano.utils) +
      + + +
      update() (CustomSocket method) +
      + +
      + +
      (Manager method) +
      + + +
      (Registry method) +
      + +
      + +
      update_dependencies() (DependencyMetadataMixin method) +
      + + +
      update_endpoints() (EndpointMetadataMixin method) +
      + + +
      update_file() (Hosting method) +
      + + +
      update_or_create() (Manager method) +
      + + +
      updated_at (APNSDevice attribute) +
      + +
      + +
      (APNSMessage attribute) +
      + + +
      (Class attribute) +
      + + +
      (CustomSocket attribute) +
      + + +
      (DeviceBase attribute) +
      + + +
      (GCMDevice attribute) +
      + + +
      (GCMMessage attribute) +
      + + +
      (Group attribute) +
      + + +
      (Hosting attribute) +
      + + +
      (Instance attribute) +
      + + +
      (InstanceInvitation attribute) +
      + + +
      (MessageBase attribute) +
      + + +
      (Object attribute) +
      + + +
      (Profile attribute) +
      + + +
      (ResponseTemplate attribute) +
      + + +
      (Script attribute) +
      + + +
      (Trigger attribute) +
      + + +
      (User attribute) +
      + +
      + +
      upload_file() (Hosting method) +
      + + +
      user (APNSDevice attribute) +
      + + +
      User (class in syncano.models.accounts) +
      + + +
      user (DeviceBase attribute) +
      + +
      + +
      (GCMDevice attribute) +
      + +
      +
      + +
      USER_ALT_LOGIN_PARAMS (Connection attribute) +
      + + +
      USER_AUTH_SUFFIX (Connection attribute) +
      + + +
      user_details() (Group method) +
      + + +
      USER_INFO_SUFFIX (Connection attribute) +
      + + +
      user_key (User attribute) +
      + + +
      USER_LOGIN_PARAMS (Connection attribute) +
      + + +
      username (User attribute) +
      + + +
      UserNotFound +
      + + +
      users (Instance attribute) +
      + + +
      using() (Manager method) +
      + +
      + +

      V

      + + + +
      + +
      validate() (ArrayField method) +
      + +
      + +
      (ChoiceField method) +
      + + +
      (EmailField method) +
      + + +
      (Field method) +
      + + +
      (GeoPointField method) +
      + + +
      (JSONField method) +
      + + +
      (ListField method) +
      + + +
      (Model method) +
      + + +
      (ModelField method) +
      + + +
      (ObjectField method) +
      + + +
      (SchemaField method) +
      + + +
      (SlugField method) +
      + +
      +
      + +
      validate_params() (Connection method) +
      + +
      + +

      W

      + + + +
      + +
      with_count() (ObjectManager method) +
      + +
      + +
      WritableField (class in syncano.models.fields) +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/getting_started.html b/getting_started.html new file mode 100644 index 0000000..c83fe3f --- /dev/null +++ b/getting_started.html @@ -0,0 +1,323 @@ + + + + + + + + + + Getting Started with Syncano — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      Getting Started with Syncano

      +

      This tutorial will walk you through installing and configuring syncano, as +well how to use it to make API calls.

      +

      This tutorial assumes you are familiar with Python & that you have registered Syncano account.

      +
      +

      Installing Syncano

      +

      You can use pip to install the latest released version of syncano:

      +
      pip install syncano
      +
      +
      +

      If you want to install syncano from source:

      +
      git clone git@github.com:Syncano/syncano-python.git
      +cd syncano-python
      +python setup.py install
      +
      +
      +
      +
      +

      Using Virtual Environments

      +

      Another common way to install syncano is to use a virtualenv, which +provides isolated environments. First, install the virtualenv Python +package:

      +
      pip install virtualenv
      +
      +
      +

      Next, create a virtual environment by using the virtualenv command and +specifying where you want the virtualenv to be created (you can specify +any directory you like, though this example allows for compatibility with +virtualenvwrapper):

      +
      mkdir ~/.virtualenvs
      +virtualenv ~/.virtualenvs/syncano
      +
      +
      +

      You can now activate the virtual environment:

      +
      source ~/.virtualenvs/syncano/bin/activate
      +
      +
      +

      Now, any usage of python or pip (within the current shell) will default +to the new, isolated version within your virtualenv.

      +

      You can now install syncano into this virtual environment:

      +
      pip install syncano
      +
      +
      +

      When you are done using syncano, you can deactivate your virtual environment:

      +
      deactivate
      +
      +
      +

      If you are creating a lot of virtual environments, virtualenvwrapper +is an excellent tool that lets you easily manage your virtual environments.

      +
      +
      +

      Making Connections

      +

      syncano provides a number of convenience functions to simplify connecting to our services:

      +
      >>> import syncano
      +>>> connection = syncano.connect(email='YOUR_EMAIL', password='YOUR_PASSWORD')
      +
      +
      +

      If you want to use instance in connection you can use connect() function, +then you can omit the instance_name in other calls:

      +
      >>> import syncano
      +>>> 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_name='instance_name', api_key='YOUR_API_KEY')
      +
      +
      +
      +
      +

      Troubleshooting Connections

      +

      When calling the various queries, you might run into an error like this:

      +
      >>> import syncano
      +>>> connection = syncano.connect(api_key='abcd')
      +>>> list(connection.instances)
      +Traceback (most recent call last):
      +  ...
      +    raise SSLError(e, request=request)
      +requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:581)
      +
      +
      +

      This is because your endpoint has invalid SSL certificate. +If you want to skip SSL verification you need to create a new connection like this:

      +
      >>> import syncano
      +>>> connection = syncano.connect(api_key='abcd', verify_ssl=False)
      +>>> list(connection.instances)
      +[<Instance: syncano>]
      +
      +
      +
      +
      +

      Interacting with Syncano

      +

      The following code demonstrates how to create a new Instance +and add a ApiKey to it:

      +
      >>> import syncano
      +>>> connection = syncano.connect(api_key='abcd')
      +>>> instance = connection.instances.create(name='dummy_test', description='test')
      +>>> instance
      +<Instance: dummy_test>
      +
      +>>> api_key = instance.api_keys.create()
      +>>> api_key
      +<ApiKey: 47>
      +>>> api_key.api_key
      +u'aad17f86d41483db7088ad2549ccb87902d60e45'
      +
      +
      +

      Each model has a different set of fields and commands. For more information check available models.

      +
      +
      +

      Next Steps

      +

      If you’d like more information on interacting with Syncano, check out the interacting tutorial +or if you want to know what kind of models are available check out the available models list.

      +
      +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..eba6ecc --- /dev/null +++ b/index.html @@ -0,0 +1,269 @@ + + + + + + + + + + Syncano: A Python interface to Syncano services — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + + + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/interacting.html b/interacting.html new file mode 100644 index 0000000..ef6db89 --- /dev/null +++ b/interacting.html @@ -0,0 +1,452 @@ + + + + + + + + + + Interacting with Syncano — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      Interacting with Syncano

      +

      This tutorial will walk you through our ORM syntax and how to use it to make proper API calls.

      +
      +

      Creating a Connection

      +

      In each example I’ll be assuming that you have configured connection to syncano:

      +
      >>> import syncano
      +>>> connection = syncano.connect(email='YOUR_EMAIL', password='YOUR_PASSWORD')
      +
      +
      +
      +
      +

      Accessing models

      +

      All models are defined in syncano.models.base but syncano simplifies access to them +by attaching all of them directly to connection. Thus:

      +
      from syncano.models.base import Instance
      +
      +
      +

      and:

      +
      Instance = connection.Instance
      +
      +
      +

      are equivalent.

      +
      +
      +

      Creating objects

      +

      A model class represents a single Syncano API endpoint, +and an instance of that class represents a particular record in this endpoint.

      +

      To create an object, instantiate it using keyword arguments to the model class, +then call save() to save it to the Syncano API.

      +

      Here’s an example:

      +
      >>> instance = Instance(name='test-one', description='')
      +>>> instance.save()
      +
      +
      +

      This performs a POST request to Syncano API behind the scenes. +Syncano doesn’t hit the API until you explicitly call save().

      +
      +

      Note

      +

      To create and save an object in a single step, use the create() method. +To create and save multiple objects in a single step, use the bulk_create() method.

      +
      +
      +
      +

      Saving changes to objects

      +

      To save changes to an object that’s already in the Syncano API, use save(). +Regarding our instance from previous example, +this example changes its description and updates its record in the Syncano API:

      +
      >>> instance.description = 'new description'
      +>>> instance.save()
      +
      +
      +

      This performs a PUT request to Syncano API behind the scenes. +Syncano doesn’t hit the API until you explicitly call save().

      +
      +

      Note

      +

      To change and save an object in a single step, use the update() method.

      +
      +
      +
      +

      Retrieving objects

      +

      To retrieve objects from Syncano API, construct a query via a Manager on your model class.

      +

      Each model has only one Manager, and it’s called please by default. +Access it directly via the model class, like so:

      +
      >>> Instance.please
      +[<Instance: test>, <Instance: test-two>, '...(remaining elements truncated)...']
      +>>> i = Instance(name='Foo', description='Bar')
      +>>> i.please
      +Traceback:
      +...
      +AttributeError: Manager isn't accessible via Instance instances.
      +
      +
      +
      +

      Note

      +

      Managers are accessible only via model classes, rather than from model instances, +to enforce a separation between “table-level” operations and “record-level” operations.

      +
      +
      +
      +

      Retrieving all objects

      +

      The simplest way to retrieve objects from a Syncano API is to get all of them. +To do this, use the all() or list() +method on a Manager:

      +
      >>> Instance.please
      +>>> Instance.please.all()
      +>>> Instance.please.list()
      +
      +
      +

      This performs a GET request to Syncano API list endpoint behind the scenes.

      +
      +

      Note

      +

      all() removes any limits from query and loads all +possible objects from API, while the list() method +just executes current query.

      +
      +
      +
      +

      Manager is lazy

      +

      Manager is lazy – the act of creating a Manager doesn’t involve any API activity. +You can stack Manager methods all day long, and Syncano won’t actually run the API call until the Manager is evaluated. +Take a look at this example:

      +
      >>> query = Class.please.list('test-instance')
      +>>> query = query.limit(10)
      +>>> print(query)
      +
      +
      +

      Though this looks like two API calls, in fact it hits API only once, at the last line (print(query)). +In general, the results of a Manager aren’t fetched from API until you “ask” for them.

      +
      +
      +

      Retrieving a single object

      +

      If you know there is only one object that matches your API call, +you can use the get() method on a Manager +which returns the object directly:

      +
      >>> instance = Instance.please.get('instance-name')
      +
      +
      +

      This performs a GET request to Syncano API details endpoint behind the scenes.

      +

      If there are no results that match the API call, get() +will raise a SyncanoDoesNotExist exception. +This exception is an attribute of the model class that the API call is being performed on - so in the code above, +if there is no Instance object with a name equal “instance-name”, Syncano will raise Instance.DoesNotExist.

      +
      +

      Note

      +

      To have more RESTful like method names there is detail() +alias for get() method.

      +
      +
      +
      +

      Removing a single object

      +

      The delete method, conveniently, is named delete(). +This method immediately deletes the object and has no return value. +Example:

      +
      >>> instance = Instance.please.get('test-one')
      +>>> instance.delete()
      +
      +
      +

      This performs a DELETE request to Syncano API details endpoint behind the scenes.

      +
      +
      +

      Limiting returned objects

      +

      Use a subset of Python’s array-slicing syntax to limit your +Manager to a certain number of results.

      +

      For example, this returns the first 5 objects:

      +
      >>> Instance.please[:5]
      +
      +
      +

      This returns the sixth through tenth objects:

      +
      >>> Instance.please[5:10]
      +
      +
      +

      Negative indexing (i.e. Instance.please.all()[-1]) is not supported.

      +
      +

      Note

      +

      If you don’t want to use array-slicing syntax there +is a special manager method called limit().

      +
      +
      +

      Warning

      +

      Python’s array-slicing syntax is a expensive operation in context of API calls so using +limit() is a recommended way.

      +
      +
      +
      +

      Lookups that span relationships

      +

      Syncano API has nested architecture so in some cases there will be a need to provide +a few additional arguments to resolve endpoint URL.

      +

      For example ApiKey is related to Instance and +its URL patter looks like this:

      +
      /v1/instances/{instance_name}/api_keys/{id}
      +
      +
      +

      This example will not work:

      +
      >>> ApiKey.please.list()
      +Traceback:
      +...
      +SyncanoValueError: Request property "instance_name" is required.
      +
      +
      +

      So how to fix that? We need to provide instance_name as an argument +to list() method:

      +
      >>> ApiKey.please.list(instance_name='test-one')
      +[<ApiKey 1>...]
      +
      +
      +

      This performs a GET request to /v1/instances/test-one/api_keys/.

      +
      +

      Note

      +

      Additional request properties are resolved in order as they occurred in URL pattern. +So if you have pattern like this /v1/{a}/{b}/{c}/ list() +method can be invoked like any other Python function i.e list('a', 'b', 'c') or list('a', c='c', b='b').

      +
      +
      +
      +

      Backward relations

      +

      For example Instance has related ApiKey model so +all Instance objects will have backward relation to list of ApiKey‘s:

      +
      >>> instance = Instance.please.get('test-one')
      +>>> instance.api_keys.list()
      +[<ApiKey 1>...]
      +>>> instance.api_keys.get(id=1)
      +<ApiKey 1>
      +
      +
      +
      +

      Note

      +

      Related objects do not require additional request properties passed to +list() method.

      +
      +
      +
      +

      Falling back to raw JSON

      +

      If you find yourself needing to work on raw JSON data instead of Python objects just use +raw() method:

      +
      >>> Instance.please.list()
      +[<Instance: test>, <Instance: test-two>, '...(remaining elements truncated)...']
      +
      +>>> Instance.please.list().raw()
      +[{u'name': u'test-one'...} ...]
      +
      +>>> Instance.please.list().limit(1).raw()
      +[{u'name': u'test-one'...}]
      +
      +>>> Instance.please.raw().get('test-one')
      +{u'name': u'test-one'...}
      +
      +
      +
      +
      +

      Environmental variables

      +

      Some settings can be overwritten via environmental variables e.g:

      +
      $ export SYNCANO_LOGLEVEL=DEBUG
      +$ export SYNCANO_APIROOT='https://127.0.0.1/'
      +$ export SYNCANO_EMAIL=admin@syncano.com
      +$ export SYNCANO_PASSWORD=dummy
      +$ export SYNCANO_APIKEY=dummy123
      +$ export SYNCANO_INSTANCE=test
      +
      +
      +
      +

      Warning

      +

      DEBUG loglevel will disable SSL cert check.

      +
      +
      +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/models.html b/models.html new file mode 100644 index 0000000..eacaa13 --- /dev/null +++ b/models.html @@ -0,0 +1,197 @@ + + + + + + + + + + Available models — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 0000000..9e55955 Binary files /dev/null and b/objects.inv differ diff --git a/py-modindex.html b/py-modindex.html new file mode 100644 index 0000000..f664222 --- /dev/null +++ b/py-modindex.html @@ -0,0 +1,329 @@ + + + + + + + + + + Python Module Index — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      +
        +
      • Docs »
      • + +
      • +
      • + +
      • +
      +
      +
      +
      + + +

      Python Module Index

      + +
      + s +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
       
      + s
      + syncano +
          + syncano.connection +
          + syncano.exceptions +
          + syncano.models +
          + syncano.models.accounts +
          + syncano.models.archetypes +
          + syncano.models.billing +
          + syncano.models.channels +
          + syncano.models.classes +
          + syncano.models.custom_response +
          + syncano.models.custom_sockets +
          + syncano.models.custom_sockets_utils +
          + syncano.models.data_views +
          + syncano.models.fields +
          + syncano.models.geo +
          + syncano.models.hosting +
          + syncano.models.incentives +
          + syncano.models.instances +
          + syncano.models.manager +
          + syncano.models.options +
          + syncano.models.push_notification +
          + syncano.models.registry +
          + syncano.models.traces +
          + syncano.utils +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.connection.html b/refs/syncano.connection.html new file mode 100644 index 0000000..5b52c8c --- /dev/null +++ b/refs/syncano.connection.html @@ -0,0 +1,494 @@ + + + + + + + + + + syncano.connection — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.connection

      +
      +
      +class Connection(host=None, **kwargs)[source]
      +

      Bases: object

      +

      Base connection class.

      + +++ + + + +
      Variables:
        +
      • host – Syncano API host
      • +
      • email – Your Syncano email address
      • +
      • password – Your Syncano password
      • +
      • api_key – Your Syncano Account Key or instance Api Key
      • +
      • user_key – Your Syncano User Key
      • +
      • instance_name – Your Syncano Instance Name
      • +
      • logger – Python logger instance
      • +
      • timeout – Default request timeout
      • +
      • verify_ssl – Verify SSL certificate
      • +
      +
      +
      +
      +CONTENT_TYPE = 'application/json'
      +
      + +
      +
      +ACCOUNT_SUFFIX = 'v1.1/account/'
      +
      + +
      +
      +SOCIAL_AUTH_SUFFIX = 'v1.1/account/auth/{social_backend}/'
      +
      + +
      +
      +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 = set(['password', 'email'])
      +
      + +
      +
      +ALT_LOGIN_PARAMS = set(['api_key'])
      +
      + +
      +
      +USER_LOGIN_PARAMS = set(['instance_name', 'username', 'password', 'api_key'])
      +
      + +
      +
      +USER_ALT_LOGIN_PARAMS = set(['instance_name', 'user_key', 'api_key'])
      +
      + +
      +
      +SOCIAL_LOGIN_PARAMS = set(['social_backend', 'token'])
      +
      + +
      +
      +AUTH_SUFFIX = 'v1.1/account/auth'
      +
      + +
      +
      +is_user[source]
      +
      + +
      +
      +is_social[source]
      +
      + +
      +
      +is_alt_login[source]
      +
      + +
      +
      +auth_key[source]
      +
      + +
      +
      +build_params(params)[source]
      +
      +++ + + + + + + + +
      Parameters:params (dict) – Params which will be passed to request
      Return type:dict
      Returns:Request params
      +
      + +
      +
      +build_url(path)[source]
      +

      Ensures proper format for provided path.

      + +++ + + + + + + + +
      Parameters:path (string) – Request path
      Return type:string
      Returns:Request URL
      +
      + +
      +
      +request(method_name, path, **kwargs)[source]
      +

      Simple wrapper around make_request() which +will ensure that request is authenticated.

      + +++ + + + + + + + +
      Parameters:
        +
      • method_name (string) – HTTP request method e.g: GET
      • +
      • path (string) – Request path or full URL
      • +
      +
      Return type:

      dict

      +
      Returns:

      JSON response

      +
      +
      + +
      +
      +make_request(method_name, path, **kwargs)[source]
      +
      +++ + + + + + + + + + +
      Parameters:
        +
      • method_name (string) – HTTP request method e.g: GET
      • +
      • path (string) – Request path or full URL
      • +
      +
      Return type:

      dict

      +
      Returns:

      JSON response

      +
      Raises:
        +
      • SyncanoValueError – if invalid request method was chosen
      • +
      • SyncanoRequestError – if something went wrong during the request
      • +
      +
      +
      + +
      +
      +get_response_content(url, response)[source]
      +
      + +
      +
      +is_authenticated()[source]
      +

      Checks if current session is authenticated.

      + +++ + + + + + +
      Return type:boolean
      Returns:Session authentication state
      +
      + +
      +
      +authenticate(**kwargs)[source]
      +
      +++ + + + + + + + +
      Parameters:
        +
      • email (string) – Your Syncano account email address
      • +
      • password (string) – Your Syncano password
      • +
      • api_key (string) – Your Syncano api_key for instance
      • +
      +
      Return type:

      string

      +
      Returns:

      Your Account Key

      +
      +
      + +
      +
      +validate_params(kwargs, params)[source]
      +
      + +
      +
      +authenticate_admin(**kwargs)[source]
      +
      + +
      +
      +authenticate_user(**kwargs)[source]
      +
      + +
      +
      +get_account_info(api_key=None)[source]
      +
      + +
      +
      +get_user_info(api_key=None, user_key=None)[source]
      +
      + +
      +
      +register(email, password, first_name=None, last_name=None, invitation_key=None)[source]
      +
      + +
      + +
      +
      +class ConnectionMixin(*args, **kwargs)[source]
      +

      Bases: object

      +

      Injects connection attribute with support of basic validation.

      +
      +
      +connection[source]
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.exceptions.html b/refs/syncano.exceptions.html new file mode 100644 index 0000000..8fd7c88 --- /dev/null +++ b/refs/syncano.exceptions.html @@ -0,0 +1,293 @@ + + + + + + + + + + syncano.exceptions — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.exceptions

      +
      +
      +exception SyncanoException(reason=None, *args)[source]
      +

      Bases: exceptions.Exception

      +

      General Syncano client exception

      +
      + +
      +
      +exception SyncanoValueError(reason=None, *args)[source]
      +

      Bases: syncano.exceptions.SyncanoException

      +

      A Python ValueError error occurred.

      +
      + +
      +
      +exception SyncanoRequestError(status_code, reason, *args)[source]
      +

      Bases: syncano.exceptions.SyncanoException

      +

      An HTTP error occurred.

      + +++ + + + +
      Variables:
        +
      • status_code – HTTP status code e.g: 404
      • +
      • reason – Error text representation
      • +
      +
      +
      + +
      +
      +exception SyncanoValidationError(reason=None, *args)[source]
      +

      Bases: syncano.exceptions.SyncanoValueError

      +

      A validation error occurred.

      +
      + +
      +
      +exception SyncanoFieldError(reason=None, *args)[source]
      +

      Bases: syncano.exceptions.SyncanoValidationError

      +

      A field error occurred.

      + +++ + + + +
      Variables:field_name – Related field name
      +
      +
      +field_name = None
      +
      + +
      + +
      +
      +exception SyncanoDoesNotExist(reason=None, *args)[source]
      +

      Bases: syncano.exceptions.SyncanoException

      +

      Syncano object doesn’t exist error occurred.

      +
      + +
      +
      +exception RevisionMismatchException(status_code, reason, *args)[source]
      +

      Bases: syncano.exceptions.SyncanoRequestError

      +

      Revision do not match with expected one

      +
      + +
      +
      +exception UserNotFound(status_code, reason, *args)[source]
      +

      Bases: syncano.exceptions.SyncanoRequestError

      +

      Special error to handle user not found case.

      +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.html b/refs/syncano.html new file mode 100644 index 0000000..ff8b6eb --- /dev/null +++ b/refs/syncano.html @@ -0,0 +1,294 @@ + + + + + + + + + + Syncano references — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      Syncano references

      + + +
      +

      Module contents

      +
      +
      +connect(*args, **kwargs)[source]
      +

      Connects to Syncano API.

      + +++ + + + + + + + +
      Parameters:
        +
      • email (string) – Your Syncano account email address
      • +
      • password (string) – Your Syncano password
      • +
      • api_key (string) – Your Syncano account key or instance api_key
      • +
      • username (string) – Instance user name
      • +
      • user_key (string) – Instance user key
      • +
      • instance_name (string) – Your Syncano instance_name
      • +
      • verify_ssl (boolean) – Verify SSL certificate
      • +
      +
      Return type:

      syncano.models.registry.Registry

      +
      Returns:

      A models registry

      +
      +

      Usage:

      +
      # Admin login
      +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='')
      +# OR
      +connection = syncano.connect(user_key='', api_key='', instance_name='')
      +
      +
      +
      + +
      +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.accounts.html b/refs/syncano.models.accounts.html new file mode 100644 index 0000000..cfcbf22 --- /dev/null +++ b/refs/syncano.models.accounts.html @@ -0,0 +1,596 @@ + + + + + + + + + + syncano.models.accounts — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.accounts

      +
      +
      +class Admin(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around instance admins link.

      + +++ + + + +
      Variables: +
      +
      +
      +ROLE_CHOICES = ({'display_name': 'full', 'value': 'full'}, {'display_name': 'write', 'value': 'write'}, {'display_name': 'read', 'value': 'read'})
      +
      + +
      +
      +first_name = None
      +
      + +
      +
      +last_name = None
      +
      + +
      +
      +email = None
      +
      + +
      +
      +role = None
      +
      + +
      + +
      + +
      +
      +DoesNotExist
      +

      alias of AdminDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class Profile(**kwargs)[source]
      +

      Bases: syncano.models.classes.DataObjectMixin, syncano.models.classes.Object

      +
      +
      +PREDEFINED_CLASS_NAME = 'user_profile'
      +
      + +
      +
      +PERMISSIONS_CHOICES = ({'display_name': 'None', 'value': 'none'}, {'display_name': 'Read', 'value': 'read'}, {'display_name': 'Write', 'value': 'write'}, {'display_name': 'Full', 'value': 'full'})
      +
      + +
      +
      +owner = None
      +
      + +
      +
      +owner_permissions = None
      +
      + +
      +
      +group = None
      +
      + +
      +
      +group_permissions = None
      +
      + +
      +
      +other_permissions = None
      +
      + +
      +
      +channel = None
      +
      + +
      +
      +channel_room = None
      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +please
      +
      + +
      +
      +DoesNotExist
      +

      alias of ProfileDoesNotExist

      +
      + +
      +
      +class_name = None
      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      + +
      +
      +class User(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around users link.

      + +++ + + + +
      Variables: +
      +
      +
      +username = None
      +
      + +
      +
      +password = None
      +
      + +
      +
      +user_key = None
      +
      + +
      +
      +profile = None
      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +reset_key()[source]
      +
      + +
      +
      +auth(username=None, password=None)[source]
      +
      + +
      +
      +add_to_group(group_id)[source]
      +
      + +
      +
      +list_groups()[source]
      +
      + +
      +
      +group_details(group_id)[source]
      +
      + +
      +
      +remove_from_group(group_id)[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of UserDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class Group(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around groups link.

      + +++ + + + +
      Variables: +
      +
      +
      +label = None
      +
      + +
      +
      +description = None
      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +list_users()[source]
      +
      + +
      +
      +add_user(user_id)[source]
      +
      + +
      +
      +user_details(user_id)[source]
      +
      + +
      +
      +delete_user(user_id)[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of GroupDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.archetypes.html b/refs/syncano.models.archetypes.html new file mode 100644 index 0000000..48e9f9c --- /dev/null +++ b/refs/syncano.models.archetypes.html @@ -0,0 +1,323 @@ + + + + + + + + + + syncano.models.archetypes — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.archetypes

      +
      +
      +class ModelMetaclass[source]
      +

      Bases: type

      +

      Metaclass for all models.

      +
      +
      +add_to_class(name, value)[source]
      +
      + +
      +
      +create_error_class()[source]
      +
      + +
      +
      +build_doc(name, meta)[source]
      +

      Give the class a docstring if it’s not defined.

      +
      + +
      + +
      +
      +class Model(**kwargs)[source]
      +

      Bases: object

      +

      Base class for all models.

      +
      +
      +save(**kwargs)[source]
      +

      Creates or updates the current instance. +Override this in a subclass if you want to control the saving process.

      +
      + +
      +
      +classmethod batch_object(method, path, body, properties=None)[source]
      +
      + +
      +
      +mark_for_batch()[source]
      +
      + +
      +
      +delete(**kwargs)[source]
      +

      Removes the current instance.

      +
      + +
      +
      +reload(**kwargs)[source]
      +

      Reloads the current instance.

      +
      + +
      +
      +validate()[source]
      +

      Validates the current instance.

      + +++ + + + +
      Raises:SyncanoValidationError, SyncanoFieldError
      +
      + +
      +
      +is_valid()[source]
      +
      + +
      +
      +is_new()[source]
      +
      + +
      +
      +to_python(data)[source]
      +

      Converts raw data to python types and built-in objects.

      + +++ + + + +
      Parameters:data (dict) – Raw data
      +
      + +
      +
      +to_native()[source]
      +

      Converts the current instance to raw data which +can be serialized to JSON and send to API.

      +
      + +
      +
      +get_endpoint_data()[source]
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.billing.html b/refs/syncano.models.billing.html new file mode 100644 index 0000000..897934a --- /dev/null +++ b/refs/syncano.models.billing.html @@ -0,0 +1,362 @@ + + + + + + + + + + syncano.models.billing — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.billing

      +
      +
      +class Coupon(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around coupons link.

      + +++ + + + +
      Variables: +
      +
      +
      +CURRENCY_CHOICES = ({'display_name': 'USD', 'value': 'usd'},)
      +
      + +
      +
      +name = None
      +
      + +
      +
      +redeem_by = None
      +
      + +
      + +
      + +
      +
      +percent_off = None
      +
      + +
      +
      +amount_off = None
      +
      + +
      +
      +currency = None
      +
      + +
      +
      +duration = None
      +
      + +
      +
      +DoesNotExist
      +

      alias of CouponDoesNotExist

      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class Discount(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around discounts link.

      + +++ + + + +
      Variables: +
      +
      +
      +instance = None
      +
      + +
      +
      +coupon = None
      +
      + +
      +
      +start = None
      +
      + +
      +
      +end = None
      +
      + +
      + +
      + +
      +
      +DoesNotExist
      +

      alias of DiscountDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.channels.html b/refs/syncano.models.channels.html new file mode 100644 index 0000000..8511fad --- /dev/null +++ b/refs/syncano.models.channels.html @@ -0,0 +1,441 @@ + + + + + + + + + + syncano.models.channels — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.channels

      +
      +
      +class PollThread(connection, endpoint, callback, error=None, *args, **kwargs)[source]
      +

      Bases: threading.Thread

      +
      +
      +request()[source]
      +
      + +
      +
      +run()[source]
      +
      + +
      +
      +stop()[source]
      +
      + +
      + +
      +
      +class Channel(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around channels `link http://docs.syncano.io/docs/realtime-communication`_.

      + +++ + + + +
      Variables: +
      +
      +

      Note

      +

      Channel has two special methods called publish and poll. +First one will send message to the channel:

      +
      >>> channel = Channel.please.get('instance-name', 'channel-name')
      +>>> channel.publish({"x": 1})
      +
      +
      +

      second one will create long polling connection which will listen for messages:

      +
      >>> def callback(message=None):
      +...    print message
      +...    return True
      +
      +>>> channel = Channel.please.get('instance-name', 'channel-name')
      +>>> channel.poll(callback=callback)
      +
      +
      +
      +
      +
      +TYPE_CHOICES = ({'display_name': 'Default', 'value': 'default'}, {'display_name': 'Separate rooms', 'value': 'separate_rooms'})
      +
      + +
      +
      +PERMISSIONS_CHOICES = ({'display_name': 'None', 'value': 'none'}, {'display_name': 'Subscribe', 'value': 'subscribe'}, {'display_name': 'Publish', 'value': 'publish'})
      +
      + +
      +
      +name = None
      +
      + +
      +
      +type = None
      +
      + +
      +
      +group = None
      +
      + +
      +
      +group_permissions = None
      +
      + +
      +
      +other_permissions = None
      +
      + +
      +
      +custom_publish = None
      +
      + +
      + +
      + +
      +
      +poll(room=None, last_id=None, callback=None, error=None, timeout=None)[source]
      +
      + +
      +
      +publish(payload, room=None)[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of ChannelDoesNotExist

      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class Message(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around channel hisotry `link http://docs.syncano.io/docs/realtime-communication`_.

      + +++ + + + +
      Variables: +
      +
      +
      +ACTION_CHOICES = ({'display_name': 'custom', 'value': 0}, {'display_name': 'create', 'value': 1}, {'display_name': 'update', 'value': 2}, {'display_name': 'delete', 'value': 3})
      +
      + +
      +
      +room = None
      +
      + +
      +
      +action = None
      +
      + +
      +
      +author = None
      +
      + +
      +
      +metadata = None
      +
      + +
      +
      +payload = None
      +
      + +
      +
      +created_at = None
      +
      + +
      +
      +DoesNotExist
      +

      alias of MessageDoesNotExist

      +
      + +
      +
      +channel_name = None
      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.classes.html b/refs/syncano.models.classes.html new file mode 100644 index 0000000..52d7ea7 --- /dev/null +++ b/refs/syncano.models.classes.html @@ -0,0 +1,513 @@ + + + + + + + + + + syncano.models.classes — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.classes

      +
      +
      +class Class(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around instance classes link.

      + +++ + + + +
      Variables: +
      +
      +

      Note

      +

      This model is special because each related Object will be +dynamically populated with fields defined in schema attribute.

      +
      +
      +
      +PERMISSIONS_CHOICES = ({'display_name': 'None', 'value': 'none'}, {'display_name': 'Read', 'value': 'read'}, {'display_name': 'Create objects', 'value': 'create_objects'})
      +
      + +
      +
      +name = None
      +
      + +
      +
      +description = None
      +
      + +
      +
      +objects_count = None
      +
      + +
      +
      +schema = None
      +
      + +
      + +
      + +
      +
      +status = None
      +
      + +
      +
      +metadata = None
      +
      + +
      +
      +revision = None
      +
      + +
      +
      +expected_revision = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +created_at = None
      +
      + +
      +
      +group = None
      +
      + +
      +
      +group_permissions = None
      +
      + +
      +
      +other_permissions = None
      +
      + +
      +
      +objects
      +
      + +
      +
      +save(**kwargs)[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of ClassDoesNotExist

      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class Object(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around data objects link.

      + +++ + + + +
      Variables: +
      +
      +

      Note

      +

      This model is special because each instance will be dynamically populated +with fields defined in related 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 = None
      +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +owner = None
      +
      + +
      +
      +owner_permissions = None
      +
      + +
      +
      +group = None
      +
      + +
      +
      +group_permissions = None
      +
      + +
      +
      +other_permissions = None
      +
      + +
      +
      +channel = None
      +
      + +
      +
      +channel_room = None
      +
      + +
      +
      +please
      +
      + +
      +
      +classmethod create_subclass(name, schema)[source]
      +
      + +
      +
      +classmethod get_or_create_subclass(name, schema)[source]
      +
      + +
      +
      +classmethod get_subclass_name(instance_name, class_name)[source]
      +
      + +
      +
      +classmethod get_class_schema(instance_name, class_name)[source]
      +
      + +
      +
      +classmethod get_subclass_model(instance_name, class_name, **kwargs)[source]
      +

      Creates custom Object sub-class definition based +on passed instance_name and class_name.

      +
      + +
      +
      +DoesNotExist
      +

      alias of ObjectDoesNotExist

      +
      + +
      +
      +class_name = None
      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      + +
      +
      +class DataObjectMixin[source]
      +

      Bases: object

      +
      +
      +classmethod get_class_object()[source]
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.custom_response.html b/refs/syncano.models.custom_response.html new file mode 100644 index 0000000..721c530 --- /dev/null +++ b/refs/syncano.models.custom_response.html @@ -0,0 +1,330 @@ + + + + + + + + + + syncano.models.custom_response — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.custom_response

      +
      +
      +class CustomResponseHandler[source]
      +

      Bases: object

      +

      A helper class which allows to define and maintain custom response handlers.

      +

      Consider an example: +Script code:

      +
      set_response(HttpResponse(status_code=200, content='{"one": 1}', content_type='application/json'))
      +
      +
      +

      When suitable ScriptTrace is used:

      +
      trace = ScriptTrace.please.get(id=<code_box_trace_id>, script=<script_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:

      +
      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
      • +
      • text/plain
      • +
      +
      +
      +
      +
      +register_handler(content_type, handler)[source]
      +
      + +
      +
      +overwrite_handler(content_type, handler)[source]
      +
      + +
      +
      +process_response(response)[source]
      +
      + +
      +
      +static json_handler(response)[source]
      +
      + +
      +
      +static plain_handler(response)[source]
      +
      + +
      + +
      +
      +class CustomResponseMixin[source]
      +

      Bases: object

      +
      +
      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.

      +
      +
      +response_handler = <syncano.models.custom_response.CustomResponseHandler object at 0x1d37a90>
      +
      + +
      +
      +content[source]
      +
      + +
      +
      +status_code[source]
      +
      + +
      +
      +error[source]
      +
      + +
      +
      +content_type[source]
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.custom_sockets.html b/refs/syncano.models.custom_sockets.html new file mode 100644 index 0000000..a6dd433 --- /dev/null +++ b/refs/syncano.models.custom_sockets.html @@ -0,0 +1,401 @@ + + + + + + + + + + syncano.models.custom_sockets — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.custom_sockets

      +
      +
      +class CustomSocket(*args, **kwargs)[source]
      +

      Bases: syncano.models.custom_sockets_utils.EndpointMetadataMixin, syncano.models.custom_sockets_utils.DependencyMetadataMixin, syncano.models.archetypes.Model

      +

      OO wrapper around instance custom sockets. +Look at the custom socket documentation for more details.

      + +++ + + + +
      Variables: +
      +
      +
      +name = None
      +
      + +
      +
      +description = None
      +
      + +
      +
      +endpoints = None
      +
      + +
      +
      +dependencies = None
      +
      + +
      +
      +metadata = None
      +
      + +
      +
      +config = None
      +
      + +
      +
      +status = None
      +
      + +
      +
      +status_info = None
      +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      + +
      + +
      +
      +get_endpoints()[source]
      +
      + +
      +
      +run(endpoint_name, method='GET', data=None)[source]
      +
      + +
      +
      +install_from_url(url, instance_name=None, config=None)[source]
      +
      + +
      +
      +install()[source]
      +
      + +
      +
      +update()[source]
      +
      + +
      +
      +recheck()[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of CustomSocketDoesNotExist

      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class SocketEndpoint(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around endpoints defined in CustomSocket instance. +Look at the custom socket documentation for more details.

      + +++ + + + +
      Variables: +
      +
      +
      +name = None
      +
      + +
      +
      +allowed_methods = None
      +
      + +
      + +
      + +
      +
      +run(method='GET', data=None)[source]
      +
      + +
      +
      +classmethod get_all_endpoints(instance_name=None)[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of SocketEndpointDoesNotExist

      +
      + +
      +
      +custom_socket_name = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.custom_sockets_utils.html b/refs/syncano.models.custom_sockets_utils.html new file mode 100644 index 0000000..c45c2b1 --- /dev/null +++ b/refs/syncano.models.custom_sockets_utils.html @@ -0,0 +1,480 @@ + + + + + + + + + + syncano.models.custom_sockets_utils — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.custom_sockets_utils

      +
      +
      +class CallType[source]
      +

      Bases: object

      +

      The type of the call object used in the custom socket;

      +
      +
      +SCRIPT = 'script'
      +
      + +
      + +
      +
      +class DependencyType[source]
      +

      Bases: object

      +

      The type of the dependency object used in the custom socket;

      +
      +
      +SCRIPT = 'script'
      +
      + +
      +
      +CLASS = 'class'
      +
      + +
      + +
      +
      +class BaseCall(name, methods)[source]
      +

      Bases: object

      +

      Base class for call object.

      +
      +
      +call_type = None
      +
      + +
      +
      +to_dict()[source]
      +
      + +
      + +
      +
      +class ScriptCall(name, methods)[source]
      +

      Bases: syncano.models.custom_sockets_utils.BaseCall

      +

      Script call object.

      +

      The JSON format is as follows (to_dict in the base class):

      +
      {
      +    'type': 'script',
      +    'name': '<script_label>,
      +    'methods': [<method_list>],
      +}
      +
      +
      +
      +
      methods can be as follows:
      +
        +
      • [‘GET’]
      • +
      • [‘*’] - which will do a call on every request method;
      • +
      +
      +
      +
      +
      +call_type = 'script'
      +
      + +
      + +
      +
      +class Endpoint(name)[source]
      +

      Bases: object

      +

      The object which stores metadata about endpoints in custom socket;

      +

      The JSON format is as follows:

      +
      {
      +    '<endpoint_name>': {
      +        'calls': [
      +            <list of JSON format of Calls objects>
      +        ]
      +    }
      +}
      +
      +
      +
      +
      +add_call(call)[source]
      +
      + +
      +
      +to_endpoint_data()[source]
      +
      + +
      + +
      +
      +class BaseDependency[source]
      +

      Bases: 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
      +
      + +
      +
      +name = None
      +
      + +
      +
      +to_dependency_data()[source]
      +
      + +
      +
      +get_name()[source]
      +
      + +
      +
      +get_dependency_data()[source]
      +
      + +
      +
      +create_from_raw_data(raw_data)[source]
      +
      + +
      + +
      +
      +class ScriptDependency(script_or_script_endpoint, name=None)[source]
      +

      Bases: syncano.models.custom_sockets_utils.BaseDependency

      +

      Script dependency object;

      +
      +
      The JSON format is as follows::
      +
      +
      {
      +
      ‘type’: ‘script’, +‘runtime_name’: ‘<runtime name defined in RuntimeChoices>’, +‘source’: ‘<source>’, +‘name’: ‘<name>’
      +
      +

      }

      +
      +
      +
      +
      +dependency_type = 'script'
      +
      + +
      +
      +fields = ['runtime_name', 'source']
      +
      + +
      +
      +get_dependency_data()[source]
      +
      + +
      +
      +classmethod create_from_raw_data(raw_data)[source]
      +
      + +
      + +
      +
      +class ClassDependency(class_instance)[source]
      +

      Bases: syncano.models.custom_sockets_utils.BaseDependency

      +

      Class dependency object;

      +
      +
      The JSON format is as follows::
      +
      +
      {
      +

      ‘type’: ‘class’, +‘name’: ‘<class_name>’, +‘schema’: [

      +
      +
      {“name”: “f1”, “type”: “string”}, +{“name”: “f2”, “type”: “string”}, +{“name”: “f3”, “type”: “integer”}
      +

      ],

      +
      +
      +

      }

      +
      +
      +
      +
      +dependency_type = 'class'
      +
      + +
      +
      +fields = ['name', 'schema']
      +
      + +
      +
      +get_dependency_data()[source]
      +
      + +
      +
      +classmethod create_from_raw_data(raw_data)[source]
      +
      + +
      + +
      +
      +class EndpointMetadataMixin(*args, **kwargs)[source]
      +

      Bases: object

      +

      A mixin which allows to collect Endpoints objects and transform them to the appropriate JSON format.

      +
      +
      +update_endpoints()[source]
      +
      + +
      +
      +add_endpoint(endpoint)[source]
      +
      + +
      +
      +remove_endpoint(endpoint_name)[source]
      +
      + +
      +
      +endpoints_data[source]
      +
      + +
      + +
      +
      +class DependencyMetadataMixin(*args, **kwargs)[source]
      +

      Bases: object

      +

      A mixin which allows to collect Dependencies objects and transform them to the appropriate JSON format.

      +
      +
      +update_dependencies()[source]
      +
      + +
      +
      +add_dependency(depedency)[source]
      +
      + +
      +
      +remove_dependency(dependency_name)[source]
      +
      + +
      +
      +dependencies_data[source]
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.data_views.html b/refs/syncano.models.data_views.html new file mode 100644 index 0000000..f2e142a --- /dev/null +++ b/refs/syncano.models.data_views.html @@ -0,0 +1,330 @@ + + + + + + + + + + syncano.models.data_views — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.data_views

      +
      +
      +class DataEndpoint(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      + +++ + + + +
      Variables: +
      +
      +
      +PERMISSIONS_CHOICES = ({'display_name': 'None', 'value': 'none'}, {'display_name': 'Read', 'value': 'read'}, {'display_name': 'Write', 'value': 'write'}, {'display_name': 'Full', 'value': 'full'})
      +
      + +
      +
      +name = None
      +
      + +
      +
      +description = None
      +
      + +
      +
      +query = None
      +
      + +
      +
      +class_name = None
      +
      + +
      +
      +excluded_fields = None
      +
      + +
      +
      +expand = None
      +
      + +
      +
      +order_by = None
      +
      + +
      +
      +page_size = None
      +
      + +
      + +
      + +
      +
      +rename(new_name)[source]
      +
      + +
      +
      +clear_cache()[source]
      +
      + +
      +
      +get(cache_key=None, response_template=None, **kwargs)[source]
      +
      + +
      +
      +add_object(**kwargs)[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of DataEndpointDoesNotExist

      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.fields.html b/refs/syncano.models.fields.html new file mode 100644 index 0000000..db95565 --- /dev/null +++ b/refs/syncano.models.fields.html @@ -0,0 +1,806 @@ + + + + + + + + + + syncano.models.fields — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.fields

      +
      +
      +class JSONToPythonMixin[source]
      +

      Bases: object

      +
      +
      +to_python(value)[source]
      +
      + +
      + +
      +
      +class Field(name=None, **kwargs)[source]
      +

      Bases: object

      +

      Base class for all field types.

      +
      +
      +allow_increment = False
      +
      + +
      +
      +field_lookups = []
      +
      + +
      +
      +default = None
      +
      + +
      +
      +required = False
      +
      + +
      +
      +read_only = True
      +
      + +
      +
      +blank = True
      +
      + +
      +
      +query_allowed = True
      +
      + +
      +
      +has_data = True
      +
      + +
      +
      +has_endpoint_data = False
      +
      + +
      +
      +primary_key = False
      +
      + +
      +
      +creation_counter = 318
      +
      + +
      +
      +validate(value, model_instance)[source]
      +

      Validates the current field instance.

      + +++ + + + +
      Raises:SyncanoFieldError
      +
      + +
      +
      +to_python(value)[source]
      +

      Returns field’s value prepared for usage in Python.

      +
      + +
      +
      +to_native(value)[source]
      +

      Returns field’s value prepared for serialization into JSON.

      +
      + +
      +
      +to_query(value, lookup_type, **kwargs)[source]
      +

      Returns field’s value prepared for usage in HTTP request query.

      +
      + +
      +
      +contribute_to_class(cls, name)[source]
      +
      + +
      + +
      +
      +class RelatedManagerField(model_name, endpoint='list', *args, **kwargs)[source]
      +

      Bases: syncano.models.fields.Field

      +
      +
      +contribute_to_class(cls, name)[source]
      +
      + +
      + +
      +
      +class PrimaryKeyField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.Field

      +
      +
      +primary_key = True
      +
      + +
      + +
      +
      +class WritableField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.Field

      +
      +
      +required = True
      +
      + +
      +
      +read_only = False
      +
      + +
      + +
      +
      +class EndpointField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.WritableField

      +
      +
      +has_data = False
      +
      + +
      +
      +has_endpoint_data = True
      +
      + +
      + +
      +
      +class StringField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.WritableField

      +
      +
      +field_lookups = ['startswith', 'endswith', 'contains', 'istartswith', 'iendswith', 'icontains', 'ieq']
      +
      + +
      +
      +to_python(value)[source]
      +
      + +
      + +
      +
      +class IntegerField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.WritableField

      +
      +
      +allow_increment = True
      +
      + +
      +
      +to_python(value)[source]
      +
      + +
      + +
      +
      +class ReferenceField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.IntegerField

      +
      +
      +to_python(value)[source]
      +
      + +
      + +
      +
      +class FloatField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.WritableField

      +
      +
      +allow_increment = True
      +
      + +
      +
      +to_python(value)[source]
      +
      + +
      + +
      +
      +class BooleanField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.WritableField

      +
      +
      +to_python(value)[source]
      +
      + +
      + +
      +
      +class SlugField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.StringField

      +
      +
      +regex = <_sre.SRE_Pattern object at 0x1c63030>
      +
      + +
      +
      +validate(value, model_instance)[source]
      +
      + +
      + +
      +
      +class EmailField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.StringField

      +
      +
      +regex = <_sre.SRE_Pattern object at 0x1b745f0>
      +
      + +
      +
      +validate(value, model_instance)[source]
      +
      + +
      + +
      +
      +class ChoiceField(*args, **kwargs)[source]
      +

      Bases: syncano.models.fields.WritableField

      +
      +
      +validate(value, model_instance)[source]
      +
      + +
      + +
      +
      +class DateField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.WritableField

      +
      +
      +date_regex = <_sre.SRE_Pattern object at 0x17a4a80>
      +
      + +
      +
      +re = <_sre.SRE_Pattern object at 0x17a4a80>
      +
      + +
      +
      +to_python(value)[source]
      +
      + +
      +
      +parse_date(value)[source]
      +
      + +
      +
      +to_native(value)[source]
      +
      + +
      + +
      +
      +class DateTimeField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.DateField

      +
      +
      +FORMAT = '%Y-%m-%dT%H:%M:%S.%f'
      +
      + +
      +
      +to_python(value)[source]
      +
      + +
      +
      +parse_from_string(value)[source]
      +
      + +
      +
      +parse_from_date(value)[source]
      +
      + +
      +
      +to_native(value)[source]
      +
      + +
      + +
      + +

      Bases: object

      +
      +
      +to_native()[source]
      +
      + +
      + +
      +
      +class LinksField(*args, **kwargs)[source]
      +

      Bases: syncano.models.fields.Field

      +
      +
      +query_allowed = False
      +
      + +
      + +
      + +
      +
      +to_python(value)[source]
      +
      + +
      +
      +to_native(value)[source]
      +
      + +
      + +
      +
      +class ModelField(rel, *args, **kwargs)[source]
      +

      Bases: syncano.models.fields.Field

      +
      +
      +contribute_to_class(cls, name)[source]
      +
      + +
      +
      +validate(value, model_instance)[source]
      +
      + +
      +
      +to_python(value)[source]
      +
      + +
      +
      +to_native(value)[source]
      +
      + +
      + +
      +
      +class FileField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.WritableField

      +
      +
      +param_name = 'files'
      +
      + +
      +
      +to_native(value)[source]
      +
      + +
      + +
      +
      +class JSONField(*args, **kwargs)[source]
      +

      Bases: syncano.models.fields.JSONToPythonMixin, syncano.models.fields.WritableField

      +
      +
      +query_allowed = False
      +
      + +
      +
      +schema = None
      +
      + +
      +
      +validate(value, model_instance)[source]
      +
      + +
      +
      +to_native(value)[source]
      +
      + +
      + +
      +
      +class ArrayField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.JSONToPythonMixin, syncano.models.fields.WritableField

      +
      +
      +validate(value, model_instance)[source]
      +
      + +
      + +
      +
      +class ObjectField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.JSONToPythonMixin, syncano.models.fields.WritableField

      +
      +
      +validate(value, model_instance)[source]
      +
      + +
      + +
      +
      +class SchemaField(*args, **kwargs)[source]
      +

      Bases: syncano.models.fields.JSONField

      +
      +
      +required = False
      +
      + +
      +
      +query_allowed = False
      +
      + +
      +
      +not_indexable_types = ['text', 'file']
      +
      + +
      +
      +schema = {'items': {'type': 'object', 'properties': {'order_index': {'required': False, 'type': 'boolean'}, 'filter_index': {'required': False, 'type': 'boolean'}, 'type': {'required': True, 'type': 'string', 'enum': ['string', 'text', 'integer', 'float', 'boolean', 'datetime', 'file', 'reference', 'relation', 'array', 'object', 'geopoint']}, 'name': {'required': True, 'type': 'string'}, 'target': {'required': False, 'type': 'string'}}}, 'type': 'array'}
      +
      + +
      +
      +validate(value, model_instance)[source]
      +
      + +
      +
      +to_python(value)[source]
      +
      + +
      +
      +to_native(value)[source]
      +
      + +
      + +
      +
      +class PushJSONField(*args, **kwargs)[source]
      +

      Bases: syncano.models.fields.JSONField

      +
      +
      +to_native(value)[source]
      +
      + +
      + +
      +
      +class ListField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.WritableField

      +
      +
      +validate(value, model_instance)[source]
      +
      + +
      + +
      +
      +class GeoPointField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.fields.Field

      +
      +
      +field_lookups = ['near', 'exists']
      +
      + +
      +
      +validate(value, model_instance)[source]
      +
      + +
      +
      +to_native(value)[source]
      +
      + +
      +
      +to_query(value, lookup_type, **kwargs)[source]
      +

      Returns field’s value prepared for usage in HTTP request query.

      +
      + +
      +
      +to_python(value)[source]
      +
      + +
      + +
      +
      +class RelationField(name=None, **kwargs)[source]
      +

      Bases: syncano.models.relations.RelationValidatorMixin, syncano.models.fields.WritableField

      +
      +
      +query_allowed = True
      +
      + +
      +
      +field_lookups = ['contains', 'is']
      +
      + +
      +
      +to_python(value)[source]
      +
      + +
      +
      +to_query(value, lookup_type, related_field_name=None, related_field_lookup=None, **kwargs)[source]
      +
      + +
      +
      +to_native(value)[source]
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.geo.html b/refs/syncano.models.geo.html new file mode 100644 index 0000000..3ee765b --- /dev/null +++ b/refs/syncano.models.geo.html @@ -0,0 +1,230 @@ + + + + + + + + + + syncano.models.geo — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.hosting.html b/refs/syncano.models.hosting.html new file mode 100644 index 0000000..f253dfe --- /dev/null +++ b/refs/syncano.models.hosting.html @@ -0,0 +1,352 @@ + + + + + + + + + + syncano.models.hosting — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.hosting

      +
      +
      +class Hosting(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around hosting.

      +
      +
      +name = None
      +
      + +
      +
      +is_default = None
      +
      + +
      +
      +is_active = None
      +
      + +
      +
      +description = None
      +
      + +
      +
      +domains = None
      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +upload_file(path, file)[source]
      +

      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;

      +
      + +
      +
      +update_file(path, file)[source]
      +

      Updates an existing file. +:param path: the file path; +:param file: the file to be uploaded; +:return: the response from the API;

      +
      + +
      +
      +list_files()[source]
      +
      + +
      +
      +set_default()[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of HostingDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class HostingFile(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around hosting file.

      +
      +
      +path = None
      +
      + +
      +
      +file = None
      +
      + +
      + +
      + +
      +
      +DoesNotExist
      +

      alias of HostingFileDoesNotExist

      +
      + +
      +
      +hosting_id = None
      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.html b/refs/syncano.models.html new file mode 100644 index 0000000..dbe24ad --- /dev/null +++ b/refs/syncano.models.html @@ -0,0 +1,237 @@ + + + + + + + + + + syncano.models — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + + + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.incentives.html b/refs/syncano.models.incentives.html new file mode 100644 index 0000000..d95aec4 --- /dev/null +++ b/refs/syncano.models.incentives.html @@ -0,0 +1,793 @@ + + + + + + + + + + syncano.models.incentives — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.incentives

      +
      +
      +class RuntimeChoices[source]
      +

      Bases: object

      +

      Store available Script runtimes;

      +
      +
      +PYTHON = 'python'
      +
      + +
      +
      +PYTHON_V4_2 = 'python_library_v4.2'
      +
      + +
      +
      +PYTHON_V5_0 = 'python_library_v5.0'
      +
      + +
      +
      +NODEJS = 'nodejs'
      +
      + +
      +
      +NODEJS_V0_4 = 'nodejs_library_v0.4'
      +
      + +
      +
      +NODEJS_V1_0 = 'nodejs_library_v1.0'
      +
      + +
      +
      +GOLANG = 'golang'
      +
      + +
      +
      +SWIFT = 'swift'
      +
      + +
      +
      +PHP = 'php'
      +
      + +
      +
      +RUBY = 'ruby'
      +
      + +
      + +
      +
      +class Script(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around scripts link.

      + +++ + + + +
      Variables: +
      +
      +

      Note

      +

      Script has special method called run which will execute attached source code:

      +
      >>> 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:

      +
      >>> s = Script.please.get('instance-name', 1234)
      +>>> s.run()
      +>>> s.run(variable_one=1, variable_two=2)
      +
      +
      +
      +
      +
      +label = None
      +
      + +
      +
      +description = None
      +
      + +
      +
      +source = None
      +
      + +
      +
      +runtime_name = None
      +
      + +
      +
      +config = None
      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +traces
      +
      + +
      +
      +please
      +
      + +
      +
      +run(**payload)[source]
      +

      Usage:

      +
      >>> s = Script.please.get('instance-name', 1234)
      +>>> s.run()
      +>>> s.run(variable_one=1, variable_two=2)
      +
      +
      +
      + +
      +
      +DoesNotExist
      +

      alias of ScriptDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      + +
      +
      +class Schedule(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around script schedules link.

      + +++ + + + +
      Variables: +
      +
      +
      +label = None
      +
      + +
      +
      +script = None
      +
      + +
      +
      +interval_sec = None
      +
      + +
      +
      +crontab = None
      +
      + +
      +
      +payload = None
      +
      + +
      +
      +timezone = None
      +
      + +
      +
      +created_at = None
      +
      + +
      +
      +scheduled_next = None
      +
      + +
      + +
      + +
      +
      +traces
      +
      + +
      +
      +DoesNotExist
      +

      alias of ScheduleDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class Trigger(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around triggers link.

      + +++ + + + +
      Variables: +
      +
      +
      +SIGNAL_CHOICES = ({'display_name': 'post_update', 'value': 'post_update'}, {'display_name': 'post_create', 'value': 'post_create'}, {'display_name': 'post_delete', 'value': 'post_delete'})
      +
      + +
      +
      +label = None
      +
      + +
      +
      +script = None
      +
      + +
      +
      +class_name = None
      +
      + +
      +
      +signal = None
      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +traces
      +
      + +
      +
      +DoesNotExist
      +

      alias of TriggerDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class ScriptEndpoint(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around script endpoints link.

      + +++ + + + +
      Variables: +
      +
      +

      Note

      +

      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}")
      +
      +
      +

      or via instance:

      +
      >>> se = ScriptEndpoint.please.get('instance-name', 'script-name')
      +>>> se.run()
      +>>> se.run(variable_one=1, variable_two=2)
      +
      +
      +
      +
      +
      +name = None
      +
      + +
      +
      +script = None
      +
      + +
      +
      +public = None
      +
      + +
      + +
      + +
      + +
      + +
      +
      +traces
      +
      + +
      +
      +please
      +
      + +
      +
      +run(cache_key=None, **payload)[source]
      +

      Usage:

      +
      >>> se = ScriptEndpoint.please.get('instance-name', 'script-name')
      +>>> se.run()
      +>>> se.run(variable_one=1, variable_two=2)
      +
      +
      +
      + +
      + +

      Usage:

      +
      >>> se = ScriptEndpoint.please.get('instance-name', 'script-name')
      +>>> se.reset_link()
      +
      +
      +
      + +
      +
      +DoesNotExist
      +

      alias of ScriptEndpointDoesNotExist

      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      + +
      +
      +class ResponseTemplate(**kwargs)[source]
      +

      Bases: syncano.models.mixins.RenameMixin, syncano.models.archetypes.Model

      +

      OO wrapper around templates.

      + +++ + + + +
      Variables: +
      +
      +
      +name = None
      +
      + +
      +
      +content = None
      +
      + +
      +
      +content_type = None
      +
      + +
      +
      +context = None
      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +render(context=None)[source]
      +
      + +
      +
      +rename(new_name)[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of ResponseTemplateDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.instances.html b/refs/syncano.models.instances.html new file mode 100644 index 0000000..3637cf5 --- /dev/null +++ b/refs/syncano.models.instances.html @@ -0,0 +1,652 @@ + + + + + + + + + + syncano.models.instances — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.instances

      +
      +
      +class Instance(**kwargs)[source]
      +

      Bases: syncano.models.mixins.RenameMixin, syncano.models.archetypes.Model

      +

      OO wrapper around instances link.

      + +++ + + + +
      Variables: +
      +
      +
      +name = None
      +
      + +
      +
      +description = None
      +
      + +
      +
      +role = None
      +
      + +
      +
      +owner = None
      +
      + +
      + +
      + +
      +
      +metadata = None
      +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +api_keys
      +
      + +
      +
      +users
      +
      + +
      +
      +admins
      +
      + +
      +
      +groups
      +
      + +
      +
      +scripts
      +
      + +
      +
      +script_endpoints
      +
      + +
      +
      +data_endpoints
      +
      + +
      +
      +templates
      +
      + +
      +
      +triggers
      +
      + +
      +
      +schedules
      +
      + +
      +
      +classes
      +
      + +
      +
      +invitations
      +
      + +
      +
      +hostings
      +
      + +
      +
      +gcm_devices
      +
      + +
      +
      +gcm_messages
      +
      + +
      +
      +apns_devices
      +
      + +
      +
      +apns_messages
      +
      + +
      +
      +get_config()[source]
      +
      + +
      +
      +set_config(config)[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of InstanceDoesNotExist

      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class ApiKey(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around instance api keys link.

      + +++ + + + +
      Variables: +
      +
      +
      +api_key = None
      +
      + +
      +
      +description = None
      +
      + +
      +
      +allow_user_create = None
      +
      + +
      +
      +ignore_acl = None
      +
      + +
      +
      +allow_anonymous_read = None
      +
      + +
      + +
      + +
      +
      +DoesNotExist
      +

      alias of ApiKeyDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class InstanceInvitation(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around instance +invitations link.

      + +++ + + + +
      Variables: +
      +
      +
      +email = None
      +
      + +
      +
      +role = None
      +
      + +
      +
      +key = None
      +
      + +
      +
      +state = None
      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +resend()[source]
      +

      Resend the invitation. +:return: InstanceInvitation instance;

      +
      + +
      +
      +class Admin(**kwargs)
      +

      Bases: syncano.models.archetypes.Model

      +

      OO wrapper around instance admins link.

      + +++ + + + +
      Variables: +
      +
      +
      +DoesNotExist
      +

      alias of AdminDoesNotExist

      +
      + +
      +
      +ROLE_CHOICES = ({'display_name': 'full', 'value': 'full'}, {'display_name': 'write', 'value': 'write'}, {'display_name': 'read', 'value': 'read'})
      +
      + +
      +
      +email = None
      +
      + +
      +
      +first_name = None
      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +last_name = None
      +
      + +
      + +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      +
      +role = None
      +
      + +
      + +
      +
      +InstanceInvitation.DoesNotExist
      +

      alias of InstanceInvitationDoesNotExist

      +
      + +
      +
      +InstanceInvitation.id = None
      +
      + +
      +
      +InstanceInvitation.instance_name = None
      +
      + +
      +
      +InstanceInvitation.pk = None
      +
      + +
      +
      +InstanceInvitation.please
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.manager.html b/refs/syncano.models.manager.html new file mode 100644 index 0000000..01151a7 --- /dev/null +++ b/refs/syncano.models.manager.html @@ -0,0 +1,894 @@ + + + + + + + + + + syncano.models.manager — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.manager

      +
      +
      +class ManagerDescriptor(manager)[source]
      +

      Bases: object

      +
      + +
      +
      +class Manager[source]
      +

      Bases: syncano.connection.ConnectionMixin

      +

      Base class responsible for all ORM (please) actions.

      +
      +
      +BATCH_URI = '/v1.1/instances/{name}/batch/'
      +
      + +
      +
      +as_batch()[source]
      +
      + +
      +
      +batch(*args)[source]
      +

      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.

      +

      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}, <User: 9>, <User: 11>, ...]
      +
      +
      + +++ + + + + + +
      Parameters:args – a arg is on of the: klass.objects.as_batch().create(...), klass.objects.as_batch().update(...), +klass.objects.as_batch().delete(...)
      Returns: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);
      +
      + +
      +
      +create(**kwargs)[source]
      +

      A convenience method for creating an object and saving it all in one step. Thus:

      +
      instance = Instance.please.create(name='test-one', description='description')
      +
      +
      +

      and:

      +
      instance = Instance(name='test-one', description='description')
      +instance.save()
      +
      +
      +

      are equivalent.

      +
      + +
      +
      +bulk_create(*objects)[source]
      +

      Creates many new instances based on provided list of objects.

      +

      Usage:

      +
      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:

      +
      This method is restricted to handle 50 objects at once.
      +
      +
      +
      + +
      +
      +get(*args, **kwargs)[source]
      +

      Returns the object matching the given lookup parameters.

      +

      Usage:

      +
      instance = Instance.please.get('test-one')
      +instance = Instance.please.get(name='test-one')
      +
      +
      +
      + +
      +
      +in_bulk(*args, **kwargs)[source]
      +

      A method which allows to bulk get objects;

      +

      Use:

      +
      response = Classes.please.in_bulk(['test_class', ...])
      +
      +
      +

      response is:

      +
      +
      > {‘test_class’: <Class: 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: <SyncanoTestClassObject: 1>, 2: {u’content’: {u’detail’: u’Not found.’}, u’code’: 404}}
      + +++ + + + + + +
      Parameters:object_ids_list – This list expects the primary keys - id in api, a names, ids can be used here;
      Returns:a dict in which keys are the object_ids_list elements, and values are a populated objects;
      +
      + +
      +
      +detail(*args, **kwargs)[source]
      +

      Wrapper around get method.

      +

      Usage:

      +
      instance = Instance.please.detail('test-one')
      +instance = Instance.please.detail(name='test-one')
      +
      +
      +
      + +
      +
      +get_or_create(**kwargs)[source]
      +

      A convenience method for looking up an object with the given +lookup parameters, creating one if necessary.

      +

      Returns a tuple of (object, created), where object is the retrieved or +created object and created is a boolean specifying whether a new object was created.

      +

      This is meant as a shortcut to boilerplatish code. For example:

      +
      try:
      +    instance = Instance.please.get(name='test-one')
      +except Instance.DoesNotExist:
      +    instance = Instance(name='test-one', description='test')
      +    instance.save()
      +
      +
      +

      The above example can be rewritten using get_or_create() like so:

      +
      instance, created = Instance.please.get_or_create(name='test-one', defaults={'description': 'test'})
      +
      +
      +
      + +
      +
      +delete(*args, **kwargs)[source]
      +

      Removes single instance based on provided arguments. +Returns None if deletion went fine.

      +

      Usage:

      +
      Instance.please.delete('test-one')
      +Instance.please.delete(name='test-one')
      +
      +
      +
      + +
      +
      +filter(*args, **kwargs)[source]
      +
      + +
      +
      +update(*args, **kwargs)[source]
      +
      + +
      +
      +new_update(*args, **kwargs)[source]
      +

      Updates multiple instances based on provided arguments. There to ways to do so:

      +
      +
        +
      1. Django-style update.
      2. +
      3. By specifying arguments.
      4. +
      +
      +

      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;

      +
      + +
      +
      +old_update(*args, **kwargs)[source]
      +

      Updates single instance based on provided arguments. There to ways to do so:

      +
      +
        +
      1. Django-style update.
      2. +
      3. By specifying data argument.
      4. +
      +
      +

      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'})
      +
      +
      +
      + +
      +
      +update_or_create(defaults=None, **kwargs)[source]
      +

      A convenience method for updating an object with the given parameters, creating a new one if necessary. +The defaults is a dictionary of (field, value) pairs used to update the object.

      +

      Returns a tuple of (object, created), where object is the created or updated object and created +is a boolean specifying whether a new object was created.

      +

      The update_or_create method tries to fetch an object from Syncano API based on the given kwargs. +If a match is found, it updates the fields passed in the defaults dictionary.

      +

      This is meant as a shortcut to boilerplatish code. For example:

      +
      try:
      +    instance = Instance.please.update(name='test-one', data=updated_values)
      +except Instance.DoesNotExist:
      +    updated_values.update({'name': 'test-one'})
      +    instance = Instance(**updated_values)
      +    instance.save()
      +
      +
      +

      This pattern gets quite unwieldy as the number of fields in a model goes up. +The above example can be rewritten using update_or_create() like so:

      +
      instance, created = Instance.please.update_or_create(name='test-one',
      +                                                     defaults=updated_values)
      +
      +
      +
      + +
      +
      +all(*args, **kwargs)[source]
      +

      Returns a copy of the current Manager with limit removed.

      +

      Usage:

      +
      instances = Instance.please.all()
      +
      +
      +
      + +
      +
      +list(*args, **kwargs)[source]
      +

      Returns a copy of the current Manager containing objects that match the given lookup parameters.

      +
      +
      Usage::
      +
      instance = Instance.please.list() +classes = Class.please.list(instance_name=’test-one’)
      +
      +
      + +
      +
      +first(*args, **kwargs)[source]
      +

      Returns the first object matched by the lookup parameters or None, if there is no matching object.

      +

      Usage:

      +
      instance = Instance.please.first()
      +classes = Class.please.first(instance_name='test-one')
      +
      +
      +
      + +
      +
      +page_size(*args, **kwargs)[source]
      +

      Sets page size.

      +

      Usage:

      +
      instances = Instance.please.page_size(20).all()
      +
      +
      +
      + +
      +
      +limit(*args, **kwargs)[source]
      +

      Sets limit of returned objects.

      +

      Usage:

      +
      instances = Instance.please.list().limit(10)
      +classes = Class.please.list(instance_name='test-one').limit(10)
      +
      +
      +
      + +
      +
      +ordering(*args, **kwargs)[source]
      +

      Sets order of returned objects.

      +

      Usage:

      +
      instances = Instance.please.ordering()
      +
      +
      +
      + +
      +
      +raw(*args, **kwargs)[source]
      +

      Disables serialization. request method will return raw Python types.

      +

      Usage:

      +
      >>> instances = Instance.please.list().raw()
      +>>> instances
      +[{'description': 'new one', 'name': 'test-one'...}...]
      +
      +
      +
      + +
      +
      +template(*args, **kwargs)[source]
      +

      Disables serialization. request method will return raw text.

      +

      Usage:

      +
      >>> instances = Instance.please.list().template('test')
      +>>> instances
      +u'text'
      +
      +
      +
      + +
      +
      +using(*args, **kwargs)[source]
      +

      Connection juggling.

      +
      + +
      +
      +contribute_to_class(model, name)[source]
      +
      + +
      +
      +serialize(data, model=None)[source]
      +

      Serializes passed data to related Model class.

      +
      + +
      +
      +build_request(request)[source]
      +
      + +
      +
      +request(method=None, path=None, **request)[source]
      +

      Internal method, which calls Syncano API and returns serialized data.

      +
      + +
      +
      +get_allowed_method(*methods)[source]
      +
      + +
      +
      +iterator()[source]
      +

      Pagination handler

      +
      + +
      + +
      +
      +class ScriptManager[source]
      +

      Bases: syncano.models.manager.Manager

      +

      Custom Manager +class for Script model.

      +
      +
      +run(*args, **kwargs)[source]
      +
      + +
      + +
      +
      +class ScriptEndpointManager[source]
      +

      Bases: syncano.models.manager.Manager

      +

      Custom Manager +class for ScriptEndpoint model.

      +
      +
      +run(*args, **kwargs)[source]
      +
      + +
      + +
      +
      +class ObjectManager[source]
      +

      Bases: syncano.models.manager_mixins.IncrementMixin, syncano.models.manager_mixins.ArrayOperationsMixin, syncano.models.manager.Manager

      +

      Custom Manager +class for Object model.

      +
      +
      +LOOKUP_SEPARATOR = '__'
      +
      + +
      +
      +ALLOWED_LOOKUPS = ['gt', 'gte', 'lt', 'lte', 'eq', 'neq', 'exists', 'in', 'nin', 'near', 'is', 'contains', 'startswith', 'endswith', 'contains', 'istartswith', 'iendswith', 'icontains', 'ieq', 'near']
      +
      + +
      +
      +serialize(data, model=None)[source]
      +
      + +
      +
      +count(*args, **kwargs)[source]
      +

      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()
      +
      +
      + +++ + + + +
      Returns:The count of the returned objects: count = DataObjects.please.list(...).count();
      +
      + +
      +
      +with_count(*args, **kwargs)[source]
      +

      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()
      +
      +
      + +++ + + + + + +
      Parameters:page_size – The size of the pagination; Default to 20;
      Returns:The tuple with objects and the count: objects, count = DataObjects.please.list(...).with_count();
      +
      + +
      +
      +filter(*args, **kwargs)[source]
      +

      Special method just for data object Object model.

      +

      Usage:

      +
      objects = Object.please.list('instance-name', 'class-name').filter(henryk__gte='hello')
      +
      +
      +
      + +
      +
      +bulk_create(*objects)[source]
      +

      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')
      +)
      +
      +
      + +++ + + + + + +
      Parameters:objects – a list of the instances of data objects to be created;
      Returns:a created and populated list of objects; When error occurs a plain dict is returned in that place;
      +
      + +
      +
      +fields(*args, **kwargs)[source]
      +

      Special method just for data object Object model.

      +

      Usage:

      +
      objects = Object.please.list('instance-name', 'class-name').fields('name', 'id')
      +
      +
      +
      + +
      +
      +exclude(*args, **kwargs)[source]
      +

      Special method just for data object Object model.

      +

      Usage:

      +
      objects = Object.please.list('instance-name', 'class-name').exclude('avatar')
      +
      +
      +
      + +
      +
      +ordering(order=None)[source]
      +
      + +
      +
      +order_by(*args, **kwargs)[source]
      +

      Sets ordering field of returned objects.

      +

      Usage:

      +
      # ASC order
      +instances = Object.please.order_by('name')
      +
      +# DESC order
      +instances = Object.please.order_by('-name')
      +
      +
      +
      + +
      + +
      +
      +class SchemaManager(schema=None)[source]
      +

      Bases: object

      +

      Custom Manager +class for SchemaFiled.

      +
      +
      +set(value)[source]
      +

      Sets schema value.

      +
      + +
      +
      +add(*objects)[source]
      +

      Adds multiple objects to schema.

      +
      + +
      +
      +remove(*names)[source]
      +

      Removes selected objects based on their names.

      +
      + +
      +
      +clear()[source]
      +

      Sets empty schema.

      +
      + +
      +
      +set_index(field, order=False, filter=False)[source]
      +

      Sets index on selected field.

      + +++ + + + +
      Parameters:
        +
      • field (string) – Name of schema field
      • +
      • filter (bool) – Sets filter index on selected field
      • +
      • order (bool) – Sets order index on selected field
      • +
      +
      +
      + +
      +
      +set_order_index(field)[source]
      +

      Shortcut for set_index(field, order=True).

      +
      + +
      +
      +set_filter_index(field)[source]
      +

      Shortcut for set_index(field, filter=True).

      +
      + +
      +
      +remove_index(field, order=False, filter=False)[source]
      +

      Removes index from selected field.

      + +++ + + + +
      Parameters:
        +
      • field (string) – Name of schema field
      • +
      • filter (bool) – Removes filter index from selected field
      • +
      • order (bool) – Removes order index from selected field
      • +
      +
      +
      + +
      +
      +remove_order_index(field)[source]
      +

      Shortcut for remove_index(field, order=True).

      +
      + +
      +
      +remove_filter_index(field)[source]
      +

      Shortcut for remove_index(field, filter=True).

      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.options.html b/refs/syncano.models.options.html new file mode 100644 index 0000000..2ffb2e7 --- /dev/null +++ b/refs/syncano.models.options.html @@ -0,0 +1,286 @@ + + + + + + + + + + syncano.models.options — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.options

      +
      +
      +class Options(meta=None)[source]
      +

      Bases: syncano.connection.ConnectionMixin

      +

      Holds metadata related to model definition.

      +
      +
      +build_properties()[source]
      +
      + +
      +
      +contribute_to_class(cls, name)[source]
      +
      + +
      +
      +resolve_parent_data()[source]
      +
      + +
      +
      +add_field(field)[source]
      +
      + +
      +
      +get_field(field_name)[source]
      +
      + +
      +
      +get_endpoint(name)[source]
      +
      + +
      +
      +get_endpoint_properties(name)[source]
      +
      + +
      +
      +get_endpoint_path(name)[source]
      +
      + +
      +
      +get_endpoint_methods(name)[source]
      +
      + +
      +
      +resolve_endpoint(endpoint_name, properties, http_method=None)[source]
      +
      + +
      +
      +is_http_method_available(http_method_name, endpoint_name)[source]
      +
      + +
      +
      +get_endpoint_query_params(name, params)[source]
      +
      + +
      +
      +get_path_properties(path)[source]
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.push_notification.html b/refs/syncano.models.push_notification.html new file mode 100644 index 0000000..fcb2947 --- /dev/null +++ b/refs/syncano.models.push_notification.html @@ -0,0 +1,849 @@ + + + + + + + + + + syncano.models.push_notification — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.push_notification

      +
      +
      +class DeviceBase[source]
      +

      Bases: object

      +

      Base abstract class for GCM and APNS Devices;

      +
      + +
      + +
      +
      +registration_id = None
      +
      + +
      +
      +device_id = None
      +
      + +
      +
      +is_active = None
      +
      + +
      +
      +label = None
      +
      + +
      +
      +user = None
      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +class Meta[source]
      +
      +
      +abstract = True
      +
      + +
      + +
      +
      +DeviceBase.send_message(content)[source]
      +

      A method which allows to send message directly to the device; +:param contet: Message content structure - object like; +:return:

      +
      + +
      + +
      +
      +class GCMDevice(**kwargs)[source]
      +

      Bases: syncano.models.push_notification.DeviceBase, syncano.models.archetypes.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='example label',
      +    registration_id=86152312314401555,
      +    user_id=u.id,
      +    device_id='10000000001',
      +)
      +
      +gcm_device.save()
      +
      +Read:
      +gcm_device = GCMDevice.please.get(registration_id=86152312314401554)
      +
      +Delete:
      +gcm_device.delete()
      +
      +Update:
      +gcm_device.label = 'some new label'
      +gcm_device.save()
      +
      +
      +
      +
      +DoesNotExist
      +

      alias of GCMDeviceDoesNotExist

      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +device_id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +is_active = None
      +
      + +
      +
      +label = None
      +
      + +
      + +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      +
      +registration_id = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +user = None
      +
      + +
      + +
      +
      +class APNSDevice(**kwargs)[source]
      +

      Bases: syncano.models.push_notification.DeviceBase, syncano.models.archetypes.Model

      +

      Model which handles the Apple Push Notification Server Device. +CORE supports only Create, Delete and Read;

      +

      Usage:

      +
      Create a new Device:
      +apns_device = APNSDevice(
      +    label='example label',
      +    registration_id='4719084371920471208947120984731208947910827409128470912847120894',
      +    user_id=u.id,
      +    device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a',
      +)
      +apns_device.save()
      +
      +Read:
      +apns_device =
      +    APNSDevice.please.get(registration_id='4719084371920471208947120984731208947910827409128470912847120894')
      +
      +Delete:
      +apns_device.delete()
      +
      +Update:
      +apns_device.label = 'some new label'
      +apns_device.save()
      +
      +
      +
      +

      Note

      +

      Also note the different format (from GCM) of registration_id required by APNS; the device_id have different +format too.

      +
      +
      +
      +DoesNotExist
      +

      alias of APNSDeviceDoesNotExist

      +
      + +
      + +
      + +
      +
      +created_at = None
      +
      + +
      +
      +device_id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +is_active = None
      +
      + +
      +
      +label = None
      +
      + +
      + +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      +
      +registration_id = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +user = None
      +
      + +
      + +
      +
      +class MessageBase[source]
      +

      Bases: object

      +

      Base abstract class for GCM and APNS Messages;

      +
      +
      +status = None
      +
      + +
      +
      +content = None
      +
      + +
      +
      +result = None
      +
      + +
      +
      +created_at = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      +
      +class Meta[source]
      +
      +
      +abstract = True
      +
      + +
      + +
      + +
      +
      +class GCMMessage(**kwargs)[source]
      +

      Bases: syncano.models.push_notification.MessageBase, syncano.models.archetypes.Model

      +

      Model which handles the Google Cloud Messaging Message. +Only creating and reading is allowed.

      +

      Usage:

      +
      Create a new Message:
      +
      +message = GCMMessage(
      +    content={
      +        'registration_ids': [gcm_device.registration_id],  # maximum 1000 elements;
      +        'data': {
      +            'example_data_one': 1,
      +            'example_data_two': 2,
      +        }
      +    }
      +)
      +message.save()
      +
      +
      +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;
      +
      +
      +

      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.

      +
      +
      +
      +DoesNotExist
      +

      alias of GCMMessageDoesNotExist

      +
      + +
      +
      +content = None
      +
      + +
      +
      +created_at = None
      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      +
      +result = None
      +
      + +
      +
      +status = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      + +
      +
      +class APNSMessage(**kwargs)[source]
      +

      Bases: syncano.models.push_notification.MessageBase, syncano.models.archetypes.Model

      +

      Model which handles the Apple Push Notification Server Message. +Only creating and reading is allowed.

      +

      Usage:

      +
      Create new Message:
      +apns_message = APNSMessage(
      +    content={
      +        'registration_ids': [gcm_device.registration_id],
      +        'aps': {'alert': 'test alert'},
      +    }
      +)
      +
      +apns_message.save()
      +
      +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;
      +
      +
      +

      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;

      +
      +
      +
      +DoesNotExist
      +

      alias of APNSMessageDoesNotExist

      +
      + +
      +
      +content = None
      +
      + +
      +
      +created_at = None
      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      +
      +result = None
      +
      + +
      +
      +status = None
      +
      + +
      +
      +updated_at = None
      +
      + +
      + +
      +
      +class GCMConfig(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.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 = None
      +
      + +
      +
      +development_api_key = None
      +
      + +
      +
      +is_new()[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of GCMConfigDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      +
      +class APNSConfig(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      +

      A model which stores information with APNS Push certificates;

      +

      Usage:

      +
      Add (modify) new keys:
      +cert_file = open('cert_file.p12', 'rb')
      +apns_config = APNSConfig(development_certificate=cert_file)
      +apns_config.save()
      +cert_file.close()
      +
      +
      +
      +
      +production_certificate_name = None
      +
      + +
      +
      +production_certificate = None
      +
      + +
      +
      +production_bundle_identifier = None
      +
      + +
      +
      +production_expiration_date = None
      +
      + +
      +
      +development_certificate_name = None
      +
      + +
      +
      +development_certificate = None
      +
      + +
      +
      +development_bundle_identifier = None
      +
      + +
      +
      +development_expiration_date = None
      +
      + +
      +
      +is_new()[source]
      +
      + +
      +
      +DoesNotExist
      +

      alias of APNSConfigDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.registry.html b/refs/syncano.models.registry.html new file mode 100644 index 0000000..b861366 --- /dev/null +++ b/refs/syncano.models.registry.html @@ -0,0 +1,291 @@ + + + + + + + + + + syncano.models.registry — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.registry

      +
      +
      +class Registry(models=None)[source]
      +

      Bases: object

      +

      Models registry.

      +
      +
      +get_model_patterns(cls)[source]
      +
      + +
      +
      +get_model_by_path(path)[source]
      +
      + +
      +
      +get_model_by_name(name)[source]
      +
      + +
      +
      +update(name, cls)[source]
      +
      + +
      +
      +add(name, cls)[source]
      +
      + +
      +
      +set_default_property(name, value)[source]
      +
      + +
      +
      +set_default_instance(value)[source]
      +
      + +
      +
      +set_used_instance(instance)[source]
      +
      + +
      +
      +clear_used_instance()[source]
      +
      + +
      +
      +get_schema(class_name)[source]
      +
      + +
      +
      +set_schema(class_name, schema)[source]
      +
      + +
      +
      +clear_schemas()[source]
      +
      + +
      +
      +set_default_connection(default_connection)[source]
      +
      + +
      +
      +connection[source]
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.models.traces.html b/refs/syncano.models.traces.html new file mode 100644 index 0000000..6f4ff5c --- /dev/null +++ b/refs/syncano.models.traces.html @@ -0,0 +1,547 @@ + + + + + + + + + + syncano.models.traces — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.models.traces

      +
      +
      +class ScriptTrace(**kwargs)[source]
      +

      Bases: syncano.models.custom_response.CustomResponseMixin, syncano.models.archetypes.Model

      + +++ + + + +
      Variables: +
      +
      +
      +STATUS_CHOICES = ({'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'})
      +
      + +
      +
      +status = None
      +
      + +
      + +
      + +
      +
      +executed_at = None
      +
      + +
      +
      +result = None
      +
      + +
      +
      +duration = None
      +
      + +
      +
      +DoesNotExist
      +

      alias of ScriptTraceDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      +
      +script_id = None
      +
      + +
      + +
      +
      +class ScheduleTrace(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      + +++ + + + +
      Variables: +
      +
      +
      +STATUS_CHOICES = ({'display_name': 'Success', 'value': 'success'}, {'display_name': 'Failure', 'value': 'failure'}, {'display_name': 'Timeout', 'value': 'timeout'}, {'display_name': 'Pending', 'value': 'pending'})
      +
      + +
      +
      +status = None
      +
      + +
      + +
      + +
      +
      +executed_at = None
      +
      + +
      +
      +result = None
      +
      + +
      +
      +duration = None
      +
      + +
      +
      +DoesNotExist
      +

      alias of ScheduleTraceDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      +
      +schedule_id = None
      +
      + +
      + +
      +
      +class TriggerTrace(**kwargs)[source]
      +

      Bases: syncano.models.archetypes.Model

      + +++ + + + +
      Variables: +
      +
      +
      +STATUS_CHOICES = ({'display_name': 'Success', 'value': 'success'}, {'display_name': 'Failure', 'value': 'failure'}, {'display_name': 'Timeout', 'value': 'timeout'}, {'display_name': 'Pending', 'value': 'pending'})
      +
      + +
      + +
      + +
      +
      +status = None
      +
      + +
      + +
      + +
      +
      +executed_at = None
      +
      + +
      +
      +result = None
      +
      + +
      +
      +duration = None
      +
      + +
      +
      +DoesNotExist
      +

      alias of TriggerTraceDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      +
      +trigger_id = None
      +
      + +
      + +
      +
      +class ScriptEndpointTrace(**kwargs)[source]
      +

      Bases: syncano.models.custom_response.CustomResponseMixin, syncano.models.archetypes.Model

      + +++ + + + +
      Variables: +
      +
      +
      +STATUS_CHOICES = ({'display_name': 'Success', 'value': 'success'}, {'display_name': 'Failure', 'value': 'failure'}, {'display_name': 'Timeout', 'value': 'timeout'}, {'display_name': 'Pending', 'value': 'pending'})
      +
      + +
      +
      +status = None
      +
      + +
      + +
      + +
      +
      +executed_at = None
      +
      + +
      +
      +result = None
      +
      + +
      +
      +duration = None
      +
      + +
      +
      +DoesNotExist
      +

      alias of ScriptEndpointTraceDoesNotExist

      +
      + +
      +
      +id = None
      +
      + +
      +
      +instance_name = None
      +
      + +
      +
      +pk = None
      +
      + +
      +
      +please
      +
      + +
      +
      +script_endpoint_name = None
      +
      + +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/refs/syncano.utils.html b/refs/syncano.utils.html new file mode 100644 index 0000000..ecee8fc --- /dev/null +++ b/refs/syncano.utils.html @@ -0,0 +1,232 @@ + + + + + + + + + + syncano.utils module — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + + + + +
      + + + + + + +
      +
      +
      + +
      +
      +
      + +
      +

      syncano.utils module

      +
      +
      +camelcase_to_underscore(text)[source]
      +

      Converts camelcase text to underscore format.

      +
      + +
      +
      +underscore_to_camelcase(text)[source]
      +

      Converts underscore text to camelcase format.

      +
      + +
      +
      +get_class_name(*args)[source]
      +

      Generates safe class name based on provided arguments.

      +
      + +
      +
      +force_text(s, encoding='utf-8', strings_only=False, errors='strict')[source]
      +
      + +
      + + +
      + +
      +
      + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/release.sh b/release.sh deleted file mode 100755 index 0682dbc..0000000 --- a/release.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/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 diff --git a/requirements-docs.txt b/requirements-docs.txt deleted file mode 100644 index 77f659d..0000000 --- a/requirements-docs.txt +++ /dev/null @@ -1,2 +0,0 @@ -Sphinx==1.2.3 -sphinx-rtd-theme==0.1.6 diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index aff493c..0000000 --- a/requirements-test.txt +++ /dev/null @@ -1 +0,0 @@ -tox==2.3.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 70cdd96..0000000 --- a/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -Unidecode==0.4.18 -coverage==3.7.1 -pep8==1.5.7 -flake8==2.4.1 -funcsigs==0.4 -isort==4.0.0 -mccabe==0.3.1 -mock==1.3.0 -nose==1.3.7 -pbr==1.6.0 -pyflakes==0.8.1 -python-slugify==0.1.0 -requests==2.7.0 -six==1.9.0 -validictory==1.0.0 diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index ddc6f6b..0000000 --- a/run_tests.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -set -e - -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 diff --git a/search.html b/search.html new file mode 100644 index 0000000..b3efbc1 --- /dev/null +++ b/search.html @@ -0,0 +1,213 @@ + + + + + + + + + + Search — Syncano 4.0.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 0000000..ed2a025 --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({envversion:42,terms:{set_schema:2,represent:11,code:[19,9,11,15,25,17,18],get_class_object:20,henryk__gt:17,skip:18,global:15,is_act:[4,5],auth_suffix:23,verify_ssl:[12,18,23],mile:7,related_field_nam:14,subclass:1,abil:19,follow:[15,4,24,18,19],apnsdevic:4,lookup:8,make_request:23,content:8,nodej:25,python_v5_0:[19,25],user_profil:[19,26],api_kei:[18,23,12,9,3],specif:19,send:[19,4,1,13],environment:8,common:18,snippet:17,under:[15,19],triggerdoesnotexist:25,set:[19,23,17,9,18],deactiv:18,sourc:[0,1,2,3,4,5,6,7,10,11,12,13,14,15,16,17,18,19,20,21,23,24,25,26,27],everi:[4,24],string:[19,12,14,23,24,17],fals:[14,17,18,27],runtime_nam:[19,25,24],trigger_id:0,new_socket_nam:19,util:12,fall:8,veri:19,social_backend:[12,23],syntax:9,remove_filter_index:17,tri:17,production_expiration_d:4,objects_count:20,level:9,set_index:17,get_all_endpoint:[19,10],script_label:24,list:[19,9,14,24,17,18],last_nam:[23,26,3],iter:17,hosting_id:5,item:14,build_request:17,nodejs_v1_0:25,stderr:15,verif:18,syncanovalidationerror:[1,11],remove_endpoint:[19,24],prepar:14,pleas:[19,20,0,21,9,10,3,15,4,25,5,26,6,17,13],repres:9,cache_kei:[25,21],syncanodoesnotexist:[9,11],hostingfil:5,apnsmessag:4,second:[19,13],possibl:[15,4,9,19],dummy123:9,pass:[19,20,9,23,4,17,18],managerdescriptor:17,compat:18,index:[8,17,9],what:[19,18],response_handl:15,sub:20,currenc:6,neg:9,abl:19,"while":9,camelcase_to_underscor:27,dataobject:17,current:[19,1,9,14,23,15,17,18],delet:[19,1,9,4,17,13],version:18,primary_kei:14,build_doc:1,sixth:9,kilomet:7,"new":[19,9,4,5,17,18],dependencytyp:24,add_depend:[19,24],method:[19,1,9,10,23,24,4,25,17,13],metadata:[20,16,10,24,3,13],make:19,customsocketdoesnotexist:10,set_default_properti:2,full:[20,21,23,26,3],devicebas:4,gener:[19,27,9,11],never:8,here:[19,17,9],bodi:[19,1],update_depend:24,let:18,modelmetaclass:1,address:[12,23],path:[16,1,2,23,5,17],modifi:4,object_ids_list:17,valu:[20,0,14,1,9,2,3,25,21,26,6,17,13],amount_off:6,search:8,expected_revis:20,invit:3,host:[19,23],datetim:14,cert:9,base:[0,1,2,3,4,5,6,7,9,10,11,13,14,15,16,17,20,21,23,24,25,26,27],auth_kei:23,dependency_typ:24,action:[17,13],chang:[19,8],coupondoesnotexist:6,manager_mixin:17,other_permiss:[20,26,13],get_subclass_nam:20,overrid:1,via:[25,9],set_respons:15,percent_off:6,plain:[15,17],group_permiss:[20,26,13],"_sre":14,modul:[19,8],prefer:19,submodul:8,is_authent:23,dependencymetadatamixin:[24,10],put:9,customsocket:[19,10],api:[19,1,9,17,23,3,5,12,18],apn:4,visibl:19,syncano_apikei:9,select:17,regex:14,test_nam:19,usd:6,objectmanag:17,would:19,commun:13,clear_schema:2,regist:[18,23],two:[19,17,9,13],dure:23,websit:18,few:9,handler:[15,17],call:[19,9,10,24,25,17,18,13],boilerplatish:17,criteria:19,taken:[19,24],custom_sockets_util:10,plain_handl:15,type:[19,0,1,17,14,23,24,4,12,13],until:9,more:[4,9,10,18],arrayoperationsmixin:17,delete_us:26,classmethod:[20,1,24,10],get_nam:24,relat:8,remove_depend:[19,24],deped:24,iendswith:[14,17],warn:17,post_delet:25,order_index:14,test_class:17,syncanofielderror:[14,1,11],is_us:23,particular:[19,9],known:19,actual:9,hold:16,content_typ:[15,25,23],must:19,none:[20,0,14,1,10,11,21,2,23,24,3,4,25,5,26,6,7,17,13,16],retriev:8,room:13,alia:[20,0,21,9,10,3,4,25,5,26,6,13],setup:18,work:[17,9],allow_user_cr:3,gcmconfigdoesnotexist:4,some_c:17,some_b:17,remain:9,social_login_param:23,can:[19,1,9,24,15,17,18],syncanorequesterror:[23,11],meet:19,script_id:[15,0],fetch:[17,9],def:[15,13],abcd:18,overwritten:9,omit:18,apns_devic:[4,3],date_regex:14,give:[19,1],process:[15,0,1],schemafield:[20,14,21],templat:[25,17,3],filter_index:14,want:[1,9,18],customresponsemixin:[15,0],serial:[17,14,1],occur:[15,17,9,11],relationfield:14,datetimefield:[20,0,14,3,25,26,13],post_creat:25,multipl:[17,9],variou:18,first_nam:[23,26,3],rather:9,anoth:[19,18],discountdoesnotexist:6,apns_config:4,account_suffix:23,read_onli:14,how:[9,18],syncanotestclassobject:17,instead:9,config:[25,10,3],overwrit:[15,19],isn:9,resourc:[19,17],calltyp:24,my_endpoint_3:19,clone:18,after:[19,4],befor:8,custom_nam:19,callback:13,mixin:[15,25,24,3],mai:19,end:6,underscor:27,data:[19,20,1,9,10,15,4,17],demonstr:18,device_id:4,github:[12,18],updated_at:[20,10,3,4,25,5,26],ssl:[23,12,9,18],my_custom_socket:19,created_at:[20,10,3,4,25,5,26,13],geopointfield:14,correspond:17,list_group:26,element:[4,17,9],development_expiration_d:4,inform:[19,4,17,18],maintain:15,excluded_field:21,allow:[19,24,15,4,17,18],relatedmanagerfield:[20,14,3],doesnotexist:[20,0,21,9,10,3,4,25,5,26,6,17,13],order:[17,9],"0x17a4a80":14,least:19,loglevel:9,gcm:4,linksfield:[14,10],becaus:[20,18],objectdoesnotexist:20,through:[9,18],process_respons:15,dynam:20,paramet:[4,17,1,23,12],write:[20,21,26,3],style:17,bulk_creat:[17,9],group:[20,26,13,3],directli:[4,9,18],user_auth_suffix:23,chosen:[19,23],scheduled_next:25,fix:9,gcmdevic:4,coupon:6,pend:0,set_filter_index:17,post_upd:25,gcmconfig:4,alter:4,get_response_cont:23,redeem_bi:6,them:[19,9,24],within:18,"return":[19,8],thei:[19,9],python:[19,18],safe:27,dai:9,initi:[19,4],scene:9,p12:4,verifi:[12,18,23],bar:9,now:[19,18,24],runtimechoic:[19,25,24],gcm_config:4,name:[0,1,2,3,4,5,6,9,10,11,12,13,14,16,17,18,19,20,21,23,24,25,27],simpl:[15,23],refresh:19,separ:[9,13],easili:[15,18],token:[12,23],register_handl:15,timeout:[0,23,13],each:[19,20,9,18],debug:[4,9],found:[17,11],went:[17,23],attributeerror:9,truncat:9,mean:19,subset:9,domain:5,syncano_inst:9,create_from_raw_data:24,example_data_on:4,clear_cach:21,crontab:25,upload:5,ensur:23,meta:[19,4,16,1],"static":15,connect:19,permissions_choic:[20,21,26,13],our:[19,9,18],happen:19,syncano_email:9,tenth:9,special:[19,20,9,11,25,17,13],out:18,user_b:17,"try":17,primarykeyfield:14,user_a:17,upload_fil:5,goe:17,reset_kei:26,user_detail:26,publish:13,payload:[25,13],profil:26,writablefield:14,suitabl:15,rel:14,print:[19,9,13],set_order_index:17,metaclass:1,"4ecc":4,development_certif:4,scheduletracedoesnotexist:0,differ:[4,18],currency_choic:6,standard:19,asc:17,reason:11,orm:[17,9],dictionari:17,unwieldi:17,releas:18,refer:8,pollthread:13,"7189d7b9":4,extend:15,perform:9,architectur:9,thread:13,production_api_kei:4,traceback:[9,18],ask:9,wrong:23,set_config:3,filter:17,length:17,enforc:9,place:[19,17],authenticate_us:23,lte:17,scripttracedoesnotexist:0,timezon:25,syncanovalueerror:[23,9,11],first:[19,17,9,13,18],oper:9,channel_nam:13,instance_nam:[19,20,0,21,9,10,17,23,3,4,25,5,26,12,18,13],render:25,onc:[17,9],arrai:[14,9],custom_data:19,number:[17,9,18],your_password:[9,18],yourself:9,restrict:17,email:[9,23,3,26,12,18],alreadi:[19,9],endpointfield:14,wrapper:[20,10,23,3,25,5,26,6,17,13],endpoint_nam:[19,16,24,10],blank:14,owner:[20,26,3],open:4,primari:17,hood:[15,19],size:17,avail:[19,25,18],given:17,rewritten:17,script:[19,24,3,15,25,17],mkdir:18,sometim:17,messag:[4,13],push_notif:[12,22],order_bi:[21,17],too:4,created_object:17,conveni:[17,9,18],store:[19,4,25,24],schema:[19,20,2,14,24,17],shell:18,social_auth_suffix:23,option:[12,22],relationship:8,tool:18,copi:17,specifi:[15,17,18],nodejs_v0_4:25,custom_respons:[12,22],exactli:19,than:[17,9],kind:18,target:14,keyword:9,provid:[19,9,23,17,18,27],remov:[19,8],posibl:17,schedule_id:0,structur:[19,4],scheduletrac:0,related_field_lookup:14,were:19,listen:13,production_certificate_nam:4,latitud:7,class_dep_test:19,owner_permiss:[20,26],scriptendpointmanag:17,predefined_class_nam:26,arg:[10,11,12,14,23,24,17,13,27],queryset:17,argument:[19,17,9,27],packag:18,separate_room:13,another_custom_script:19,have:[15,4,9,18],deliv:4,need:[19,9,18],user_info_suffix:23,django:17,built:1,equival:[17,9],get_endpoint_query_param:16,"0x1d37a90":15,rout:19,gte:17,note:4,also:[19,4],exampl:[19,9,15,4,17,18],take:[19,9],which:[19,1,9,23,24,15,4,25,17,18,13],variable_two:25,referencefield:14,singl:8,expens:9,method_nam:23,simplifi:[9,18],http_method_nam:16,"_ssl":18,scriptcal:[19,24],though:[9,18],usernam:[17,12,23,26],custom_socket_nam:10,registration_id:4,remove_order_index:17,most:18,param_nam:14,pair:17,modelfield:[6,14,3],"class":[19,1,9,22,23,26,6,12,13],role_choic:[26,3],other:[15,19,9,18],don:9,create_subclass:20,doc:[15,13],clear:17,hyperlinkedfield:[20,0,21,3,25,26,6],request:[19,9,14,23,24,17,18,13],interval_sec:25,rais:[1,9,14,23,4,18],dummi:9,invitation_kei:23,runtim:[19,25,24],wildcard:19,page_s:[21,17],query_allow:14,latest:18,gcm_messag:[4,3],fact:9,lookup_separ:17,class_nam:[20,21,2,24,25,26,17],datefield:[6,14],text:[15,27,14,17,11],build_url:23,my_script_3:19,model_nam:14,custom_socket:19,session:23,class_inst:[19,24],plase:19,status_info:[19,10],fine:17,find:9,has_data:14,involv:9,messagedoesnotexist:13,onli:[19,4,17,9],explicitli:9,execut:[15,19,25,9],ieq:[14,17],configur:[9,18],activ:[9,18],behind:9,executed_at:0,should:[19,8,17],"8cc61a20608a":4,dict:[15,17,1,23],batch_uri:17,get_field:16,userdoesnotexist:26,username_b:17,username_a:17,response_templ:21,meant:17,startswith:[14,17],ani:[15,19,9,18],hit:9,juggl:17,patter:9,get:19,familiar:[8,18],between:9,stop:13,account:[23,12,18,22],schemafil:17,get_model_pattern:2,made:4,development_certificate_nam:4,emailfield:[14,26,3],"import":[19,9,18],script_or_script_endpoint:24,group_detail:26,requir:[19,4,14,9],resend:3,example_data_two:4,jsontopythonmixin:14,aa59:4,scripttrac:[15,0],scriptendpointtrac:0,linkswrapp:14,neq:17,install_from_url:[19,10],"public":25,new_nam:[25,21],to_dict:24,contain:[14,17],add_cal:[19,24],act:9,apnsmessagedoesnotexist:4,where:[17,18],user:[19,11,12,23,3,15,4,26,17],certif:[4,12,18,23],messagebas:4,underscore_to_camelcas:27,allowed_method:10,get_endpoint:[19,16,10],model_inst:14,see:[15,19],result:[19,4,0,9],respons:[15,5,17,23],fail:[19,18],close:4,nabuchodonozor:17,statu:[19,20,0,10,11,4],add_us:26,scriptendpointdoesnotexist:25,create_error_class:1,pattern:[17,9],someth:23,syncano_apiroot:9,label:[4,25,26],state:[23,3],won:9,pagin:17,authent:23,simplest:9,get_allowed_method:17,alt_login_param:23,reset_link:25,is_alt_login:23,attribut:[15,20,9,24,23],check:[19,4,23,9,18],get_path_properti:16,disadvantag:19,trace:[15,25,12,22],isol:18,get_endpoint_data:1,lazi:8,mark_for_batch:1,new_endpoint:19,otherwis:[15,19],get_subclass_model:20,schemamanag:17,swift:25,socketendpoint:[19,10],popul:[20,17],"_in_kilomet":7,last:[9,18],add_field:16,admin:[12,9,26,3],get_endpoint_path:16,equal:[15,19,17,9],new_depend:19,etc:19,to_dependency_data:24,in_bulk:17,list_us:26,context:[25,9],mani:17,login:12,sre_pattern:14,my_endpoint:19,load:[15,9],dependency_nam:[19,24],instanceinvitationdoesnotexist:3,instanti:9,schedul:[4,25,3],certificate_verify_fail:18,slugfield:[25,14],walk:[9,18],triggertracedoesnotexist:0,param:[4,5,23,16],your_email:[9,18],exclud:17,apikeydoesnotexist:3,mistak:19,partially_deliv:4,gcmdevicedoesnotexist:4,assum:[9,18],last_id:13,identif:19,basedepend:24,user_id:[4,26],patch:19,get_dependency_data:24,three:17,empti:17,get_model_by_nam:2,json:[19,8],trigger:[25,3],set_default_inst:2,basic:23,get_or_create_subclass:20,addit:9,valueerror:11,allowed_lookup:17,overwrite_handl:15,strings_onli:27,aad17f86d41483db7088ad2549ccb87902d60e45:18,id__gt:17,script_endpoint:[19,3],get_endpoint_method:16,apns_messag:[4,3],convert:[1,27],is_new:[4,1],floatfield:[6,14],ignored_link:14,raw_data:24,endpoints_data:24,choicefield:[20,0,14,3,25,26,6,13],custom_handl:15,"case":[9,11],default_connect:2,get_schema:2,look:[17,9,10],get_or_cr:17,properti:[14,1,9,16],might:18,servic:18,bill:[12,22],kwarg:[20,0,21,1,10,17,14,23,24,3,4,25,5,26,6,12,13],batch:17,call_typ:24,durat:[6,0],defin:[19,20,1,9,10,24,15],invok:9,apikei:[18,9,3],usernotfound:11,abov:[19,17,9],error:[19,11,15,4,17,18,13,27],build_properti:16,"4dea":4,geopoint:[7,14],virtualenvwrapp:18,helper:15,stdout:15,to_queri:14,some_class:17,pip:18,endswith:[14,17],dependencies_data:24,syncano_loglevel:9,revis:[20,11],force_text:27,attach:[19,25,9],around:[20,10,23,3,25,5,26,6,17,13],user_alt_login_param:23,subscrib:13,endpointmetadatamixin:[24,10],pushjsonfield:14,develop:19,new_upd:17,with_count:17,author:13,links_dict:14,remove_from_group:26,has_endpoint_data:14,clear_used_inst:2,hyperlistringfieldnkedfield:25,instanc:[19,20,1,9,10,22,14,23,25,26,6,12,18,13],hostingdoesnotexist:5,failur:0,document:[4,10],scriptendpoint:[15,19,25,17],not_indexable_typ:14,nin:17,nest:9,scriptdepend:[19,24],renamemixin:[25,3],alert:4,moment:19,get_config:3,auth:[23,26],responsetempl:25,php:25,stack:9,expand:21,recent:18,subpackag:8,is_default:5,appropri:24,hisotri:13,groupdoesnotexist:26,com:[9,18],thu:[17,9],miss:19,well:18,except:[12,9,18],object:[19,8],arrayfield:14,some_a:17,client:11,command:18,resolve_parent_data:16,thi:[19,20,1,9,15,17,18],choos:19,model:[19,8,18],self:[4,0,14],ccc:4,usual:17,scriptendpointtracedoesnotexist:0,allow_incr:14,construct:9,responsetemplatedoesnotexist:25,socket_endpoint_list:19,just:[17,9],less:17,excel:18,obtain:18,rest:9,detail:[19,0,9,10,4,17],field_nam:[16,11],build_param:23,python_library_v4:25,python_library_v5:[19,25],contet:4,method_list:24,add_endpoint:[19,24],syncanoexcept:11,my_script:19,password:[9,12,23,26,17,18],display_nam:[20,0,21,3,25,26,6,13],field:[19,20,11,22,24,15,12,18],get_user_info:23,cert_fil:4,development_api_kei:4,shortcut:17,icontain:[14,17],instanceinvit:3,add:[19,4,2,17,18],admindoesnotexist:[26,3],login_param:23,action_choic:13,sslerror:18,role:[26,3],logger:23,save:[19,8],match:[19,17,9,11],bin:18,applic:[15,19,23],production_certif:4,ignore_acl:3,validate_param:23,handl:[4,17,11],regard:9,apnsconfigdoesnotexist:4,distanc:7,api_vers:19,your_api_kei:18,know:[9,18],guid:8,revisionmismatchexcept:11,httprespons:15,you:[19,8,1,9,18],desc:17,data_view:[12,22],klass:17,to_python:[14,1],success:0,realtim:13,signal:25,signal_choic:25,integ:[14,24],server:[4,17],collect:24,is_soci:23,"boolean":[17,14,12,23],necessari:17,avatar:17,get_account_info:23,group_id:26,page:[8,17],parse_d:14,incent:[12,22],set_default_connect:2,scheduledoesnotexist:25,allow_anonymous_read:3,update_endpoint:24,creation:19,some:[19,4,17,9],back:8,resolv:9,intern:[19,17],previou:9,sampl:17,classdoesnotexist:20,proper:[9,23],user_kei:[12,23,26],gcmmessagedoesnotexist:4,librari:19,field_lookup:14,virtualenv:18,kei:[12,23,3,4,17,18],register_suffix:23,poll:13,integerfield:[20,0,21,14,25,6,13],lookup_typ:14,channel:[12,26,22],definit:[20,16],"0x1b745f0":14,tutori:[9,18],python_v4_2:25,old_upd:17,inject:23,syncano_password:9,foo:9,parse_from_d:14,core:4,dataobjectmixin:[20,26],my_custom_socket_3:19,quit:17,relationvalidatormixin:14,someclass:17,stringfield:[20,0,21,10,14,3,25,26,6,13],"enum":14,usag:[8,12,14,4,25,17,18],get_class_nam:27,discount:6,step:19,gcm_devic:[4,3],post:[19,9],ddd:4,raptor:17,about:[17,24],set_used_inst:2,scriptdoesnotexist:25,get_model_by_path:2,add_object:21,http:[19,9,11,14,23,13],script_endpoint_nam:[19,0],manag:[8,18],contribute_to_class:[14,17,16],done:18,instance_a:17,to_n:[7,14,1],dummy_test:18,custom_publish:13,disabl:[17,9],classdepend:[19,24],status_cod:[15,11],filefield:14,devic:4,"float":14,encod:27,bound:19,update_fil:5,basecal:24,objectfield:14,bool:17,your:[19,23,12,9,18],channel_room:[20,26],git:18,span:8,development_bundle_identifi:4,wai:[19,17,9,18],aren:9,support:[19,9,23,24,15,4,17],transform:24,"long":[9,13],like:[19,4,17,9,18],strict:27,appl:4,sfdsdfsdf:12,lot:18,resolve_endpoint:16,update_or_cr:17,apnsdevicedoesnotexist:4,"function":[9,18],nodejs_library_v1:25,nodejs_library_v0:25,get_class_schema:20,code_box_trace_id:15,tupl:17,recommend:9,custom_script:19,user_login_param:23,cloud:4,link:[20,0,21,10,3,4,25,5,26,6,13],apnsconfig:4,type_choic:13,renam:[25,21],line:9,to_endpoint_data:24,"true":[4,14,17,13],count:17,immedi:9,utf:27,updated_valu:17,reload:[19,1],whether:17,data_endpoint:3,access:8,maximum:4,send_messag:4,record:9,batch_object:1,http_method:16,limit:8,istartswith:[14,17],camelcas:27,"export":9,jsonfield:[20,10,14,3,25,13],booleanfield:[14,13,3],expect:[17,11],creation_count:14,remove_index:17,evalu:9,archetyp:[12,26,22],certain:9,instancedoesnotexist:3,channeldoesnotexist:13,doesn:[9,11],"0x1c63030":14,golang:25,production_bundle_identifi:4,control:1,profiledoesnotexist:26,exist:[19,11,14,4,5,17],triggertrac:0,file:[19,14,5],hostingfiledoesnotexist:5,public_link:25,again:19,googl:4,titl:17,when:[15,19,17,18],is_http_method_avail:16,invalid:[18,23],dataendpoint:21,"default":[9,14,23,17,18,13],gcmmessag:4,valid:[4,14,1,23,11],dataendpointdoesnotexist:21,authenticate_admin:23,test:[19,4,17,9,18],set_default:5,add_to_class:1,add_to_group:26,as_batch:17,variabl:8,model_us:17,notif:4,connectionmixin:[16,17,23],registri:[12,22],parse_from_str:14,create_object:20,scriptmanag:17,rubi:25,"_in_mil":7,json_handl:15,variable_on:25,docstr:1,incrementmixin:17,push:4,slice:9,queri:[21,14,9,18],consid:15,status_choic:0,longitud:7,listfield:14,bulk:17,directori:18,descript:[20,21,9,10,3,25,5,26,17,18],rule:19,socketendpointdoesnotexist:10,get_endpoint_properti:16,customresponsehandl:15,time:19,"abstract":4,list_fil:5,backward:8,hello:17,is_valid:1},objtypes:{"0":"py:module","1":"py:attribute","2":"py:method","3":"py:class","4":"py:function","5":"py:staticmethod","6":"py:classmethod","7":"py:exception"},objnames:{"0":["py","module","Python module"],"1":["py","attribute","Python attribute"],"2":["py","method","Python method"],"3":["py","class","Python class"],"4":["py","function","Python function"],"5":["py","staticmethod","Python static method"],"6":["py","classmethod","Python class method"],"7":["py","exception","Python exception"]},filenames:["refs/syncano.models.traces","refs/syncano.models.archetypes","refs/syncano.models.registry","refs/syncano.models.instances","refs/syncano.models.push_notification","refs/syncano.models.hosting","refs/syncano.models.billing","refs/syncano.models.geo","index","interacting","refs/syncano.models.custom_sockets","refs/syncano.exceptions","refs/syncano","refs/syncano.models.channels","refs/syncano.models.fields","refs/syncano.models.custom_response","refs/syncano.models.options","refs/syncano.models.manager","getting_started","custom_sockets","refs/syncano.models.classes","refs/syncano.models.data_views","refs/syncano.models","refs/syncano.connection","refs/syncano.models.custom_sockets_utils","refs/syncano.models.incentives","refs/syncano.models.accounts","refs/syncano.utils"],titles:["syncano.models.traces","syncano.models.archetypes","syncano.models.registry","syncano.models.instances","syncano.models.push_notification","syncano.models.hosting","syncano.models.billing","syncano.models.geo","Syncano: A Python interface to Syncano services","Interacting with Syncano","syncano.models.custom_sockets","syncano.exceptions","Syncano references","syncano.models.channels","syncano.models.fields","syncano.models.custom_response","syncano.models.options","syncano.models.manager","Getting Started with Syncano","Custom Sockets in Syncano","syncano.models.classes","syncano.models.data_views","syncano.models","syncano.connection","syncano.models.custom_sockets_utils","syncano.models.incentives","syncano.models.accounts","syncano.utils module"],objects:{"":{syncano:[12,0,0,"-"]},"syncano.models.push_notification.GCMDevice":{registration_id:[4,1,1,""],links:[4,1,1,""],LINKS:[4,1,1,""],is_active:[4,1,1,""],created_at:[4,1,1,""],please:[4,1,1,""],updated_at:[4,1,1,""],label:[4,1,1,""],instance_name:[4,1,1,""],user:[4,1,1,""],pk:[4,1,1,""],DoesNotExist:[4,1,1,""],device_id:[4,1,1,""]},"syncano.models.registry.Registry":{set_schema:[2,2,1,""],get_schema:[2,2,1,""],set_default_property:[2,2,1,""],get_model_by_name:[2,2,1,""],get_model_by_path:[2,2,1,""],update:[2,2,1,""],connection:[2,1,1,""],add:[2,2,1,""],set_used_instance:[2,2,1,""],clear_used_instance:[2,2,1,""],clear_schemas:[2,2,1,""],set_default_instance:[2,2,1,""],get_model_patterns:[2,2,1,""],set_default_connection:[2,2,1,""]},"syncano.models.fields.IntegerField":{to_python:[14,2,1,""],allow_increment:[14,1,1,""]},"syncano.models.fields.LinksWrapper":{to_native:[14,2,1,""]},"syncano.models.instances":{Instance:[3,3,1,""],ApiKey:[3,3,1,""],InstanceInvitation:[3,3,1,""]},"syncano.models.fields.ModelField":{contribute_to_class:[14,2,1,""],validate:[14,2,1,""],to_python:[14,2,1,""],to_native:[14,2,1,""]},"syncano.models.push_notification.DeviceBase.Meta":{"abstract":[4,1,1,""]},"syncano.models.geo.GeoPoint":{to_native:[7,2,1,""]},"syncano.models.custom_sockets_utils.BaseDependency":{dependency_type:[24,1,1,""],name:[24,1,1,""],create_from_raw_data:[24,2,1,""],fields:[24,1,1,""],get_dependency_data:[24,2,1,""],get_name:[24,2,1,""],to_dependency_data:[24,2,1,""]},"syncano.models.fields.ListField":{validate:[14,2,1,""]},"syncano.models.classes.Class":{status:[20,1,1,""],expected_revision:[20,1,1,""],objects_count:[20,1,1,""],save:[20,2,1,""],group:[20,1,1,""],revision:[20,1,1,""],links:[20,1,1,""],group_permissions:[20,1,1,""],created_at:[20,1,1,""],description:[20,1,1,""],please:[20,1,1,""],updated_at:[20,1,1,""],name:[20,1,1,""],instance_name:[20,1,1,""],metadata:[20,1,1,""],objects:[20,1,1,""],pk:[20,1,1,""],PERMISSIONS_CHOICES:[20,1,1,""],DoesNotExist:[20,1,1,""],other_permissions:[20,1,1,""],schema:[20,1,1,""]},"syncano.models.custom_response":{CustomResponseMixin:[15,3,1,""],CustomResponseHandler:[15,3,1,""]},"syncano.models.push_notification.APNSConfig":{development_certificate:[4,1,1,""],development_expiration_date:[4,1,1,""],production_certificate:[4,1,1,""],production_expiration_date:[4,1,1,""],production_bundle_identifier:[4,1,1,""],please:[4,1,1,""],is_new:[4,2,1,""],production_certificate_name:[4,1,1,""],instance_name:[4,1,1,""],development_certificate_name:[4,1,1,""],development_bundle_identifier:[4,1,1,""],pk:[4,1,1,""],DoesNotExist:[4,1,1,""],id:[4,1,1,""]},"syncano.models.custom_sockets_utils.ScriptCall":{call_type:[24,1,1,""]},"syncano.models.hosting":{HostingFile:[5,3,1,""],Hosting:[5,3,1,""]},"syncano.models.push_notification.GCMConfig":{please:[4,1,1,""],is_new:[4,2,1,""],id:[4,1,1,""],instance_name:[4,1,1,""],pk:[4,1,1,""],production_api_key:[4,1,1,""],DoesNotExist:[4,1,1,""],development_api_key:[4,1,1,""]},"syncano.models.fields.ArrayField":{validate:[14,2,1,""]},"syncano.models.classes":{Object:[20,3,1,""],Class:[20,3,1,""],DataObjectMixin:[20,3,1,""]},"syncano.models.traces":{ScriptTrace:[0,3,1,""],ScriptEndpointTrace:[0,3,1,""],TriggerTrace:[0,3,1,""],ScheduleTrace:[0,3,1,""]},"syncano.models.instances.ApiKey":{api_key:[3,1,1,""],description:[3,1,1,""],links:[3,1,1,""],allow_anonymous_read:[3,1,1,""],please:[3,1,1,""],ignore_acl:[3,1,1,""],instance_name:[3,1,1,""],pk:[3,1,1,""],DoesNotExist:[3,1,1,""],id:[3,1,1,""],allow_user_create:[3,1,1,""]},"syncano.models.push_notification.DeviceBase":{registration_id:[4,1,1,""],LINKS:[4,1,1,""],links:[4,1,1,""],created_at:[4,1,1,""],is_active:[4,1,1,""],updated_at:[4,1,1,""],label:[4,1,1,""],send_message:[4,2,1,""],Meta:[4,3,1,""],user:[4,1,1,""],device_id:[4,1,1,""]},"syncano.models.fields.LinksField":{query_allowed:[14,1,1,""],IGNORED_LINKS:[14,1,1,""],to_native:[14,2,1,""],to_python:[14,2,1,""]},"syncano.models.fields.ObjectField":{validate:[14,2,1,""]},"syncano.models.custom_sockets_utils.DependencyMetadataMixin":{add_dependency:[24,2,1,""],dependencies_data:[24,1,1,""],remove_dependency:[24,2,1,""],update_dependencies:[24,2,1,""]},"syncano.exceptions":{SyncanoException:[11,7,1,""],SyncanoDoesNotExist:[11,7,1,""],RevisionMismatchException:[11,7,1,""],SyncanoFieldError:[11,7,1,""],SyncanoRequestError:[11,7,1,""],UserNotFound:[11,7,1,""],SyncanoValidationError:[11,7,1,""],SyncanoValueError:[11,7,1,""]},"syncano.models.traces.ScheduleTrace":{status:[0,1,1,""],links:[0,1,1,""],please:[0,1,1,""],executed_at:[0,1,1,""],instance_name:[0,1,1,""],schedule_id:[0,1,1,""],result:[0,1,1,""],STATUS_CHOICES:[0,1,1,""],duration:[0,1,1,""],pk:[0,1,1,""],DoesNotExist:[0,1,1,""],id:[0,1,1,""]},"syncano.connection.Connection":{USER_LOGIN_PARAMS:[23,1,1,""],authenticate_user:[23,2,1,""],USER_INFO_SUFFIX:[23,1,1,""],is_authenticated:[23,2,1,""],build_url:[23,2,1,""],USER_ALT_LOGIN_PARAMS:[23,1,1,""],make_request:[23,2,1,""],is_social:[23,1,1,""],REGISTER_SUFFIX:[23,1,1,""],SOCIAL_LOGIN_PARAMS:[23,1,1,""],validate_params:[23,2,1,""],build_params:[23,2,1,""],get_user_info:[23,2,1,""],AUTH_SUFFIX:[23,1,1,""],authenticate_admin:[23,2,1,""],SOCIAL_AUTH_SUFFIX:[23,1,1,""],is_alt_login:[23,1,1,""],authenticate:[23,2,1,""],get_response_content:[23,2,1,""],is_user:[23,1,1,""],auth_key:[23,1,1,""],USER_AUTH_SUFFIX:[23,1,1,""],ALT_LOGIN_PARAMS:[23,1,1,""],register:[23,2,1,""],request:[23,2,1,""],LOGIN_PARAMS:[23,1,1,""],CONTENT_TYPE:[23,1,1,""],get_account_info:[23,2,1,""],ACCOUNT_SUFFIX:[23,1,1,""]},"syncano.models.fields.EndpointField":{has_endpoint_data:[14,1,1,""],has_data:[14,1,1,""]},"syncano.models.incentives.Schedule":{links:[25,1,1,""],script:[25,1,1,""],scheduled_next:[25,1,1,""],created_at:[25,1,1,""],please:[25,1,1,""],interval_sec:[25,1,1,""],crontab:[25,1,1,""],label:[25,1,1,""],instance_name:[25,1,1,""],pk:[25,1,1,""],id:[25,1,1,""],timezone:[25,1,1,""],DoesNotExist:[25,1,1,""],traces:[25,1,1,""],payload:[25,1,1,""]},"syncano.models.channels.PollThread":{request:[13,2,1,""],run:[13,2,1,""],stop:[13,2,1,""]},"syncano.models.push_notification.MessageBase":{status:[4,1,1,""],created_at:[4,1,1,""],updated_at:[4,1,1,""],content:[4,1,1,""],Meta:[4,3,1,""],result:[4,1,1,""]},"syncano.models.fields.RelationField":{query_allowed:[14,1,1,""],field_lookups:[14,1,1,""],to_query:[14,2,1,""],to_python:[14,2,1,""],to_native:[14,2,1,""]},"syncano.models.billing.Discount":{end:[6,1,1,""],links:[6,1,1,""],start:[6,1,1,""],coupon:[6,1,1,""],please:[6,1,1,""],instance:[6,1,1,""],pk:[6,1,1,""],DoesNotExist:[6,1,1,""],id:[6,1,1,""]},"syncano.models.archetypes.ModelMetaclass":{create_error_class:[1,2,1,""],build_doc:[1,2,1,""],add_to_class:[1,2,1,""]},"syncano.models.custom_sockets_utils.CallType":{SCRIPT:[24,1,1,""]},"syncano.models.classes.DataObjectMixin":{get_class_object:[20,6,1,""]},"syncano.models.fields.JSONToPythonMixin":{to_python:[14,2,1,""]},"syncano.models.geo":{Distance:[7,3,1,""],GeoPoint:[7,3,1,""]},"syncano.models.fields.RelatedManagerField":{contribute_to_class:[14,2,1,""]},"syncano.models.incentives.ResponseTemplate":{rename:[25,2,1,""],content:[25,1,1,""],content_type:[25,1,1,""],links:[25,1,1,""],created_at:[25,1,1,""],please:[25,1,1,""],updated_at:[25,1,1,""],instance_name:[25,1,1,""],render:[25,2,1,""],context:[25,1,1,""],pk:[25,1,1,""],DoesNotExist:[25,1,1,""],id:[25,1,1,""],name:[25,1,1,""]},"syncano.models.fields.JSONField":{query_allowed:[14,1,1,""],validate:[14,2,1,""],to_native:[14,2,1,""],schema:[14,1,1,""]},"syncano.models.manager.ObjectManager":{count:[17,2,1,""],order_by:[17,2,1,""],with_count:[17,2,1,""],ordering:[17,2,1,""],fields:[17,2,1,""],serialize:[17,2,1,""],ALLOWED_LOOKUPS:[17,1,1,""],filter:[17,2,1,""],LOOKUP_SEPARATOR:[17,1,1,""],bulk_create:[17,2,1,""],exclude:[17,2,1,""]},"syncano.models.custom_sockets_utils.EndpointMetadataMixin":{add_endpoint:[24,2,1,""],remove_endpoint:[24,2,1,""],update_endpoints:[24,2,1,""],endpoints_data:[24,1,1,""]},"syncano.models.incentives.Trigger":{signal:[25,1,1,""],links:[25,1,1,""],script:[25,1,1,""],class_name:[25,1,1,""],created_at:[25,1,1,""],please:[25,1,1,""],updated_at:[25,1,1,""],SIGNAL_CHOICES:[25,1,1,""],instance_name:[25,1,1,""],label:[25,1,1,""],pk:[25,1,1,""],DoesNotExist:[25,1,1,""],traces:[25,1,1,""],id:[25,1,1,""]},"syncano.models.fields.ReferenceField":{to_python:[14,2,1,""]},"syncano.models.billing":{Coupon:[6,3,1,""],Discount:[6,3,1,""]},"syncano.models.fields.StringField":{field_lookups:[14,1,1,""],to_python:[14,2,1,""]},"syncano.models.registry":{Registry:[2,3,1,""]},"syncano.models.custom_sockets_utils.DependencyType":{CLASS:[24,1,1,""],SCRIPT:[24,1,1,""]},"syncano.models.manager.ScriptEndpointManager":{run:[17,2,1,""]},"syncano.models.push_notification.APNSDevice":{registration_id:[4,1,1,""],LINKS:[4,1,1,""],links:[4,1,1,""],please:[4,1,1,""],created_at:[4,1,1,""],is_active:[4,1,1,""],updated_at:[4,1,1,""],label:[4,1,1,""],instance_name:[4,1,1,""],user:[4,1,1,""],pk:[4,1,1,""],DoesNotExist:[4,1,1,""],device_id:[4,1,1,""]},"syncano.models.fields.FileField":{param_name:[14,1,1,""],to_native:[14,2,1,""]},"syncano.models.custom_sockets.SocketEndpoint":{allowed_methods:[10,1,1,""],get_all_endpoints:[10,6,1,""],links:[10,1,1,""],please:[10,1,1,""],name:[10,1,1,""],instance_name:[10,1,1,""],run:[10,2,1,""],pk:[10,1,1,""],DoesNotExist:[10,1,1,""],custom_socket_name:[10,1,1,""]},"syncano.models.custom_sockets_utils.ClassDependency":{dependency_type:[24,1,1,""],get_dependency_data:[24,2,1,""],fields:[24,1,1,""],create_from_raw_data:[24,6,1,""]},"syncano.models.fields.BooleanField":{to_python:[14,2,1,""]},"syncano.models.push_notification.GCMMessage":{status:[4,1,1,""],instance_name:[4,1,1,""],created_at:[4,1,1,""],please:[4,1,1,""],updated_at:[4,1,1,""],content:[4,1,1,""],result:[4,1,1,""],pk:[4,1,1,""],DoesNotExist:[4,1,1,""],id:[4,1,1,""]},"syncano.models.fields.SlugField":{regex:[14,1,1,""],validate:[14,2,1,""]},"syncano.models.fields.EmailField":{regex:[14,1,1,""],validate:[14,2,1,""]},"syncano.models.incentives.ScriptEndpoint":{run:[25,2,1,""],name:[25,1,1,""],links:[25,1,1,""],script:[25,1,1,""],reset_link:[25,2,1,""],please:[25,1,1,""],"public":[25,1,1,""],instance_name:[25,1,1,""],pk:[25,1,1,""],DoesNotExist:[25,1,1,""],traces:[25,1,1,""],public_link:[25,1,1,""]},"syncano.models.options":{Options:[16,3,1,""]},"syncano.connection":{Connection:[23,3,1,""],ConnectionMixin:[23,3,1,""]},"syncano.models.classes.Object":{class_name:[20,1,1,""],updated_at:[20,1,1,""],owner:[20,1,1,""],PERMISSIONS_CHOICES:[20,1,1,""],DoesNotExist:[20,1,1,""],id:[20,1,1,""],channel_room:[20,1,1,""],group:[20,1,1,""],please:[20,1,1,""],pk:[20,1,1,""],get_or_create_subclass:[20,6,1,""],channel:[20,1,1,""],revision:[20,1,1,""],get_subclass_name:[20,6,1,""],get_class_schema:[20,6,1,""],group_permissions:[20,1,1,""],get_subclass_model:[20,6,1,""],create_subclass:[20,6,1,""],other_permissions:[20,1,1,""],owner_permissions:[20,1,1,""],instance_name:[20,1,1,""],created_at:[20,1,1,""]},"syncano.models.instances.InstanceInvitation.Admin":{first_name:[3,1,1,""],last_name:[3,1,1,""],links:[3,1,1,""],please:[3,1,1,""],email:[3,1,1,""],instance_name:[3,1,1,""],ROLE_CHOICES:[3,1,1,""],role:[3,1,1,""],pk:[3,1,1,""],DoesNotExist:[3,1,1,""],id:[3,1,1,""]},"syncano.models.fields.Field":{read_only:[14,1,1,""],primary_key:[14,1,1,""],to_python:[14,2,1,""],creation_counter:[14,1,1,""],field_lookups:[14,1,1,""],"default":[14,1,1,""],required:[14,1,1,""],has_endpoint_data:[14,1,1,""],query_allowed:[14,1,1,""],contribute_to_class:[14,2,1,""],to_query:[14,2,1,""],blank:[14,1,1,""],allow_increment:[14,1,1,""],to_native:[14,2,1,""],validate:[14,2,1,""],has_data:[14,1,1,""]},"syncano.models.fields.ChoiceField":{validate:[14,2,1,""]},syncano:{exceptions:[11,0,0,"-"],models:[22,0,0,"-"],connection:[23,0,0,"-"],utils:[27,0,0,"-"],connect:[12,4,1,""]},"syncano.models.custom_response.CustomResponseMixin":{content:[15,1,1,""],status_code:[15,1,1,""],response_handler:[15,1,1,""],content_type:[15,1,1,""],error:[15,1,1,""]},"syncano.exceptions.SyncanoFieldError":{field_name:[11,1,1,""]},"syncano.models.channels.Channel":{poll:[13,2,1,""],group:[13,1,1,""],name:[13,1,1,""],links:[13,1,1,""],TYPE_CHOICES:[13,1,1,""],other_permissions:[13,1,1,""],please:[13,1,1,""],publish:[13,2,1,""],instance_name:[13,1,1,""],DoesNotExist:[13,1,1,""],group_permissions:[13,1,1,""],pk:[13,1,1,""],PERMISSIONS_CHOICES:[13,1,1,""],custom_publish:[13,1,1,""],type:[13,1,1,""]},"syncano.models.fields.DateTimeField":{parse_from_string:[14,2,1,""],parse_from_date:[14,2,1,""],to_python:[14,2,1,""],to_native:[14,2,1,""],FORMAT:[14,1,1,""]},"syncano.models.fields.PrimaryKeyField":{primary_key:[14,1,1,""]},"syncano.models.traces.ScriptTrace":{status:[0,1,1,""],links:[0,1,1,""],please:[0,1,1,""],executed_at:[0,1,1,""],instance_name:[0,1,1,""],result:[0,1,1,""],pk:[0,1,1,""],duration:[0,1,1,""],STATUS_CHOICES:[0,1,1,""],script_id:[0,1,1,""],DoesNotExist:[0,1,1,""],id:[0,1,1,""]},"syncano.models.channels.Message":{ACTION_CHOICES:[13,1,1,""],room:[13,1,1,""],author:[13,1,1,""],created_at:[13,1,1,""],please:[13,1,1,""],payload:[13,1,1,""],instance_name:[13,1,1,""],channel_name:[13,1,1,""],action:[13,1,1,""],pk:[13,1,1,""],DoesNotExist:[13,1,1,""],id:[13,1,1,""],metadata:[13,1,1,""]},"syncano.models.manager.SchemaManager":{set:[17,2,1,""],remove_index:[17,2,1,""],set_index:[17,2,1,""],clear:[17,2,1,""],remove_order_index:[17,2,1,""],add:[17,2,1,""],set_filter_index:[17,2,1,""],remove:[17,2,1,""],set_order_index:[17,2,1,""],remove_filter_index:[17,2,1,""]},"syncano.models":{custom_sockets:[10,0,0,"-"],traces:[0,0,0,"-"],billing:[6,0,0,"-"],archetypes:[1,0,0,"-"],data_views:[21,0,0,"-"],geo:[7,0,0,"-"],incentives:[25,0,0,"-"],hosting:[5,0,0,"-"],push_notification:[4,0,0,"-"],channels:[13,0,0,"-"],instances:[3,0,0,"-"],manager:[17,0,0,"-"],classes:[20,0,0,"-"],accounts:[26,0,0,"-"],registry:[2,0,0,"-"],custom_response:[15,0,0,"-"],fields:[14,0,0,"-"],custom_sockets_utils:[24,0,0,"-"],options:[16,0,0,"-"]},"syncano.models.fields.DateField":{date_regex:[14,1,1,""],re:[14,1,1,""],to_python:[14,2,1,""],parse_date:[14,2,1,""],to_native:[14,2,1,""]},"syncano.models.incentives":{RuntimeChoices:[25,3,1,""],Script:[25,3,1,""],Schedule:[25,3,1,""],Trigger:[25,3,1,""],ScriptEndpoint:[25,3,1,""],ResponseTemplate:[25,3,1,""]},"syncano.models.custom_response.CustomResponseHandler":{plain_handler:[15,5,1,""],overwrite_handler:[15,2,1,""],register_handler:[15,2,1,""],process_response:[15,2,1,""],json_handler:[15,5,1,""]},"syncano.models.incentives.Script":{run:[25,2,1,""],description:[25,1,1,""],links:[25,1,1,""],config:[25,1,1,""],created_at:[25,1,1,""],please:[25,1,1,""],updated_at:[25,1,1,""],label:[25,1,1,""],instance_name:[25,1,1,""],source:[25,1,1,""],runtime_name:[25,1,1,""],pk:[25,1,1,""],DoesNotExist:[25,1,1,""],traces:[25,1,1,""],id:[25,1,1,""]},"syncano.models.geo.Distance":{MILES:[7,1,1,""],to_native:[7,2,1,""],KILOMETERS:[7,1,1,""]},"syncano.models.instances.Instance":{templates:[3,1,1,""],links:[3,1,1,""],schedules:[3,1,1,""],updated_at:[3,1,1,""],owner:[3,1,1,""],DoesNotExist:[3,1,1,""],users:[3,1,1,""],gcm_messages:[3,1,1,""],please:[3,1,1,""],role:[3,1,1,""],apns_messages:[3,1,1,""],pk:[3,1,1,""],metadata:[3,1,1,""],gcm_devices:[3,1,1,""],hostings:[3,1,1,""],api_keys:[3,1,1,""],data_endpoints:[3,1,1,""],apns_devices:[3,1,1,""],get_config:[3,2,1,""],groups:[3,1,1,""],scripts:[3,1,1,""],invitations:[3,1,1,""],name:[3,1,1,""],triggers:[3,1,1,""],created_at:[3,1,1,""],description:[3,1,1,""],set_config:[3,2,1,""],admins:[3,1,1,""],classes:[3,1,1,""],script_endpoints:[3,1,1,""]},"syncano.models.custom_sockets":{CustomSocket:[10,3,1,""],SocketEndpoint:[10,3,1,""]},"syncano.models.fields":{DateField:[14,3,1,""],PrimaryKeyField:[14,3,1,""],RelatedManagerField:[14,3,1,""],LinksField:[14,3,1,""],GeoPointField:[14,3,1,""],SlugField:[14,3,1,""],ModelField:[14,3,1,""],FloatField:[14,3,1,""],Field:[14,3,1,""],EndpointField:[14,3,1,""],EmailField:[14,3,1,""],PushJSONField:[14,3,1,""],JSONField:[14,3,1,""],SchemaField:[14,3,1,""],WritableField:[14,3,1,""],BooleanField:[14,3,1,""],JSONToPythonMixin:[14,3,1,""],RelationField:[14,3,1,""],IntegerField:[14,3,1,""],StringField:[14,3,1,""],DateTimeField:[14,3,1,""],ObjectField:[14,3,1,""],ChoiceField:[14,3,1,""],FileField:[14,3,1,""],ListField:[14,3,1,""],LinksWrapper:[14,3,1,""],ArrayField:[14,3,1,""],ReferenceField:[14,3,1,""]},"syncano.models.fields.FloatField":{to_python:[14,2,1,""],allow_increment:[14,1,1,""]},"syncano.models.fields.GeoPointField":{validate:[14,2,1,""],field_lookups:[14,1,1,""],to_query:[14,2,1,""],to_python:[14,2,1,""],to_native:[14,2,1,""]},"syncano.models.data_views":{DataEndpoint:[21,3,1,""]},"syncano.models.custom_sockets_utils.Endpoint":{to_endpoint_data:[24,2,1,""],add_call:[24,2,1,""]},"syncano.models.custom_sockets_utils.ScriptDependency":{dependency_type:[24,1,1,""],create_from_raw_data:[24,6,1,""],get_dependency_data:[24,2,1,""],fields:[24,1,1,""]},"syncano.models.push_notification.MessageBase.Meta":{"abstract":[4,1,1,""]},"syncano.models.manager":{ManagerDescriptor:[17,3,1,""],ObjectManager:[17,3,1,""],Manager:[17,3,1,""],ScriptEndpointManager:[17,3,1,""],SchemaManager:[17,3,1,""],ScriptManager:[17,3,1,""]},"syncano.models.archetypes.Model":{to_python:[1,2,1,""],to_native:[1,2,1,""],is_new:[1,2,1,""],get_endpoint_data:[1,2,1,""],reload:[1,2,1,""],batch_object:[1,6,1,""],is_valid:[1,2,1,""],mark_for_batch:[1,2,1,""],validate:[1,2,1,""],save:[1,2,1,""],"delete":[1,2,1,""]},"syncano.models.custom_sockets.CustomSocket":{status:[10,1,1,""],DoesNotExist:[10,1,1,""],run:[10,2,1,""],description:[10,1,1,""],links:[10,1,1,""],name:[10,1,1,""],created_at:[10,1,1,""],please:[10,1,1,""],updated_at:[10,1,1,""],install_from_url:[10,2,1,""],update:[10,2,1,""],instance_name:[10,1,1,""],dependencies:[10,1,1,""],get_endpoints:[10,2,1,""],install:[10,2,1,""],pk:[10,1,1,""],status_info:[10,1,1,""],endpoints:[10,1,1,""],config:[10,1,1,""],recheck:[10,2,1,""],metadata:[10,1,1,""]},"syncano.models.manager.Manager":{all:[17,2,1,""],iterator:[17,2,1,""],list:[17,2,1,""],serialize:[17,2,1,""],update_or_create:[17,2,1,""],raw:[17,2,1,""],ordering:[17,2,1,""],create:[17,2,1,""],old_update:[17,2,1,""],detail:[17,2,1,""],page_size:[17,2,1,""],in_bulk:[17,2,1,""],BATCH_URI:[17,1,1,""],bulk_create:[17,2,1,""],get_allowed_method:[17,2,1,""],template:[17,2,1,""],get:[17,2,1,""],new_update:[17,2,1,""],update:[17,2,1,""],using:[17,2,1,""],request:[17,2,1,""],batch:[17,2,1,""],filter:[17,2,1,""],contribute_to_class:[17,2,1,""],build_request:[17,2,1,""],limit:[17,2,1,""],as_batch:[17,2,1,""],get_or_create:[17,2,1,""],first:[17,2,1,""],"delete":[17,2,1,""]},"syncano.models.accounts.Admin":{first_name:[26,1,1,""],last_name:[26,1,1,""],links:[26,1,1,""],please:[26,1,1,""],id:[26,1,1,""],instance_name:[26,1,1,""],ROLE_CHOICES:[26,1,1,""],role:[26,1,1,""],pk:[26,1,1,""],DoesNotExist:[26,1,1,""],email:[26,1,1,""]},"syncano.models.traces.TriggerTrace":{status:[0,1,1,""],links:[0,1,1,""],LINKS:[0,1,1,""],please:[0,1,1,""],executed_at:[0,1,1,""],instance_name:[0,1,1,""],trigger_id:[0,1,1,""],pk:[0,1,1,""],duration:[0,1,1,""],STATUS_CHOICES:[0,1,1,""],DoesNotExist:[0,1,1,""],id:[0,1,1,""],result:[0,1,1,""]},"syncano.utils":{underscore_to_camelcase:[27,4,1,""],camelcase_to_underscore:[27,4,1,""],get_class_name:[27,4,1,""],force_text:[27,4,1,""]},"syncano.models.fields.SchemaField":{to_python:[14,2,1,""],to_native:[14,2,1,""],required:[14,1,1,""],query_allowed:[14,1,1,""],validate:[14,2,1,""],not_indexable_types:[14,1,1,""],schema:[14,1,1,""]},"syncano.models.push_notification.APNSMessage":{status:[4,1,1,""],content:[4,1,1,""],created_at:[4,1,1,""],please:[4,1,1,""],updated_at:[4,1,1,""],instance_name:[4,1,1,""],result:[4,1,1,""],pk:[4,1,1,""],DoesNotExist:[4,1,1,""],id:[4,1,1,""]},"syncano.models.hosting.HostingFile":{instance_name:[5,1,1,""],links:[5,1,1,""],please:[5,1,1,""],hosting_id:[5,1,1,""],file:[5,1,1,""],pk:[5,1,1,""],path:[5,1,1,""],DoesNotExist:[5,1,1,""],id:[5,1,1,""]},"syncano.models.accounts.Profile":{channel_room:[26,1,1,""],group:[26,1,1,""],links:[26,1,1,""],class_name:[26,1,1,""],group_permissions:[26,1,1,""],created_at:[26,1,1,""],please:[26,1,1,""],updated_at:[26,1,1,""],owner_permissions:[26,1,1,""],instance_name:[26,1,1,""],other_permissions:[26,1,1,""],id:[26,1,1,""],owner:[26,1,1,""],pk:[26,1,1,""],PERMISSIONS_CHOICES:[26,1,1,""],DoesNotExist:[26,1,1,""],PREDEFINED_CLASS_NAME:[26,1,1,""],channel:[26,1,1,""]},"syncano.models.custom_sockets_utils.BaseCall":{to_dict:[24,2,1,""],call_type:[24,1,1,""]},"syncano.models.channels":{PollThread:[13,3,1,""],Message:[13,3,1,""],Channel:[13,3,1,""]},"syncano.models.incentives.RuntimeChoices":{PYTHON_V5_0:[25,1,1,""],NODEJS:[25,1,1,""],PYTHON:[25,1,1,""],SWIFT:[25,1,1,""],PYTHON_V4_2:[25,1,1,""],GOLANG:[25,1,1,""],NODEJS_V0_4:[25,1,1,""],NODEJS_V1_0:[25,1,1,""],PHP:[25,1,1,""],RUBY:[25,1,1,""]},"syncano.models.archetypes":{Model:[1,3,1,""],ModelMetaclass:[1,3,1,""]},"syncano.models.accounts":{Admin:[26,3,1,""],Profile:[26,3,1,""],Group:[26,3,1,""],User:[26,3,1,""]},"syncano.models.push_notification":{GCMDevice:[4,3,1,""],APNSMessage:[4,3,1,""],APNSConfig:[4,3,1,""],MessageBase:[4,3,1,""],APNSDevice:[4,3,1,""],DeviceBase:[4,3,1,""],GCMConfig:[4,3,1,""],GCMMessage:[4,3,1,""]},"syncano.models.accounts.User":{profile:[26,1,1,""],created_at:[26,1,1,""],DoesNotExist:[26,1,1,""],links:[26,1,1,""],reset_key:[26,2,1,""],remove_from_group:[26,2,1,""],please:[26,1,1,""],updated_at:[26,1,1,""],auth:[26,2,1,""],username:[26,1,1,""],list_groups:[26,2,1,""],group_details:[26,2,1,""],user_key:[26,1,1,""],pk:[26,1,1,""],password:[26,1,1,""],instance_name:[26,1,1,""],id:[26,1,1,""],add_to_group:[26,2,1,""]},"syncano.models.accounts.Group":{created_at:[26,1,1,""],description:[26,1,1,""],links:[26,1,1,""],user_details:[26,2,1,""],list_users:[26,2,1,""],please:[26,1,1,""],updated_at:[26,1,1,""],delete_user:[26,2,1,""],label:[26,1,1,""],instance_name:[26,1,1,""],add_user:[26,2,1,""],pk:[26,1,1,""],DoesNotExist:[26,1,1,""],id:[26,1,1,""]},"syncano.models.manager.ScriptManager":{run:[17,2,1,""]},"syncano.models.billing.Coupon":{name:[6,1,1,""],percent_off:[6,1,1,""],currency:[6,1,1,""],please:[6,1,1,""],links:[6,1,1,""],amount_off:[6,1,1,""],redeem_by:[6,1,1,""],CURRENCY_CHOICES:[6,1,1,""],duration:[6,1,1,""],pk:[6,1,1,""],DoesNotExist:[6,1,1,""]},"syncano.models.custom_sockets_utils":{CallType:[24,3,1,""],EndpointMetadataMixin:[24,3,1,""],Endpoint:[24,3,1,""],BaseDependency:[24,3,1,""],ScriptCall:[24,3,1,""],ClassDependency:[24,3,1,""],ScriptDependency:[24,3,1,""],DependencyMetadataMixin:[24,3,1,""],BaseCall:[24,3,1,""],DependencyType:[24,3,1,""]},"syncano.models.hosting.Hosting":{instance_name:[5,1,1,""],name:[5,1,1,""],links:[5,1,1,""],is_active:[5,1,1,""],list_files:[5,2,1,""],created_at:[5,1,1,""],description:[5,1,1,""],please:[5,1,1,""],updated_at:[5,1,1,""],is_default:[5,1,1,""],set_default:[5,2,1,""],domains:[5,1,1,""],pk:[5,1,1,""],upload_file:[5,2,1,""],DoesNotExist:[5,1,1,""],id:[5,1,1,""],update_file:[5,2,1,""]},"syncano.models.options.Options":{get_endpoint_path:[16,2,1,""],contribute_to_class:[16,2,1,""],get_endpoint_properties:[16,2,1,""],get_endpoint:[16,2,1,""],get_endpoint_methods:[16,2,1,""],resolve_endpoint:[16,2,1,""],add_field:[16,2,1,""],resolve_parent_data:[16,2,1,""],get_endpoint_query_params:[16,2,1,""],get_field:[16,2,1,""],get_path_properties:[16,2,1,""],build_properties:[16,2,1,""],is_http_method_available:[16,2,1,""]},"syncano.connection.ConnectionMixin":{connection:[23,1,1,""]},"syncano.models.fields.PushJSONField":{to_native:[14,2,1,""]},"syncano.models.fields.WritableField":{read_only:[14,1,1,""],required:[14,1,1,""]},"syncano.models.traces.ScriptEndpointTrace":{status:[0,1,1,""],links:[0,1,1,""],script_endpoint_name:[0,1,1,""],please:[0,1,1,""],executed_at:[0,1,1,""],instance_name:[0,1,1,""],result:[0,1,1,""],STATUS_CHOICES:[0,1,1,""],duration:[0,1,1,""],pk:[0,1,1,""],DoesNotExist:[0,1,1,""],id:[0,1,1,""]},"syncano.models.data_views.DataEndpoint":{rename:[21,2,1,""],add_object:[21,2,1,""],order_by:[21,1,1,""],name:[21,1,1,""],links:[21,1,1,""],clear_cache:[21,2,1,""],class_name:[21,1,1,""],get:[21,2,1,""],please:[21,1,1,""],excluded_fields:[21,1,1,""],page_size:[21,1,1,""],instance_name:[21,1,1,""],query:[21,1,1,""],pk:[21,1,1,""],PERMISSIONS_CHOICES:[21,1,1,""],DoesNotExist:[21,1,1,""],expand:[21,1,1,""],description:[21,1,1,""]},"syncano.models.instances.InstanceInvitation":{links:[3,1,1,""],Admin:[3,3,1,""],created_at:[3,1,1,""],please:[3,1,1,""],updated_at:[3,1,1,""],id:[3,1,1,""],instance_name:[3,1,1,""],state:[3,1,1,""],role:[3,1,1,""],key:[3,1,1,""],pk:[3,1,1,""],DoesNotExist:[3,1,1,""],email:[3,1,1,""],resend:[3,2,1,""]}},titleterms:{all:[19,9],bill:6,backward:9,incent:25,creat:[19,9],modul:[27,12,22],interact:[9,18],back:9,indic:8,raw:[19,9],custom_socket:10,connect:[23,9,18],tabl:8,instal:[19,18],environ:18,custom:19,option:16,variabl:9,content:[12,22],from:19,relationship:9,start:[8,18],syncano:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27],make:18,format:19,run:19,except:11,virtual:18,next:18,environment:9,field:14,json:9,interfac:8,troubleshoot:18,lookup:9,custom_sockets_util:24,save:9,singl:9,updat:19,model:[20,0,14,1,9,10,22,21,2,24,3,15,4,25,5,26,6,7,17,13,16],object:9,"return":9,registri:2,trace:0,get:[8,18],python:8,relat:9,submodul:[12,22],push_notif:4,host:5,lazi:9,fall:9,archetyp:1,depend:19,custom_respons:15,geo:7,"class":20,data_view:21,subpackag:12,util:27,account:26,endpoint:19,retriev:9,socket:19,read:19,servic:8,access:9,step:18,manag:[17,9],url:19,instanc:3,limit:9,channel:13,span:9,chang:9,refer:12,remov:9,recheck:19}}) \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 27713dd..0000000 --- a/setup.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[bdist_wheel] -universal = 1 - -[flake8] -ignore = W292 -max-line-length = 120 -max-complexity = 10 diff --git a/setup.py b/setup.py deleted file mode 100644 index 587588b..0000000 --- a/setup.py +++ /dev/null @@ -1,40 +0,0 @@ -from setuptools import find_packages, setup -from syncano import __version__ - - -def readme(): - with open('README.rst') as f: - return f.read() - -setup( - name='syncano', - version=__version__, - description='Python Library for syncano.com api', - long_description=readme(), - author='Syncano', - author_email='support@syncano.io', - url='http://syncano.com', - packages=find_packages(exclude=['tests']), - zip_safe=False, - classifiers=[ - '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', - 'certifi==2015.09.06.2', - 'ndg-httpsclient==0.4.0', - 'pyasn1==0.1.8', - 'pyOpenSSL==0.15.1', - 'python-slugify==0.1.0', - 'six==1.9.0', - 'validictory==1.0.0', - ], - tests_require=[ - 'mock>=1.0.1', - 'coverage>=3.7.1', - ], -) diff --git a/syncano/connection.py b/syncano/connection.py deleted file mode 100644 index f7c54f9..0000000 --- a/syncano/connection.py +++ /dev/null @@ -1,473 +0,0 @@ -import json -import time -from copy import deepcopy - -import requests -import six -import syncano -from syncano.exceptions import RevisionMismatchException, SyncanoRequestError, SyncanoValueError - -if six.PY3: - from urllib.parse import urljoin -else: - from urlparse import urljoin - - -__all__ = ['Connection', 'ConnectionMixin'] - - -def is_success(code): - """Checks if response code is successful.""" - return 200 <= code <= 299 - - -def is_client_error(code): - """Checks if response code has client error.""" - return 400 <= code <= 499 - - -def is_server_error(code): - """Checks if response code has server error.""" - return 500 <= code <= 599 - - -class DefaultConnection(object): - """Singleton class which holds default connection.""" - - def __init__(self): - self._connection = None - - def __call__(self): - if not self._connection: - raise SyncanoValueError('Please open new connection.') - return self._connection - - def open(self, *args, **kwargs): - connection = Connection(*args, **kwargs) - if not self._connection: - self._connection = connection - return connection - - -class Connection(object): - """Base connection class. - - :ivar host: Syncano API host - :ivar email: Your Syncano email address - :ivar password: Your Syncano password - :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 - """ - - CONTENT_TYPE = 'application/json' - - AUTH_SUFFIX = 'v1.1/account/auth' - ACCOUNT_SUFFIX = 'v1.1/account/' - SOCIAL_AUTH_SUFFIX = AUTH_SUFFIX + '/{social_backend}/' - - 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'} - 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 - 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) - - self._init_login_params(kwargs) - - if self.is_user: - 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() - - def _init_login_params(self, login_kwargs): - 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) - - def _are_params_ok(self, params): - return all(getattr(self, p) for p in params) - - @property - def is_user(self): - 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_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.ALT_LOGIN_PARAMS) - - @property - def auth_key(self): - if self.is_user: - return self.user_key - return self.api_key - - def build_params(self, params): - """ - :type params: dict - :param params: Params which will be passed to request - - :rtype: dict - :return: Request params - """ - params = deepcopy(params) - 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.is_user: - 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'] = 'token {}'.format(self.api_key) - - # We don't need to check SSL cert in DEBUG mode - if syncano.DEBUG or not self.verify_ssl: - params['verify'] = False - - return params - - def build_url(self, path): - """Ensures proper format for provided path. - - :type path: string - :param path: Request path - - :rtype: string - :return: Request URL - """ - if not isinstance(path, six.string_types): - raise SyncanoValueError('"path" should be a string.') - - query = None - - if path.startswith(self.host): - return path - - if '?' in path: - path, query = path.split('?', 1) - - if not path.endswith('/'): - path += '/' - - if path.startswith('/'): - path = path[1:] - - if query: - path = '{0}?{1}'.format(path, query) - - return urljoin(self.host, path) - - def request(self, method_name, path, **kwargs): - """Simple wrapper around :func:`~syncano.connection.Connection.make_request` which - will ensure that request is authenticated. - - :type method_name: string - :param method_name: HTTP request method e.g: GET - - :type path: string - :param path: Request path or full URL - - :rtype: dict - :return: JSON response - """ - 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): - """ - :type method_name: string - :param method_name: HTTP request method e.g: GET - - :type path: string - :param path: Request path or full URL - - :rtype: dict - :return: JSON response - - :raises SyncanoValueError: if invalid request method was chosen - :raises SyncanoRequestError: if something went wrong during the request - """ - data = kwargs.get('data', {}) - files = data.pop('files', None) - - self._check_batch_files(data) - - if files is None: - files = {k: v for k, v in six.iteritems(data) if hasattr(v, 'read')} - if data: - 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) - - # 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( - debug_params, - sort_keys=True, - indent=2, - separators=(',', ': ') - ) - self.logger.debug('API Root: %s', self.host) - self.logger.debug('Request: %s %s\n%s', method_name, path, formatted_params) - - if method is None: - raise SyncanoValueError('Invalid request method: {0}.'.format(method_name)) - - # 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) - - 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: - # remove 'data' and 'content-type' to avoid "ValueError: Data must not be a string." - params.pop('data') - params['headers'].pop('content-type') - params['files'] = self._process_apns_cert_files(files) - - if response.status_code == 201: - 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 - - def get_response_content(self, url, response): - try: - content = response.json() - except ValueError: - content = response.text - - if is_server_error(response.status_code): - raise SyncanoRequestError(response.status_code, 'Server error.') - - # Validation error - if is_client_error(response.status_code): - if response.status_code == 400 and 'expected_revision' in content: - raise RevisionMismatchException(response.status_code, content) - raise SyncanoRequestError(response.status_code, content) - - # Other errors - if not is_success(response.status_code): - self.logger.debug('Request Error: %s', url) - self.logger.debug('Status code: %d', response.status_code) - self.logger.debug('Response: %s', content) - raise SyncanoRequestError(response.status_code, content) - - return content - - def is_authenticated(self): - """Checks if current session is authenticated. - - :rtype: boolean - :return: Session authentication state - """ - if self.is_user: - return self.user_key is not None - return self.api_key is not None - - def authenticate(self, **kwargs): - """ - :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 ``Account Key`` - """ - is_auth = self.is_authenticated() - - if is_auth: - msg = 'Connection already authenticated: {}' - else: - 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, params): - for k in 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): - 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) - request_args['access_token'] = request_args.pop('token') # core expects a access_token field; - 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): - 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') - } - response = self.make_request('POST', self.AUTH_SUFFIX, data=request_args, headers=headers) - self.user_key = response.get('user_key') - return self.user_key - - def get_account_info(self, api_key=None): - self.api_key = api_key or 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 or self.api_key - self.user_key = user_key or self.user_key - - for attribute_name in ('api_key', 'user_key', 'instance_name'): - if not getattr(self, attribute_name, None): - 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}) - - @classmethod - def _check_batch_files(cls, data): - if 'requests' in data: # batch requests - for request in data['requests']: - per_request_files = request.get('body', {}).get('files', {}) - if per_request_files: - raise SyncanoValueError('Batch do not support files upload.') - - def _process_apns_cert_files(self, files): - files = files.copy() - 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): - 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 - - 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.""" - - def __init__(self, *args, **kwargs): - self._connection = None - super(ConnectionMixin, self).__init__(*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 - def connection(self, value): - if not isinstance(value, Connection): - raise SyncanoValueError('"connection" needs to be a Syncano Connection instance.') - self._connection = value - - @connection.deleter - def connection(self): - self._connection = None diff --git a/syncano/models/__init__.py b/syncano/models/__init__.py deleted file mode 100644 index 530d26c..0000000 --- a/syncano/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .base import * # NOQA diff --git a/syncano/models/base.py b/syncano/models/base.py deleted file mode 100644 index 2f4eeeb..0000000 --- a/syncano/models/base.py +++ /dev/null @@ -1,17 +0,0 @@ -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 .data_views import * # NOQA -from .incentives import * # NOQA -from .traces import * # NOQA -from .push_notification import * # NOQA -from .geo import * # NOQA -from .backups 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 diff --git a/syncano/models/hosting.py b/syncano/models/hosting.py deleted file mode 100644 index 48b95e4..0000000 --- a/syncano/models/hosting.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- - -from . import fields -from .base import Model -from .instances import Instance - - -class Hosting(Model): - """ - OO wrapper around hosting. - """ - - 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=[]) - - 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): - """ - 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() - 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: - return - 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: - # create if not found; - hosting_file = self.upload_file(path, file) - return hosting_file - - connection = self._get_connection() - 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: - return - return HostingFile(**response.json()) - - def list_files(self): - return self._get_files() - - def set_default(self): - default_path = self.links.set_default - connection = self._get_connection() - - 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/syncano/models/incentives.py b/syncano/models/incentives.py deleted file mode 100644 index 84ef2e4..0000000 --- a/syncano/models/incentives.py +++ /dev/null @@ -1,363 +0,0 @@ - - -import json - -from syncano.exceptions import SyncanoValidationError - -from . import fields -from .base import Model -from .instances import Instance -from .manager import ScriptEndpointManager, ScriptManager -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 `_. - - :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:: - **Script** has special method called ``run`` which will execute attached source code:: - - >>> 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:: - - >>> s = Script.please.get('instance-name', 1234) - >>> s.run() - >>> s.run(variable_one=1, variable_two=2) - """ - - label = fields.StringField(max_length=80, required=False) - description = fields.StringField(required=False) - source = fields.StringField() - runtime_name = fields.StringField() - 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) - - traces = fields.RelatedManagerField('ScriptTrace') - - please = ScriptManager() - - class Meta: - parent = Instance - name = 'Script' - plural_name = 'Scripts' - endpoints = { - 'detail': { - 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/snippets/scripts/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/snippets/scripts/', - }, - 'run': { - 'methods': ['post'], - 'path': '/snippets/scripts/{id}/run/', - }, - } - - def run(self, **payload): - """ - Usage:: - - >>> s = Script.please.get('instance-name', 1234) - >>> s.run() - >>> s.run(variable_one=1, variable_two=2) - """ - from .traces import ScriptTrace - - if self.is_new(): - raise SyncanoValidationError('Method allowed only on existing model.') - - properties = self.get_endpoint_data() - 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(http_method, endpoint, **request) - response.update({'instance_name': self.instance_name, 'script_id': self.id}) - return ScriptTrace(**response) - - -class Schedule(Model): - """ - OO wrapper around script schedules `link `_. - - :ivar label: :class:`~syncano.models.fields.StringField` - :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` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar scheduled_next: :class:`~syncano.models.fields.DateTimeField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - """ - - label = fields.StringField(max_length=80) - 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) - 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() - - traces = fields.RelatedManagerField('ScheduleTraces') - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/schedules/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/schedules/', - } - } - - -class Trigger(Model): - """ - OO wrapper around triggers `link `_. - - :ivar label: :class:`~syncano.models.fields.StringField` - :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` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - """ - - 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) - script = fields.IntegerField(label='script id') - class_name = fields.StringField(label='class name', mapping='class') - signal = fields.ChoiceField(choices=SIGNAL_CHOICES) - 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') - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/triggers/{id}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/triggers/', - } - } - - -class ScriptEndpoint(Model): - """ - OO wrapper around script endpoints `link `_. - - :ivar name: :class:`~syncano.models.fields.SlugField` - :ivar script: :class:`~syncano.models.fields.IntegerField` - :ivar links: :class:`~syncano.models.fields.HyperlinkedField` - - .. note:: - **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}") - - or via instance:: - - >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') - >>> se.run() - >>> se.run(variable_one=1, variable_two=2) - - """ - - 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.LinksField() - - traces = fields.RelatedManagerField('ScriptEndpointTrace') - please = ScriptEndpointManager() - - class Meta: - parent = Instance - endpoints = { - 'detail': { - 'methods': ['put', 'get', 'patch', 'delete'], - 'path': '/endpoints/scripts/{name}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/endpoints/scripts/', - }, - 'run': { - 'methods': ['post'], - 'path': '/endpoints/scripts/{name}/run/', - }, - 'reset': { - 'methods': ['post'], - 'path': '/endpoints/scripts/{name}/reset_link/', - }, - 'public': { - 'methods': ['get'], - 'path': '/endpoints/scripts/p/{public_link}/{name}/', - } - } - - def run(self, cache_key=None, **payload): - """ - Usage:: - - >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') - >>> se.run() - >>> se.run(variable_one=1, variable_two=2) - """ - from .traces import ScriptEndpointTrace - - if self.is_new(): - raise SyncanoValidationError('Method allowed only on existing model.') - - properties = self.get_endpoint_data() - http_method = 'POST' - endpoint = self._meta.resolve_endpoint('run', properties, http_method) - connection = self._get_connection(**payload) - - params = {} - if cache_key is not None: - params = {'cache_key': cache_key} - - kwargs = {'data': payload} - if params: - kwargs.update({'params': params}) - - 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, - '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:: - - >>> se = ScriptEndpoint.please.get('instance-name', 'script-name') - >>> se.reset_link() - """ - properties = self.get_endpoint_data() - http_method = 'POST' - endpoint = self._meta.resolve_endpoint('reset', properties, http_method) - connection = self._get_connection() - - response = connection.request(http_method, endpoint) - self.public_link = response['public_link'] - - -class ResponseTemplate(RenameMixin, Model): - """ - OO wrapper around templates. - - :ivar name: :class:`~syncano.models.fields.StringField` - :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` - :ivar created_at: :class:`~syncano.models.fields.DateTimeField` - :ivar updated_at: :class:`~syncano.models.fields.DateTimeField` - """ - - 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.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': ['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 or {} - properties = self.get_endpoint_data() - http_method = 'POST' - endpoint = self._meta.resolve_endpoint('render', properties, http_method) - - connection = self._get_connection() - return connection.request(http_method, 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 deleted file mode 100644 index 788ba05..0000000 --- a/syncano/models/instances.py +++ /dev/null @@ -1,193 +0,0 @@ -import json - -import six -from syncano.exceptions import SyncanoValueError - -from . import fields -from .base import Model -from .mixins import RenameMixin - - -class Instance(RenameMixin, Model): - """ - OO wrapper around instances `link `_. - - :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` - :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) - 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.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') - data_endpoints = fields.RelatedManagerField('DataEndpoint') - templates = fields.RelatedManagerField('ResponseTemplate') - - triggers = fields.RelatedManagerField('Trigger') - schedules = fields.RelatedManagerField('Schedule') - classes = fields.RelatedManagerField('Class') - invitations = fields.RelatedManagerField('InstanceInvitation') - hostings = fields.RelatedManagerField('Hosting') - - # 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': { - 'methods': ['delete', 'patch', 'put', 'get'], - 'path': '/v1.1/instances/{name}/', - }, - 'list': { - 'methods': ['post', 'get'], - 'path': '/v1.1/instances/', - }, - 'config': { - 'methods': ['put', 'get'], - 'path': '/v1.1/instances/{name}/snippets/config/', - }, - 'endpoints': { - 'methods': ['get'], - 'path': '/v1.1/instances/{name}/endpoints/sockets/' - } - } - - 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): - 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) - data = {'config': config} - connection = self._get_connection() - connection.request(http_method, endpoint, data=data) - - -class ApiKey(Model): - """ - OO wrapper around instance api keys `link `_. - - :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` - """ - - 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.LinksField() - - 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 `link `_. - - :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` - """ - 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.LinksField() - 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/', - } - } - - 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 diff --git a/syncano/models/manager.py b/syncano/models/manager.py deleted file mode 100644 index 61e7ae2..0000000 --- a/syncano/models/manager.py +++ /dev/null @@ -1,1247 +0,0 @@ -import json -from copy import deepcopy - -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 ArrayOperationsMixin, IncrementMixin, clone - -from .registry import registry - -# The maximum number of items to display in a Manager.__repr__ -REPR_OUTPUT_SIZE = 20 - - -class ManagerDescriptor(object): - - def __init__(self, manager): - self.manager = manager - - def __get__(self, instance, owner=None): - if instance is not None: - raise AttributeError("Manager isn't accessible via {0} instances.".format(owner.__name__)) - return self.manager.all() - - -class Manager(ConnectionMixin): - """Base class responsible for all ORM (``please``) actions.""" - - BATCH_URI = '/v1.1/instances/{name}/batch/' - - def __init__(self): - self.name = None - self.model = None - - self.endpoint = None - self.properties = {} - - self.method = None - self.query = {} - self.data = {} - self.is_lazy = False - self._filter_kwargs = {} - - 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]) - if len(data) > REPR_OUTPUT_SIZE: - data[-1] = '...(remaining elements truncated)...' - return repr(data) - - def __str__(self): # pragma: no cover - return ''.format(self.model.__name__) - - def __unicode__(self): # pragma: no cover - return six.u(str(self)) - - def __len__(self): # pragma: no cover - return self.iterator() - - 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] - return True - except IndexError: - return False - - def __getitem__(self, k): - """ - Retrieves an item or slice from the set of results. - """ - if not isinstance(k, (slice,) + six.integer_types): - raise TypeError - assert ((not isinstance(k, slice) and (k >= 0)) or - (isinstance(k, slice) and (k.start is None or k.start >= 0) and - (k.stop is None or k.stop >= 0))), \ - "Negative indexing is not supported." - - manager = self._clone() - - if isinstance(k, slice): - if k.stop is not None: - manager.limit(int(k.stop) + 1) - return list(manager)[k.start:k.stop:k.step] - - manager.limit(k + 1) - return list(manager)[k] - - 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 - - def as_batch(self): - self.is_lazy = True - 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. - - 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, - 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: - 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.instance_name), - **{'data': {'requests': requests}} - ) - - 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): - """ - A convenience method for creating an object and saving it all in one step. Thus:: - - instance = Instance.please.create(name='test-one', description='description') - - and:: - - instance = Instance(name='test-one', description='description') - instance.save() - - are equivalent. - """ - data = self.properties.copy() - attrs = kwargs.copy() - 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) - - saved_instance = instance.save() - if not self.is_lazy: - return instance - - return saved_instance - - def bulk_create(self, *objects): - """ - Creates many new instances based on provided list of objects. - - Usage:: - - 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:: - - This method is restricted to handle 50 objects at once. - """ - return ModelBulkCreate(objects, self).process() - - @clone - def get(self, *args, **kwargs): - """ - Returns the object matching the given lookup parameters. - - Usage:: - - instance = Instance.please.get('test-one') - instance = Instance.please.get(name='test-one') - """ - self.method = 'GET' - self.endpoint = 'detail' - 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.instance_name), - **{'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. - - Usage:: - - instance = Instance.please.detail('test-one') - instance = Instance.please.detail(name='test-one') - """ - return self.get(*args, **kwargs) - - def get_or_create(self, **kwargs): - """ - A convenience method for looking up an object with the given - lookup parameters, creating one if necessary. - - Returns a tuple of **(object, created)**, where **object** is the retrieved or - **created** object and created is a boolean specifying whether a new object was created. - - This is meant as a shortcut to boilerplatish code. For example:: - - try: - instance = Instance.please.get(name='test-one') - except Instance.DoesNotExist: - instance = Instance(name='test-one', description='test') - instance.save() - - The above example can be rewritten using **get_or_create()** like so:: - - instance, created = Instance.please.get_or_create(name='test-one', defaults={'description': 'test'}) - """ - defaults = deepcopy(kwargs.pop('defaults', {})) - try: - instance = self.get(**kwargs) - except self.model.DoesNotExist: - defaults.update(kwargs) - instance = self.create(**defaults) - created = True - else: - created = False - return instance, created - - @clone - def delete(self, *args, **kwargs): - """ - Removes single instance based on provided arguments. - Returns None if deletion went fine. - - Usage:: - - Instance.please.delete('test-one') - Instance.please.delete(name='test-one') - """ - self.method = 'DELETE' - self.endpoint = 'detail' - 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) - - @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: # means that .filter() was run; - return self.new_update(**kwargs) - return self.old_update(*args, **kwargs) - - @clone - def new_update(self, **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; - - """ - - 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.data.update(self._filter_kwargs) - 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=serialized, properties=defaults)] - - instances = [] # ObjectManager context; - for obj in self: - 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=serialized, - properties=defaults) - - instances.append(updated_instance) # always a batch structure here; - - if not self.is_lazy: - 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: - - 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('PATCH', 'PUT', 'POST') - data = kwargs.pop('data', {}) - self.data = kwargs.copy() - self.data.update(data) - - model = self.serialize(self.data, self.model) - - serialized = model.to_native() - - serialized = {k: v for k, v in six.iteritems(serialized) - 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) - - 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. - The ``defaults`` is a dictionary of (field, value) pairs used to update the object. - - Returns a tuple of **(object, created)**, where object is the created or updated object and created - is a boolean specifying whether a new object was created. - - The **update_or_create** method tries to fetch an object from Syncano API based on the given kwargs. - If a match is found, it updates the fields passed in the defaults dictionary. - - This is meant as a shortcut to boilerplatish code. For example:: - - try: - instance = Instance.please.update(name='test-one', data=updated_values) - except Instance.DoesNotExist: - updated_values.update({'name': 'test-one'}) - instance = Instance(**updated_values) - instance.save() - - This pattern gets quite unwieldy as the number of fields in a model goes up. - The above example can be rewritten using **update_or_create()** like so:: - - instance, created = Instance.please.update_or_create(name='test-one', - defaults=updated_values) - """ - defaults = deepcopy(defaults or {}) - try: - instance = self.update(**kwargs) - except self.model.DoesNotExist: - defaults.update(kwargs) - instance = self.create(**defaults) - created = True - else: - created = False - return instance, created - - # List actions - - @clone - def all(self, *args, **kwargs): - """ - Returns a copy of the current ``Manager`` with limit removed. - - Usage:: - - instances = Instance.please.all() - """ - self._limit = None - return self.list(*args, **kwargs) - - @clone - def list(self, *args, **kwargs): - """ - Returns a copy of the current ``Manager`` containing objects that match the given lookup parameters. - - Usage:: - instance = Instance.please.list() - classes = Class.please.list(instance_name='test-one') - """ - self.method = 'GET' - self.endpoint = 'list' - self._filter(*args, **kwargs) - return self - - @clone - def first(self, *args, **kwargs): - """ - Returns the first object matched by the lookup parameters or None, if there is no matching object. - - Usage:: - - instance = Instance.please.first() - classes = Class.please.first(instance_name='test-one') - """ - try: - self._limit = 1 - return self.list(*args, **kwargs)[0] - except KeyError: - return None - - @clone - def page_size(self, value): - """ - Sets page size. - - Usage:: - - instances = Instance.please.page_size(20).all() - """ - if not value or not isinstance(value, six.integer_types): - raise SyncanoValueError('page_size value needs to be an int.') - - self.query['page_size'] = value - return self - - @clone - def limit(self, value): - """ - Sets limit of returned objects. - - Usage:: - - instances = Instance.please.list().limit(10) - classes = Class.please.list(instance_name='test-one').limit(10) - """ - if not value or not isinstance(value, six.integer_types): - raise SyncanoValueError('Limit value needs to be an int.') - - self._limit = value - return self - - @clone - 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 raw(self): - """ - Disables serialization. ``request`` method will return raw Python types. - - Usage:: - - >>> instances = Instance.please.list().raw() - >>> instances - [{'description': 'new one', 'name': 'test-one'...}...] - """ - 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): - """ - Connection juggling. - """ - # ConnectionMixin will validate this - self.connection = connection - return self - - # Other stuff - - def contribute_to_class(self, model, name): # pragma: no cover - setattr(model, name, ManagerDescriptor(self)) - - self.model = model - - 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 six.iteritems(serialized) - 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) - - 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 - - if too_much_properties and id_specified: - properties = ['id'] - - mapped_args = {k: v for k, v in zip(properties, args)} - self.properties.update(mapped_args) - self.properties.update(kwargs) - - def _clone(self): - # Maybe deepcopy ? - manager = self.__class__() - 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 - 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 - - return manager - - 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 - - if not isinstance(data, dict): - raise SyncanoValueError('Unsupported data type.') - - properties = deepcopy(self.properties) - properties.update(data) - 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 - method = method or self.method - allowed_methods = meta.get_endpoint_methods(self.endpoint) - - if not path: - path, defaults = self._get_endpoint_properties() - - if method.lower() not in allowed_methods: - methods = ', '.join(allowed_methods) - raise SyncanoValueError('Unsupported request method "{0}" allowed are {1}.'.format(method, methods)) - - self.build_request(request) - - try: - response = self.connection.request(method, path, **request) - except SyncanoRequestError as e: - if e.status_code == 404: - obj_id = path.rsplit('/')[-2] - raise self.model.DoesNotExist("{} not found.".format(obj_id)) - raise - - if 'next' not in response and not self._template: - return self.serialize(response) - - return response - - def get_allowed_method(self, *methods): - meta = self.model._meta - allowed_methods = meta.get_endpoint_methods(self.endpoint) - - for method in methods: - if method.lower() in allowed_methods: - return method - - methods = ', '.join(methods) - raise SyncanoValueError('Unsupported request methods {0}.'.format(methods)) - - def iterator(self): - """Pagination handler""" - - response = self._get_response() - results = 0 - while True: - if self._template: - yield response - break - objects = response.get('objects') - next_url = response.get('next') - - for o in objects: - if self._limit and results >= self._limit: - break - - results += 1 - yield self.serialize(o) - - if not objects or not next_url or (self._limit and results >= self._limit): - break - - response = self.request(path=next_url) - - def _get_response(self): - return self.request() - - 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) - return self.model._meta.resolve_endpoint(self.endpoint, defaults), defaults - - -class ScriptManager(Manager): - """ - Custom :class:`~syncano.models.manager.Manager` - class for :class:`~syncano.models.base.Script` model. - """ - - @clone - def run(self, *args, **kwargs): - payload = kwargs.pop('payload', {}) - - if not isinstance(payload, six.string_types): - payload = json.dumps(payload) - - self.method = 'POST' - self.endpoint = 'run' - self.data['payload'] = payload - self._filter(*args, **kwargs) - self._serialize = False - response = self.request() - return registry.ScriptTrace(**response) - - -class ScriptEndpointManager(Manager): - """ - Custom :class:`~syncano.models.manager.Manager` - class for :class:`~syncano.models.base.ScriptEndpoint` model. - """ - - @clone - def run(self, *args, **kwargs): - payload = kwargs.pop('payload', {}) - - if not isinstance(payload, six.string_types): - payload = json.dumps(payload) - - self.method = 'POST' - self.endpoint = 'run' - self.data['payload'] = payload - self._filter(*args, **kwargs) - self._serialize = False - response = self.request() - - # Workaround for circular import - return registry.ScriptEndpointTrace(**response) - - -class ObjectManager(IncrementMixin, ArrayOperationsMixin, Manager): - """ - Custom :class:`~syncano.models.manager.Manager` - class for :class:`~syncano.models.base.Object` model. - """ - LOOKUP_SEPARATOR = '__' - ALLOWED_LOOKUPS = [ - 'gt', 'gte', 'lt', 'lte', - 'eq', 'neq', 'exists', 'in', 'nin', - 'near', 'is', 'contains', - 'startswith', 'endswith', - 'contains', 'istartswith', - 'iendswith', 'icontains', - 'ieq', 'near', - ] - - 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) - - @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 count of the returned objects: count = DataObjects.please.list(...).count(); - """ - self.method = 'GET' - self.query.update({ - 'include_count': True, - 'page_size': 0, - }) - response = self.request() - return response['objects_count'] - - @clone - def with_count(self, page_size=20): - """ - 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(); - """ - query_data = { - 'include_count': True, - 'page_size': page_size, - } - - self.method = 'GET' - self.query.update(query_data) - response = self.request() - self._initial_response = response - return self, self._initial_response['objects_count'] - - @clone - def filter(self, **kwargs): - """ - Special method just for data object :class:`~syncano.models.base.Object` model. - - Usage:: - - objects = Object.please.list('instance-name', 'class-name').filter(henryk__gte='hello') - """ - - 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) - - # 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) - - 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) - 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. - 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_response(self): - return self._initial_response or self.request() - - 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) - - 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): - """ - Special method just for data object :class:`~syncano.models.base.Object` model. - - Usage:: - - 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' - 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') - """ - model_fields = self._get_model_field_names() - self._validate_fields(model_fields, args) - - fields = [f for f in model_fields if f not in args] - - self.query['fields'] = ','.join(fields) - self.method = 'GET' - self.endpoint = 'list' - return self - - def ordering(self, order=None): - raise AttributeError('Ordering not implemented. Use order_by instead.') - - @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 - - def _clone(self): - manager = super(ObjectManager, self)._clone() - manager._initial_response = self._initial_response - return manager - - -class SchemaManager(object): - """ - Custom :class:`~syncano.models.manager.Manager` - class for :class:`~syncano.models.fields.SchemaFiled`. - """ - - def __init__(self, schema=None): - self.schema = schema or [] - - def __eq__(self, other): - if isinstance(other, SchemaManager): - return self.schema == other.schema - return NotImplemented - - def __str__(self): # pragma: no cover - return str(self.schema) - - def __repr__(self): # pragma: no cover - return '' - - def __getitem__(self, key): - if isinstance(key, int): - return self.schema[key] - - if isinstance(key, six.string_types): - for v in self.schema: - if v['name'] == key: - return v - - raise KeyError - - def __setitem__(self, key, value): - value = deepcopy(value) - value['name'] = key - self.remove(key) - self.add(value) - - def __delitem__(self, key): - self.remove(key) - - def __iter__(self): - return iter(self.schema) - - def __contains__(self, item): - if not self.schema: - return False - return item in self.schema - - def set(self, value): - """Sets schema value.""" - self.schema = value - - def add(self, *objects): - """Adds multiple objects to schema.""" - self.schema.extend(objects) - - def remove(self, *names): - """Removes selected objects based on their names.""" - values = [v for v in self.schema if v['name'] not in names] - self.set(values) - - def clear(self): - """Sets empty schema.""" - self.set([]) - - def set_index(self, field, order=False, filter=False): - """Sets index on selected field. - - :type field: string - :param field: Name of schema field - - :type filter: bool - :param filter: Sets filter index on selected field - - :type order: bool - :param order: Sets order index on selected field - """ - if not order and not filter: - raise ValueError('Choose at least one index.') - - if order: - self[field]['order_index'] = True - - if filter: - self[field]['filter_index'] = True - - def set_order_index(self, field): - """Shortcut for ``set_index(field, order=True)``.""" - self.set_index(field, order=True) - - def set_filter_index(self, field): - """Shortcut for ``set_index(field, filter=True)``.""" - self.set_index(field, filter=True) - - def remove_index(self, field, order=False, filter=False): - """Removes index from selected field. - - :type field: string - :param field: Name of schema field - - :type filter: bool - :param filter: Removes filter index from selected field - - :type order: bool - :param order: Removes order index from selected field - """ - if not order and not filter: - raise ValueError('Choose at least one index.') - - if order and 'order_index' in self[field]: - del self[field]['order_index'] - - if filter and 'filter_index' in self[field]: - del self[field]['filter_index'] - - def remove_order_index(self, field): - """Shortcut for ``remove_index(field, order=True)``.""" - self.remove_index(field, order=True) - - def remove_filter_index(self, field): - """Shortcut for ``remove_index(field, filter=True)``.""" - self.remove_index(field, filter=True) diff --git a/syncano/models/manager_mixins.py b/syncano/models/manager_mixins.py deleted file mode 100644 index 91defef..0000000 --- a/syncano/models/manager_mixins.py +++ /dev/null @@ -1,266 +0,0 @@ -# -*- 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 - - -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 diff --git a/syncano/models/registry.py b/syncano/models/registry.py deleted file mode 100644 index e6be17b..0000000 --- a/syncano/models/registry.py +++ /dev/null @@ -1,110 +0,0 @@ - - -import re - -import six -from syncano import logger - - -class Registry(object): - """Models registry. - """ - def __init__(self, models=None): - self.models = models or {} - self.schemas = {} - self.patterns = [] - self._pending_lookups = {} - self.instance_name = None - self._default_connection = None - - def __str__(self): - return 'Registry: {0}'.format(', '.join(self.models)) - - def __unicode__(self): - return six.u(str(self)) - - def __iter__(self): - for name, model in six.iteritems(self.models): - yield model - - def get_model_patterns(self, cls): - patterns = [] - for k, v in six.iteritems(cls._meta.endpoints): - pattern = '^{0}$'.format(v['path']) - for name in v.get('properties', []): - pattern = pattern.replace('{{{0}}}'.format(name), '([^/.]+)') - patterns.append((re.compile(pattern), cls)) - return patterns - - def get_model_by_path(self, path): - for pattern, cls in self.patterns: - if pattern.match(path): - return cls - raise LookupError('Invalid path: {0}'.format(path)) - - def get_model_by_name(self, name): - return self.models[name] - - 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) - - setattr(self, str(name), cls) - setattr(self, str(related_name), cls.please.all()) - - 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) - for callback, args, kwargs in lookups: - callback(*args, **kwargs) - - return self - - def set_default_property(self, name, value): - for model in self: - if name not in model.__dict__: - continue - - 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) - - 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.instance_name = instance - - def clear_used_instance(self): - self.instance_name = 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 = {} - - 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/certificates/ApplePushDevelopment.p12 b/tests/certificates/ApplePushDevelopment.p12 deleted file mode 100644 index a8a0aa8..0000000 Binary files a/tests/certificates/ApplePushDevelopment.p12 and /dev/null differ diff --git a/tests/integration_test.py b/tests/integration_test.py deleted file mode 100644 index 2acfcf3..0000000 --- a/tests/integration_test.py +++ /dev/null @@ -1,663 +0,0 @@ -import os -import unittest -from datetime import datetime -from hashlib import md5 -from time import sleep -from uuid import uuid4 - -import syncano -from syncano.exceptions import SyncanoRequestError, SyncanoValueError -from syncano.models import ApiKey, Class, DataEndpoint, Instance, Model, Object, Script, ScriptEndpoint, registry - - -class IntegrationTest(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.API_KEY = os.getenv('INTEGRATION_API_KEY') - cls.API_EMAIL = os.getenv('INTEGRATION_API_EMAIL') - cls.API_PASSWORD = os.getenv('INTEGRATION_API_PASSWORD') - cls.API_ROOT = os.getenv('INTEGRATION_API_ROOT') - - cls.connection = syncano.connect( - host=cls.API_ROOT, - email=cls.API_EMAIL, - password=cls.API_PASSWORD, - api_key=cls.API_KEY - ) - - @classmethod - def tearDownClass(cls): - cls.connection = None - - @classmethod - def generate_hash(cls): - hash_feed = '{}{}'.format(uuid4(), datetime.now()) - return md5(hash_feed.encode('ascii')).hexdigest() - - -class InstanceMixin(object): - - @classmethod - def setUpClass(cls): - super(InstanceMixin, cls).setUpClass() - - cls.instance = cls.connection.Instance.please.create( - name='testpythonlib%s' % cls.generate_hash()[:10], - description='IntegrationTest %s' % datetime.now(), - ) - - @classmethod - def tearDownClass(cls): - cls.instance.delete() - super(InstanceMixin, cls).tearDownClass() - - -class InstanceIntegrationTest(IntegrationTest): - model = Instance - - @classmethod - def tearDownClass(cls): - for i in cls.connection.Instance.please.all(): - i.delete() - cls.connection = None - - def test_list(self): - instances = self.model.please.all() - self.assertTrue(len(list(instances)) >= 0) - - def test_create(self): - name = 'i%s' % self.generate_hash()[:10] - description = 'IntegrationTest' - - instance = self.model.please.create(name=name, description=description) - 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() - self.assertIsNotNone(instance.pk) - self.assertEqual(instance.name, name) - self.assertEqual(instance.description, description) - instance.delete() - - def test_update(self): - name = 'i%s' % self.generate_hash()[:10] - description = 'IntegrationTest' - - instance = self.model.please.create(name=name, description=description) - instance.description = 'NotIntegrationTest' - instance.save() - - instance2 = self.model.please.get(name=name) - self.assertEqual(instance.description, instance2.description) - - instance.delete() - - def test_delete(self): - name = 'i%s' % self.generate_hash()[:10] - description = 'IntegrationTest' - - instance = self.model.please.create(name=name, description=description) - instance.delete() - - 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 - - def test_instance_name_is_required(self): - registry.clear_used_instance() - with self.assertRaises(SyncanoValueError): - list(self.model.please.all()) - - with self.assertRaises(SyncanoValueError): - self.model.please.create() - - def test_list(self): - classes = self.model.please.all(instance_name=self.instance.name) - self.assertTrue(len(list(classes)) >= 0) - - cls = self.model.please.create(instance_name=self.instance.name, - name='IntegrationTest%s' % self.generate_hash()[:10], - schema=[{'type': 'string', 'name': 'test'}]) - classes = self.model.please.all(instance_name=self.instance.name) - self.assertTrue(len(list(classes)) >= 1) - - cls.delete() - - def test_create(self): - cls_one = self.model.please.create( - instance_name=self.instance.name, - name='c%s' % self.generate_hash()[:10], - schema=[ - {'type': 'string', 'name': 'string_test'}, - {'type': 'text', 'name': 'text_test'}, - {'type': 'integer', 'name': 'integer_test', 'order_index': True, 'filter_index': True}, - {'type': 'float', 'name': 'float_test'}, - {'type': 'boolean', 'name': 'boolean_test'}, - {'type': 'datetime', 'name': 'datetime_test'}, - {'type': 'file', 'name': 'file_test'}, - ] - ) - - cls_two = self.model.please.create( - instance_name=self.instance.name, - name='c%s' % self.generate_hash()[:10], - schema=[ - {'type': 'string', 'name': 'string_test'}, - {'type': 'text', 'name': 'text_test'}, - {'type': 'integer', 'name': 'integer_test'}, - {'type': 'float', 'name': 'float_test'}, - {'type': 'boolean', 'name': 'boolean_test'}, - {'type': 'datetime', 'name': 'datetime_test'}, - {'type': 'file', 'name': 'file_test'}, - {'type': 'reference', 'name': 'reference_test', - 'order_index': True, 'filter_index': True, 'target': cls_one.name}, - ] - ) - - cls_one.delete() - cls_two.delete() - - def test_update(self): - cls = self.model.please.create( - instance_name=self.instance.name, - name='c%s' % self.generate_hash()[:10], - schema=[ - {'type': 'string', 'name': 'string_test'}, - {'type': 'text', 'name': 'text_test'}, - {'type': 'integer', 'name': 'integer_test', 'order_index': True, 'filter_index': True}, - {'type': 'float', 'name': 'float_test'}, - {'type': 'boolean', 'name': 'boolean_test'}, - {'type': 'datetime', 'name': 'datetime_test'}, - {'type': 'file', 'name': 'file_test'}, - ] - ) - cls.description = 'dummy' - - for i in range(3): - try: - cls.save() - except SyncanoRequestError as e: - if i == 2: - raise - - if e.status_code == 400 and e.reason.startswith('Cannot modify class.'): - sleep(2) - - cls2 = self.model.please.get(instance_name=self.instance.name, name=cls.name) - self.assertEqual(cls.description, cls2.description) - - cls.delete() - - -class ObjectIntegrationTest(InstanceMixin, IntegrationTest): - model = Object - - @classmethod - def setUpClass(cls): - super(ObjectIntegrationTest, cls).setUpClass() - - cls.author = cls.connection.Class.please.create( - instance_name=cls.instance.name, - name='author_%s' % cls.generate_hash()[:10], - schema=[ - {'type': 'string', 'name': 'first_name', 'order_index': True, 'filter_index': True}, - {'type': 'string', 'name': 'last_name', 'order_index': True, 'filter_index': True}, - ] - ) - - cls.book = cls.connection.Class.please.create( - instance_name=cls.instance.name, - name='book_%s' % cls.generate_hash()[:10], - schema=[ - {'type': 'string', 'name': 'name'}, - {'type': 'text', 'name': 'description'}, - {'type': 'integer', 'name': 'quantity'}, - {'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}, - ] - ) - - @classmethod - def tearDownClass(cls): - cls.book.delete() - cls.author.delete() - super(ObjectIntegrationTest, cls).tearDownClass() - - def test_required_fields(self): - with self.assertRaises(SyncanoValueError): - list(self.model.please.all()) - - with self.assertRaises(SyncanoValueError): - list(self.model.please.all(instance_name=self.instance.name)) - - def test_list(self): - objects = self.model.please.all(self.instance.name, self.author.name) - self.assertTrue(len(list(objects)) >= 0) - - objects = self.model.please.all(self.instance.name, self.book.name) - self.assertTrue(len(list(objects)) >= 0) - - def test_create(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) - - 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): - author = self.model.please.create( - instance_name=self.instance.name, class_name=self.author.name, - first_name='john', last_name='doe') - - author.first_name = 'not john' - author.last_name = 'not doe' - author.save() - - author2 = self.model.please.get(author.instance_name, author.class_name, author.pk) - self.assertEqual(author.first_name, author2.first_name) - self.assertEqual(author.last_name, author2.last_name) - - author.delete() - - def test_count_and_with_count(self): - author_one = self.model.please.create( - instance_name=self.instance.name, class_name=self.author.name, - first_name='john1', last_name='doe1') - - author_two = self.model.please.create( - instance_name=self.instance.name, class_name=self.author.name, - first_name='john2', last_name='doe2') - - # just created two authors - - count = Object.please.list(instance_name=self.instance.name, class_name=self.author.name).count() - self.assertEqual(count, 2) - - objects, count = Object.please.list(instance_name=self.instance.name, - class_name=self.author.name).with_count() - - self.assertEqual(count, 2) - for o in objects: - self.assertTrue(isinstance(o, Model)) - - 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) - - 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=self.book.name, - 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=self.book.name, - 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=self.book.name, - id=book.id - ) - - self.assertEqual(book.array, [10]) - - book = Object.please.add_unique( - 'array', - [11], - class_name=self.book.name, - id=book.id - ) - - self.assertEqual(book.array, [10, 11]) - - -class ScriptIntegrationTest(InstanceMixin, IntegrationTest): - model = Script - - @classmethod - def tearDownClass(cls): - for cb in cls.instance.scripts.all(): - cb.delete() - super(ScriptIntegrationTest, cls).tearDownClass() - - def test_required_fields(self): - with self.assertRaises(SyncanoValueError): - registry.clear_used_instance() - list(self.model.please.all()) - - def test_list(self): - scripts = self.model.please.all(self.instance.name) - self.assertTrue(len(list(scripts)) >= 0) - - def test_create(self): - script = self.model.please.create( - instance_name=self.instance.name, - label='cb%s' % self.generate_hash()[:10], - runtime_name='python', - source='print "IntegrationTest"' - ) - - script.delete() - - def test_update(self): - script = self.model.please.create( - instance_name=self.instance.name, - label='cb%s' % self.generate_hash()[:10], - runtime_name='python', - source='print "IntegrationTest"' - ) - - script.source = 'print "NotIntegrationTest"' - script.save() - - script2 = self.model.please.get(self.instance.name, script.pk) - self.assertEqual(script.source, script2.source) - - script.delete() - - def test_source_run(self): - script = self.model.please.create( - instance_name=self.instance.name, - label='cb%s' % self.generate_hash()[:10], - runtime_name='python', - source='print "IntegrationTest"' - ) - - trace = script.run() - while trace.status in ['pending', 'processing']: - sleep(1) - trace.reload() - - self.assertEqual(trace.status, 'success') - self.assertDictEqual(trace.result, {'stderr': '', 'stdout': 'IntegrationTest'}) - - script.delete() - - def test_custom_response_run(self): - script = 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 = script.run() - while trace.status == 'pending': - sleep(1) - trace.reload() - - self.assertEqual(trace.status, 'success') - self.assertDictEqual(trace.content, {'one': 1}) - self.assertEqual(trace.content_type, 'application/json') - self.assertEqual(trace.status_code, 200) - - script.delete() - - -class ScriptEndpointIntegrationTest(InstanceMixin, IntegrationTest): - model = ScriptEndpoint - - @classmethod - def setUpClass(cls): - super(ScriptEndpointIntegrationTest, cls).setUpClass() - - 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_script = Script.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.script.delete() - super(ScriptEndpointIntegrationTest, cls).tearDownClass() - - def test_required_fields(self): - with self.assertRaises(SyncanoValueError): - registry.clear_used_instance() - list(self.model.please.all()) - - def test_list(self): - script_endpoints = self.model.please.all(self.instance.name) - self.assertTrue(len(list(script_endpoints)) >= 0) - - def test_create(self): - script_endpoint = self.model.please.create( - instance_name=self.instance.name, - script=self.script.id, - name='wh%s' % self.generate_hash()[:10], - ) - - script_endpoint.delete() - - def test_script_run(self): - script_endpoint = self.model.please.create( - instance_name=self.instance.name, - script=self.script.id, - name='wh%s' % self.generate_hash()[:10], - ) - - trace = script_endpoint.run() - self.assertEqual(trace.status, 'success') - self.assertDictEqual(trace.result, {'stderr': '', 'stdout': 'IntegrationTest'}) - script_endpoint.delete() - - def test_custom_script_run(self): - script_endpoint = self.model.please.create( - instance_name=self.instance.name, - script=self.custom_script.id, - name='wh%s' % self.generate_hash()[:10], - ) - - trace = script_endpoint.run() - self.assertDictEqual(trace, {'one': 1}) - script_endpoint.delete() - - -class ApiKeyIntegrationTest(InstanceMixin, IntegrationTest): - model = ApiKey - - def test_api_key_flags(self): - api_key = self.model.please.create( - allow_user_create=True, - ignore_acl=True, - allow_anonymous_read=True, - instance_name=self.instance.name, - ) - - reloaded_api_key = self.model.please.get(id=api_key.id, instance_name=self.instance.name) - - self.assertTrue(reloaded_api_key.allow_user_create, True) - self.assertTrue(reloaded_api_key.ignore_acl, True) - self.assertTrue(reloaded_api_key.allow_anonymous_read, True) - - -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, 'sample_klass') - - 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) - - def test_backward_compatibility_name(self): - from syncano.models import EndpointData - - data_endpoint = EndpointData.please.get(name='test_data_endpoint') - self.assertEqual(data_endpoint.class_name, 'sample_klass') diff --git a/tests/integration_test_accounts.py b/tests/integration_test_accounts.py deleted file mode 100644 index 44988c3..0000000 --- a/tests/integration_test_accounts.py +++ /dev/null @@ -1,74 +0,0 @@ -import os - -from syncano.connection import Connection - -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 = "login_class_test" - - 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 - - @classmethod - def tearDownClass(cls): - cls.connection.Instance.please.delete(name=cls.INSTANCE_NAME) - cls.connection = None - - def check_connection(self, con): - response = con.request('GET', '/v1.1/instances/{}/classes/'.format(self.INSTANCE_NAME)) - - obj_list = response['objects'] - - self.assertEqual(len(obj_list), 2) - 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, - email=self.API_EMAIL, - password=self.API_PASSWORD) - self.check_connection(con) - - def test_admin_alt_login(self): - con = Connection(host=self.API_ROOT, - api_key=self.API_KEY) - self.check_connection(con) - - def test_user_login(self): - 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 = 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) - - def test_user_auth(self): - self.assertTrue( - self.connection.User().auth(username=self.USER_NAME, password=self.USER_PASSWORD) - ) diff --git a/tests/integration_test_backups.py b/tests/integration_test_backups.py deleted file mode 100644 index b78f870..0000000 --- a/tests/integration_test_backups.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -import time - -from syncano.models import Backup -from tests.integration_test import InstanceMixin, IntegrationTest - - -class FullBackupTestCase(InstanceMixin, IntegrationTest): - - def _test_backup_create(self): - new_backup = Backup() - 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 = 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): - - backups = [backup for backup in Backup.please.list()] - self.assertTrue(len(backups)) # at least one backup here; - - def _test_backup_schedule_restore(self, backup_id): - backup = Backup.please.get(id=backup_id) - - # wait for backup to be saved - 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() - - restore = backup.schedule_restore() - self.assertIn(restore.status, ['success', 'scheduled']) - - def _test_backup_delete(self, backup_id): - backup = Backup.please.get(id=backup_id) - backup.delete() - backups = [backup_object for backup_object in Backup.please.list()] - self.assertEqual(len(backups), 0) - - 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_schedule_restore(backup_id=backup_id) - self._test_backup_delete(backup_id=backup_id) diff --git a/tests/integration_test_batch.py b/tests/integration_test_batch.py deleted file mode 100644 index c7e4b6e..0000000 --- a/tests/integration_test_batch.py +++ /dev/null @@ -1,132 +0,0 @@ -# -*- coding: utf-8 -*- -import six -from syncano.models import Class, Model, Object, User -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, Model)) - 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, Model)) - 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(id=self.update3.id, title='TerminatorArrival'), - 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) - - 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 six.iteritems(bulk_res): - 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) diff --git a/tests/integration_test_cache.py b/tests/integration_test_cache.py deleted file mode 100644 index 3aff7ad..0000000 --- a/tests/integration_test_cache.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- 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.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', - test2='321', - ) - - 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') diff --git a/tests/integration_test_custom_socket.py b/tests/integration_test_custom_socket.py deleted file mode 100644 index 16b4623..0000000 --- a/tests/integration_test_custom_socket.py +++ /dev/null @@ -1,249 +0,0 @@ -# -*- coding: utf-8 -*- -import time - -from syncano.models import ( - Class, - ClassDependency, - CustomSocket, - Endpoint, - RuntimeChoices, - Script, - ScriptCall, - ScriptDependency, - ScriptEndpoint, - SocketEndpoint -) -from tests.integration_test import InstanceMixin, IntegrationTest - - -class CustomSocketTest(InstanceMixin, IntegrationTest): - - def test_install_custom_socket(self): - # this tests new ScriptEndpoint dependency create; - self.assert_custom_socket('installing', self._define_dependencies_new_script_endpoint) - - def test_dependencies_new_script(self): - self.assert_custom_socket('new_script_installing', self._define_dependencies_new_script) - - def test_dependencies_existing_script(self): - 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_installing', - 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": ["GET", "POST"]}] - } - }, - dependencies=[ - { - "type": "script", - "runtime_name": "python_library_v5.0", - "name": "script_123", - "source": "print(123)" - }, - { - "type": "class", - "name": "klass", - "schema": [ - {"name": "fieldA", "type": "string"}, - {"name": "fieldB", "type": "integer"}, - ] - } - ] - ) - - self.assertTrue(custom_socket.name) - - def test_custom_socket_run(self): - suffix = 'default' - 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'], suffix) - - def test_custom_socket_recheck(self): - suffix = 'recheck' - 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) - - def test_fetching_all_endpoints(self): - all_endpoints = SocketEndpoint.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 = SocketEndpoint.get_all_endpoints()[0] - result = script_endpoint.run() - 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) - 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']) - ) - - socket_to_update.add_endpoint(new_endpoint) - socket_to_update.update() - time.sleep(2) # wait for custom socket setup; - 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) - - 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) - - @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.install() - return custom_socket - - @classmethod - def _define_endpoints(cls, suffix, custom_socket): - endpoint = Endpoint(name='my_endpoint_{}'.format(suffix)) - endpoint.add_call( - ScriptCall( - name='script_endpoint_{}'.format(suffix), - methods=['GET', 'POST'] - ) - ) - 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": "integer"}, - ] - ) - ) - ) - - @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": "integer"}, - ] - ) - klass.save() - custom_socket.add_dependency( - ClassDependency( - klass - ) - ) - - @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 - ) - ) - - @classmethod - def _define_dependencies_new_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 _define_dependencies_existing_script(cls, suffix, custom_socket): - # create Script first: - cls._create_script(suffix) - custom_socket.add_dependency( - ScriptDependency( - Script.please.first(), - name='script_endpoint_{}'.format(suffix), - ) - ) - - @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.id - ) - custom_socket.add_dependency( - ScriptDependency( - ScriptEndpoint.please.first() - ) - ) - - @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( - label='script_{}'.format(suffix), - runtime_name=RuntimeChoices.PYTHON_V5_0, - source='print("{}")'.format(suffix) - ) - - @classmethod - def _wait_till_socket_process(cls, custom_socket): - while custom_socket.status == 'checking': - custom_socket.reload() diff --git a/tests/integration_test_data_endpoint.py b/tests/integration_test_data_endpoint.py deleted file mode 100644 index 6ce9c1e..0000000 --- a/tests/integration_test_data_endpoint.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -import re - -from syncano.models import Class, DataEndpoint, Object, ResponseTemplate -from tests.integration_test import InstanceMixin, IntegrationTest - - -class DataEndpointTest(InstanceMixin, IntegrationTest): - - @classmethod - def setUpClass(cls): - super(DataEndpointTest, cls).setUpClass() - - 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" - ] - } - - cls.klass = Class(name='test_class', schema=schema).save() - cls.template = ResponseTemplate( - name='test_template', - content=template_content, - content_type='text/html', - context=template_context - ).save() - cls.data_endpoint = DataEndpoint(name='test_endpoint', class_name='test_class').save() - - 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.') diff --git a/tests/integration_test_data_objects.py b/tests/integration_test_data_objects.py deleted file mode 100644 index cc7ca06..0000000 --- a/tests/integration_test_data_objects.py +++ /dev/null @@ -1,146 +0,0 @@ -# -*- coding: utf-8 -*- -from hashlib import md5 - -import requests -import six -from syncano.models import Object -from tests.integration_test import InstanceMixin, IntegrationTest - -try: - # python2 - from StringIO import StringIO -except ImportError: - # python3 - from io import StringIO - - -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) - - 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 = self.get_s3_file(data_object.test_field_file) - 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, 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) - - # update also a file; - new_update_string = 'manager with file update' - file_content = 'manager file update' - new_file = StringIO(file_content) - 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, 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) - - # 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): - 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 - ) - - self.assertEqual(data_object.test_field_a, create_string) - self.assert_file_md5(data_object) - - @classmethod - 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): - 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) - - @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: - data_object = Object.please.create( - class_name=self.class_name, - test_field_a=self.initial_field_a, - test_field_file=f, - ) - return data_object diff --git a/tests/integration_test_geo.py b/tests/integration_test_geo.py deleted file mode 100644 index 685f828..0000000 --- a/tests/integration_test_geo.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: utf-8 -*- -import six -from syncano.exceptions import SyncanoValueError -from syncano.models import Class, Distance, GeoPoint, Object -from tests.integration_test import InstanceMixin, IntegrationTest - - -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}, - ] - ) - - 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'] - 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(instance_name=self.instance.name, class_name="city").filter( - location__near={ - "latitude": 52.2297, - "longitude": 21.0122, - "kilometers": distance, - } - ) - - result_list = self._prepare_result_list(objects) - - self.assertListEqual(result_list, cities) - - def test_filtering_on_geo_point_near_miles(self): - objects = Object.please.list(instance_name=self.instance.name, class_name="city").filter( - location__near={ - "latitude": 52.2297, - "longitude": 21.0122, - "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 = 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 = self.city_model.objects.filter( - location__exists=True - ) - - result_list = [o.city for o in objects] - - self.assertListEqual(result_list, self.list_warsaw_berlin_paris) - - objects = self.city_model.objects.filter( - location__exists=False - ) - - result_list = self._prepare_result_list(objects) - - 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/integration_test_push.py b/tests/integration_test_push.py deleted file mode 100644 index 2eaed62..0000000 --- a/tests/integration_test_push.py +++ /dev/null @@ -1,156 +0,0 @@ -# -*- coding: utf-8 -*- -import uuid - -from syncano.exceptions import SyncanoRequestError -from syncano.models import APNSConfig, APNSDevice, APNSMessage, GCMConfig, GCMDevice, GCMMessage -from tests.integration_test import InstanceMixin, IntegrationTest - - -class PushIntegrationTest(InstanceMixin, IntegrationTest): - - @classmethod - def setUpClass(cls): - super(PushIntegrationTest, cls).setUpClass() - - cls.gcm_config = GCMConfig( - development_api_key=uuid.uuid4().hex, - instance_name=cls.instance.name - ) - cls.gcm_config.save() - - with open('tests/certificates/ApplePushDevelopment.p12', 'rb') as cert: - cls.apns_config = APNSConfig( - development_certificate=cert, - development_certificate_name='test', - development_bundle_identifier='test1234', - instance_name=cls.instance.name - ) - 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(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, - label='example label', - registration_id=86152312314401666, - 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='4719084371920471208947120984731208947910827409128470912847120222', - device_id='7189d7b9-4dea-4ecc-aa59-8cc61a20608a', - ) - - 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(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(content={'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) # 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) # we want this to fail; no productions certs; - - def _test_device(self, device, manager): - - device.save() - - 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) - - self.assertEqual(device_.label, device.label) - 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() - - 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/integration_test_register.py b/tests/integration_test_register.py deleted file mode 100644 index 14a1d09..0000000 --- a/tests/integration_test_register.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- 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) diff --git a/tests/integration_test_relations.py b/tests/integration_test_relations.py deleted file mode 100644 index 052a424..0000000 --- a/tests/integration_test_relations.py +++ /dev/null @@ -1,99 +0,0 @@ -# -*- coding: utf-8 -*- -from syncano.models import Class -from tests.integration_test import InstanceMixin, IntegrationTest - - -class RelationApiTest(InstanceMixin, IntegrationTest): - - @classmethod - def setUpClass(cls): - super(RelationApiTest, 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", "filter_index": True}, - ]) - - 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.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() - - 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) - 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.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) - - self.niezwyciezony.authors_set.remove(self.prus, self.lem, self.coehlo) - self.assertEqual(self.niezwyciezony.authors, None) - - 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]) - - 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(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(list(filtered_books)), 1) - for book in filtered_books: - self.assertEqual(book.title, self.niezwyciezony.title) - - def test_multiple_lookups(self): - 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: - self.assertEqual(book.title, self.lalka.title) diff --git a/tests/integration_test_reponse_templates.py b/tests/integration_test_reponse_templates.py deleted file mode 100644 index 51045d1..0000000 --- a/tests/integration_test_reponse_templates.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- - -from syncano.models import Class, ResponseTemplate -from tests.integration_test import InstanceMixin, IntegrationTest - - -class ResponseTemplateApiTest(InstanceMixin, IntegrationTest): - - @classmethod - def setUpClass(cls): - 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(name='to_update') - self.assertTrue(isinstance(template, ResponseTemplate)) - 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', - context={'two': 2}) - self.assertTrue(isinstance(template, ResponseTemplate)) - - def test_delete_api(self): - ResponseTemplate.please.delete(name='to_delete') - with self.assertRaises(ResponseTemplate.DoesNotExist): - ResponseTemplate.please.get(name='to_delete') - - def test_update_api(self): - self.for_update.content = "
      Hello!
      " - self.for_update.save() - - 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_render', - 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
    • ') - - 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) - - def test_render_on_endpoint_list(self): - template_response = Class.please.template('objects_html_table').all() - - self.assertIn('', template_response[0]) # all() returns a list (precise: iterator) - self.assertIn('user_profile', template_response[0]) - - 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) diff --git a/tests/integration_test_snippet_config.py b/tests/integration_test_snippet_config.py deleted file mode 100644 index de0eba7..0000000 --- a/tests/integration_test_snippet_config.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- - -from syncano.exceptions import SyncanoValueError -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(SyncanoValueError): - 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.') diff --git a/tests/integration_test_string_filtering.py b/tests/integration_test_string_filtering.py deleted file mode 100644 index 70aba17..0000000 --- a/tests/integration_test_string_filtering.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- 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': 'some'}) - - 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'}) diff --git a/tests/integration_test_user.py b/tests/integration_test_user.py deleted file mode 100644 index e314e28..0000000 --- a/tests/integration_test_user.py +++ /dev/null @@ -1,116 +0,0 @@ -# -*- coding: utf-8 -*- -from syncano.exceptions import UserNotFound -from syncano.models import Group, 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' - cls.ANOTHER_SAMPLE_PROFILE_PIC = 'yet_another_url' - - 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.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.ANOTHER_SAMPLE_PROFILE_PIC) - - -class UserTest(InstanceMixin, IntegrationTest): - - @classmethod - def setUpClass(cls): - super(UserTest, cls).setUpClass() - - cls.group = cls.instance.groups.create( - label='testgroup' - ) - - 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='testb', - password='1234' - ) - - 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.username, user_test.username) - - 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.username, user_test.username) - - response = group.delete_user(user_id=user_test.id) - self.assertIsNone(response) diff --git a/tests/integration_tests_hosting.py b/tests/integration_tests_hosting.py deleted file mode 100644 index b60f942..0000000 --- a/tests/integration_tests_hosting.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -import uuid - -from tests.integration_test import InstanceMixin, IntegrationTest - -try: - # python2 - from StringIO import StringIO -except ImportError: - # python3 - from io import StringIO - - -class HostingIntegrationTests(InstanceMixin, IntegrationTest): - - 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 = 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._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) - - 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 = 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])] - ) diff --git a/tests/test_channels.py b/tests/test_channels.py deleted file mode 100644 index ed8bdc8..0000000 --- a/tests/test_channels.py +++ /dev/null @@ -1,118 +0,0 @@ -import unittest - -from syncano.models.channels import Channel, PollThread, Timeout - -try: - from unittest import mock -except ImportError: - import mock - - -class PollThreadTestCase(unittest.TestCase): - - def setUp(self): - self.connection_mock = mock.Mock() - self.connection_mock.return_value = self.connection_mock - self.callback_mock = mock.Mock() - self.callback_mock.return_value = False - self.error_mock = mock.Mock() - self.error_mock.return_value = False - self.endpoint = 'dummy' - self.thread = PollThread(self.connection_mock, self.endpoint, - self.callback_mock, self.error_mock) - - def test_request(self): - self.assertFalse(self.connection_mock.request.called) - self.thread.request() - self.assertTrue(self.connection_mock.request.called) - self.connection_mock.request.assert_called_once_with( - 'GET', - self.endpoint, - params={ - 'room': self.thread.room, - 'last_id': self.thread.last_id - }, - timeout=self.thread.timeout - ) - - def test_stop(self): - self.assertFalse(self.thread.abort) - self.thread.stop() - self.assertTrue(self.thread.abort) - self.assertIsNone(self.thread.callback) - self.assertIsNone(self.thread.error) - - @mock.patch('syncano.models.channels.PollThread.request') - def test_run(self, request_mock): - request_mock.return_value = {'id': 1} - self.assertFalse(request_mock.called) - self.assertFalse(self.callback_mock.called) - self.thread.run() - self.assertTrue(request_mock.called) - self.assertTrue(self.callback_mock.called) - - @mock.patch('syncano.models.channels.PollThread.request') - def test_run_timeout(self, request_mock): - request_mock.side_effect = Timeout - self.assertFalse(request_mock.called) - self.assertFalse(self.callback_mock.called) - self.assertFalse(self.error_mock.called) - self.thread.run() - self.assertTrue(request_mock.called) - self.assertTrue(self.callback_mock.called) - self.assertFalse(self.error_mock.called) - - @mock.patch('syncano.models.channels.PollThread.request') - def test_run_error(self, request_mock): - request_mock.side_effect = Exception('dummy') - self.assertFalse(request_mock.called) - self.assertFalse(self.callback_mock.called) - self.assertFalse(self.error_mock.called) - self.thread.run() - self.assertTrue(request_mock.called) - self.assertFalse(self.callback_mock.called) - self.assertTrue(self.error_mock.called) - - -class ChannelTestCase(unittest.TestCase): - - def setUp(self): - self.model = Channel() - - @mock.patch('syncano.models.channels.Channel._get_connection') - @mock.patch('syncano.models.channels.PollThread') - def test_poll(self, poll_thread_mock, connection_mock): - poll_thread_mock.return_value = poll_thread_mock - connection_mock.return_value = connection_mock - - self.assertFalse(poll_thread_mock.called) - self.assertFalse(connection_mock.called) - self.model.poll(room='a', last_id='b', callback='c', error='d', timeout='e') - self.assertTrue(poll_thread_mock.called) - self.assertTrue(connection_mock.called) - poll_thread_mock.assert_called_once_with( - connection_mock, - '/v1.1/instances/None/channels/None/poll/', - 'c', - 'd', - last_id='b', - room='a', - timeout='e', - name='poll_None' - ) - self.assertTrue(poll_thread_mock.start.called) - - @mock.patch('syncano.models.channels.Channel._get_connection') - def test_publish(self, connection_mock): - connection_mock.return_value = connection_mock - - self.assertFalse(connection_mock.called) - self.assertFalse(connection_mock.request.called) - self.model.publish({'a': 1}, 1) - self.assertTrue(connection_mock.called) - self.assertTrue(connection_mock.request.called) - connection_mock.request.assert_called_once_with( - 'POST', - '/v1.1/instances/None/channels/None/publish/', - data={'room': '1', 'payload': '{"a": 1}'} - ) diff --git a/tests/test_classes.py b/tests/test_classes.py deleted file mode 100644 index 8c89579..0000000 --- a/tests/test_classes.py +++ /dev/null @@ -1,152 +0,0 @@ -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') - @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): - - 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', - '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_custom_response.py b/tests/test_custom_response.py deleted file mode 100644 index eba7713..0000000 --- a/tests/test_custom_response.py +++ /dev/null @@ -1,33 +0,0 @@ -import json -import unittest - -from syncano.models.custom_response 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']) diff --git a/tests/test_deprecation_decorator.py b/tests/test_deprecation_decorator.py deleted file mode 100644 index a751713..0000000 --- a/tests/test_deprecation_decorator.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- 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, 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') - # 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('5.0.10', str(warning[-1].message)) diff --git a/tests/test_files/python-logo.png b/tests/test_files/python-logo.png deleted file mode 100644 index 738f6ed..0000000 Binary files a/tests/test_files/python-logo.png and /dev/null differ diff --git a/tests/test_incentives.py b/tests/test_incentives.py deleted file mode 100644 index 6b2b3f4..0000000 --- a/tests/test_incentives.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -import json -import unittest -from datetime import datetime - -from syncano.exceptions import SyncanoValidationError -from syncano.models import ResponseTemplate, Script, ScriptEndpoint, ScriptEndpointTrace, ScriptTrace - -try: - from unittest import mock -except ImportError: - import mock - - -class ScriptTestCase(unittest.TestCase): - - def setUp(self): - self.model = Script() - - @mock.patch('syncano.models.Script._get_connection') - def test_run(self, connection_mock): - 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} - - 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, ScriptTrace) - - connection_mock.assert_called_once_with(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): - model.run() - - -class ScriptEndpointTestCase(unittest.TestCase): - def setUp(self): - self.model = ScriptEndpoint() - - @mock.patch('syncano.models.ScriptEndpoint._get_connection') - def test_run(self, connection_mock): - 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', - 'duration': 937, - 'result': {'stdout': 1, 'stderr': ''}, - '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, ScriptEndpointTrace) - self.assertEqual(result.status, 'success') - self.assertEqual(result.duration, 937) - self.assertEqual(result.result, {'stdout': 1, 'stderr': ''}) - 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.1/instances/test/endpoints/scripts/name/run/', - data={"y": 2, "x": 1} - ) - - model = ScriptEndpoint() - 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.1/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.1/instances/test/snippets/templates/name/render/', - data={'context': {}} - ) diff --git a/tests/test_push.py b/tests/test_push.py deleted file mode 100644 index 598bb9c..0000000 --- a/tests/test_push.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- coding: utf-8 -*- -import json -import unittest - -from mock import mock -from syncano.models import APNSDevice, APNSMessage, GCMDevice, GCMMessage - - -class ScriptTestCase(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( - 'POST', '/v1.1/instances/test/push_notifications/gcm/devices/', - data={"registration_id": '86152312314401555', "device_id": "10000000001", "is_active": True, - "label": "example label"} - ) - 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/' - ) - - @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( - 'POST', '/v1.1/instances/test/push_notifications/apns/devices/', - data={"registration_id": '86152312314401555', "device_id": "10000000001", "is_active": True, - "label": "example label"} - ) - - 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/' - ) - - @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() - - 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') - 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() - 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 deleted file mode 100644 index fc2d2cf..0000000 --- a/tox.ini +++ /dev/null @@ -1,6 +0,0 @@ -[tox] -envlist = py27,py34 -[testenv] -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