├── .coverage ├── .coveralls.yml ├── tests ├── auth │ ├── __init__.py │ ├── token │ │ ├── __init__.py │ │ └── test_backends.py │ └── user │ │ ├── __init__.py │ │ └── test_utils.py ├── conf │ ├── __init__.py │ └── test_settings.py ├── fixtures │ ├── __init__.py │ ├── example_settings.py │ ├── sqlalchemy.py │ ├── command_line.py │ └── fakes.py ├── storage │ ├── __init__.py │ └── test_storage.py ├── db │ ├── __init__.py │ ├── orm │ │ ├── __init__.py │ │ ├── sqlalchemy │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ └── test_mixins.py │ │ └── django │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ └── test_compat.py │ ├── test_utils.py │ └── test_sqlite_manager.py ├── test │ ├── __init__.py │ └── test_utils.py ├── urls │ ├── __init__.py │ └── test_base.py ├── utils │ ├── __init__.py │ ├── date │ │ ├── __init__.py │ │ ├── test_formatters.py │ │ └── test_humanize_datetime.py │ ├── test_functional.py │ ├── test_text.py │ ├── test_websockets.py │ ├── test_modify.py │ ├── test_validators.py │ ├── test_field_mapping.py │ ├── test_xmlutils.py │ ├── test_encoding.py │ ├── test_fields.py │ └── test_representation.py ├── runtests.py ├── test_exceptions.py ├── keys │ ├── server.csr │ ├── server.crt │ ├── server.pem │ └── server.key ├── test_command_line.py ├── test_websocket_status.py ├── test_endpoints.py ├── test_decorators.py ├── test_parsers.py ├── test_request.py ├── test_views.py └── test_serializers.py ├── aiorest_ws ├── db │ ├── __init__.py │ ├── orm │ │ ├── __init__.py │ │ ├── django │ │ │ ├── __init__.py │ │ │ ├── compat.py │ │ │ └── validators.py │ │ ├── sqlalchemy │ │ │ ├── __init__.py │ │ │ ├── exceptions.py │ │ │ ├── mixins.py │ │ │ └── validators.py │ │ └── exceptions.py │ ├── backends │ │ ├── __init__.py │ │ └── sqlite3 │ │ │ ├── __init__.py │ │ │ ├── constants.py │ │ │ └── managers.py │ └── utils.py ├── auth │ ├── __init__.py │ ├── token │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── utils.py │ │ ├── backends.py │ │ └── middlewares.py │ ├── user │ │ ├── __init__.py │ │ ├── exceptions.py │ │ └── utils.py │ ├── exceptions.py │ └── permissions.py ├── test │ └── __init__.py ├── urls │ ├── __init__.py │ ├── exceptions.py │ ├── base.py │ └── utils.py ├── utils │ ├── __init__.py │ ├── date │ │ ├── __init__.py │ │ └── formatters.py │ ├── formatting.py │ ├── text.py │ ├── functional.py │ ├── validators.py │ ├── modify.py │ ├── structures.py │ ├── websocket.py │ ├── field_mapping.py │ ├── xmlutils.py │ ├── representation.py │ └── encoding.py ├── storages │ ├── __init__.py │ └── backends.py ├── log.py ├── __init__.py ├── endpoints.py ├── command_line.py ├── decorators.py ├── status.py ├── parsers.py ├── abstract.py ├── renderers.py ├── wrappers.py ├── request.py ├── conf │ └── global_settings.py ├── exceptions.py └── views.py ├── examples ├── auth_token │ ├── __init__.py │ ├── settings.py │ ├── client.py │ └── server.py ├── django_orm │ ├── __init__.py │ ├── server │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── app │ │ │ ├── serializers.py │ │ │ ├── __init__.py │ │ │ ├── urls.py │ │ │ ├── db.py │ │ │ └── views.py │ │ ├── run_server.py │ │ ├── server.py │ │ └── create_db.py │ └── client.py ├── sqlalchemy_orm │ ├── __init__.py │ ├── server │ │ ├── __init__.py │ │ ├── app │ │ │ ├── __init__.py │ │ │ ├── urls.py │ │ │ ├── serializers.py │ │ │ ├── db.py │ │ │ └── views.py │ │ ├── settings.py │ │ ├── run_server.py │ │ ├── server.py │ │ └── create_db.py │ └── client.py ├── function_based_api │ ├── server.py │ └── client.py └── method_based_api │ ├── server.py │ ├── client.py │ └── index.html ├── docs └── source │ ├── static │ └── logo.png │ ├── views.rst │ ├── auth.rst │ ├── wrappers.rst │ ├── request.rst │ └── routing.rst ├── setup.cfg ├── Makefile ├── .travis.yml ├── requirements-dev.txt ├── .gitignore ├── LICENSE ├── setup.py ├── AUTHORS └── CONTRIBUTING.md /.coverage: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/auth/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/conf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/auth/token/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/auth/user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/storage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/db/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/db/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/db/orm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/urls/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/auth/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/db/orm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/urls/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/utils/date/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/auth/token/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/auth/user/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/db/backends/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/storages/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /examples/auth_token/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /examples/django_orm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/db/orm/sqlalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/db/orm/django/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /examples/django_orm/server/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /aiorest_ws/db/backends/sqlite3/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /docs/source/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Relrin/aiorest-ws/HEAD/docs/source/static/logo.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | ignore = N801,N802,N803,E203,E226,E301,E309,E402,E731 -------------------------------------------------------------------------------- /aiorest_ws/utils/date/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.utils.date import dateparse, formatters # NOQA 3 | from aiorest_ws.utils.date import humanize_datetime, timezone # NOQA 4 | -------------------------------------------------------------------------------- /aiorest_ws/db/backends/sqlite3/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Constants which can be used for work with SQLite3. 4 | """ 5 | __all__ = ('IN_MEMORY', ) 6 | 7 | IN_MEMORY = ':memory:' 8 | -------------------------------------------------------------------------------- /aiorest_ws/db/orm/sqlalchemy/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.db.orm.sqlalchemy import fields, model_meta # NOQA 3 | from aiorest_ws.db.orm.sqlalchemy import serializers # NOQA 4 | -------------------------------------------------------------------------------- /tests/fixtures/example_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | DATABASES = { 3 | 'default': { 4 | 'backend': 'aiorest_ws.db.backends.sqlite3', 5 | 'name': ':memory:' 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/fixtures/sqlalchemy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | ENGINE = create_engine('sqlite://') 6 | SESSION = sessionmaker(bind=ENGINE) 7 | -------------------------------------------------------------------------------- /examples/auth_token/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # store User and Token tables in the memory 3 | DATABASES = { 4 | 'default': { 5 | 'backend': 'aiorest_ws.db.backends.sqlite3', 6 | 'name': ':memory:' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.orm import sessionmaker 4 | 5 | SQLALCHEMY_ENGINE = create_engine('sqlite://') 6 | SQLALCHEMY_SESSION = sessionmaker(bind=SQLALCHEMY_ENGINE) 7 | -------------------------------------------------------------------------------- /aiorest_ws/utils/formatting.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # JSON serializer options 4 | SHORT_SEPARATORS = (',', ':') 5 | LONG_SEPARATORS = (', ', ': ') 6 | 7 | WRONG_UNICODE_SYMBOLS = [ 8 | ("\u2028", "\\u2028"), 9 | ("\u2029", "\\u2029"), 10 | ] 11 | -------------------------------------------------------------------------------- /examples/django_orm/server/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | USE_ORM_ENGINE = True 4 | DATABASES = { 5 | 'default': { 6 | 'ENGINE': 'django.db.backends.sqlite3', 7 | 'NAME': ':memory:', 8 | } 9 | } 10 | INSTALLED_APPS = ("app", ) 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | flake: 2 | flake8 aiorest_ws tests 3 | 4 | develop: 5 | python setup.py develop 6 | 7 | test: 8 | py.test -q -s --cov aiorest_ws --cov-report term-missing --tb=native 9 | 10 | test_parallel: 11 | py.test -q -s --cov aiorest_ws --cov-report term-missing --tb=native -n4 -------------------------------------------------------------------------------- /aiorest_ws/urls/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Utility exceptions for resolve/reverse URLs purposes. 4 | """ 5 | __all__ = ('NoMatch', 'NoReverseMatch') 6 | 7 | 8 | class NoMatch(Exception): 9 | pass 10 | 11 | 12 | class NoReverseMatch(Exception): 13 | pass 14 | -------------------------------------------------------------------------------- /tests/runtests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import sys 4 | import pytest 5 | 6 | 7 | if __name__ == '__main__': 8 | sys.path.insert(0, os.path.dirname(os.path.abspath('.'))) 9 | pytest.main("-q -s --cov aiorest_ws --cov-report term-missing " 10 | "--tb=native") 11 | -------------------------------------------------------------------------------- /aiorest_ws/utils/text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Special classes and function which help to work with strings correctly. 4 | """ 5 | from aiorest_ws.utils.encoding import force_text 6 | 7 | __all__ = ('capfirst', ) 8 | 9 | 10 | def capfirst(x): 11 | return x and force_text(x)[0].upper() + force_text(x)[1:] 12 | -------------------------------------------------------------------------------- /tests/utils/test_functional.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.utils.functional import cached_property 3 | 4 | 5 | class FakeClass(object): 6 | 7 | @cached_property 8 | def test_property(self): 9 | return True 10 | 11 | 12 | def test_cached_property(): 13 | assert FakeClass().test_property is True 14 | -------------------------------------------------------------------------------- /aiorest_ws/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Logging tool for aiorest-ws framework. 4 | """ 5 | import logging 6 | import logging.config 7 | 8 | from aiorest_ws.conf import settings 9 | 10 | __all__ = ('logger', ) 11 | 12 | logging.config.dictConfig(settings.DEFAULT_LOGGING_SETTINGS) 13 | logger = logging.getLogger('aiorest-ws') 14 | -------------------------------------------------------------------------------- /tests/utils/test_text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from aiorest_ws.utils.text import capfirst 5 | 6 | 7 | @pytest.mark.parametrize("value, expected", [ 8 | (None, None), 9 | ([], []), 10 | ({}, {}), 11 | ('test', 'Test'), 12 | ]) 13 | def test_capfirst(value, expected): 14 | assert capfirst(value) == expected 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.5" 5 | - "3.6" 6 | before_install: 7 | - sudo apt-get update -qq 8 | - sudo apt-get install python-dev 9 | install: 10 | - pip install -r requirements-dev.txt 11 | script: 12 | - export PYTHONPATH=$PYTHONPATH:$(pwd) 13 | - make flake 14 | - make test_parallel 15 | after_success: 16 | - coveralls 17 | -------------------------------------------------------------------------------- /aiorest_ws/db/orm/sqlalchemy/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Exception classes for work with model serializers, fields and SQLAlchemy ORM. 4 | """ 5 | from aiorest_ws.exceptions import BaseAPIException 6 | 7 | 8 | __all__ = ('ObjectDoesNotExist', ) 9 | 10 | 11 | class ObjectDoesNotExist(BaseAPIException): 12 | default_detail = "The requested object does not exist." 13 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | alabaster>=0.7.5 2 | autobahn==0.16.0 3 | cov-core==1.15.0 4 | coverage==4.0.3 5 | Django==1.10.2 6 | flake8==2.5.2 7 | pep8==1.7.0 8 | pudb==2015.4.1 9 | psycopg2==2.6.2 10 | pyflakes==1.0.0 11 | pylint==1.5.4 12 | pytest==2.8.7 13 | pytest-asyncio==0.3.0 14 | pytest-cov==2.4.0 15 | pytest-xdist==1.14 16 | python-coveralls==2.9.0 17 | pytz==2016.4 18 | SQLAlchemy==1.1.2 19 | -------------------------------------------------------------------------------- /tests/db/orm/django/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django import setup 3 | from django.conf import settings 4 | 5 | 6 | if not settings.configured: 7 | settings.configure( 8 | DATABASES={ 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | 'NAME': ':memory:', 12 | } 13 | }, 14 | ) 15 | setup() 16 | -------------------------------------------------------------------------------- /examples/django_orm/server/app/serializers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.db.orm.django import serializers 3 | 4 | from app.db import Manufacturer, Car 5 | 6 | 7 | class ManufacturerSerializer(serializers.ModelSerializer): 8 | 9 | class Meta: 10 | model = Manufacturer 11 | 12 | 13 | class CarSerializer(serializers.ModelSerializer): 14 | 15 | class Meta: 16 | model = Car 17 | -------------------------------------------------------------------------------- /examples/django_orm/server/run_server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | 5 | def get_module_content(filename): 6 | with open(filename, 'r') as module: 7 | return module.read() 8 | 9 | 10 | if __name__ == __name__: 11 | os.environ.setdefault("AIORESTWS_SETTINGS_MODULE", "settings") 12 | exec(get_module_content('./create_db.py')) 13 | exec(get_module_content('./server.py')) 14 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/run_server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | 5 | def get_module_content(filename): 6 | with open(filename, 'r') as module: 7 | return module.read() 8 | 9 | 10 | if __name__ == __name__: 11 | os.environ.setdefault("AIORESTWS_SETTINGS_MODULE", "settings") 12 | exec(get_module_content('./create_db.py')) 13 | exec(get_module_content('./server.py')) 14 | -------------------------------------------------------------------------------- /tests/urls/test_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.urls.base import get_urlconf, set_urlconf, _urlconfs 3 | 4 | 5 | def test_set_urlconf(): 6 | data = {"dict": object()} 7 | set_urlconf(data) 8 | assert "data" in _urlconfs.keys() 9 | assert _urlconfs['data'] == data 10 | 11 | 12 | def test_get_urlconf(): 13 | data = {"list": [1, 2, 3]} 14 | set_urlconf(data) 15 | urlconfs = get_urlconf() 16 | assert urlconfs == data 17 | -------------------------------------------------------------------------------- /examples/django_orm/server/app/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | from os import path 4 | 5 | from django import setup as django_setup 6 | from django.conf import settings as django_settings 7 | 8 | from aiorest_ws.conf import settings 9 | 10 | 11 | sys.path.insert(0, path.abspath(path.join(path.dirname(__file__), '..'))) 12 | django_settings.configure( 13 | DATABASES=settings.DATABASES, 14 | INSTALLED_APPS=settings.INSTALLED_APPS 15 | ) 16 | django_setup() 17 | 18 | -------------------------------------------------------------------------------- /aiorest_ws/auth/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Exception classes for authentication. 4 | """ 5 | from aiorest_ws.exceptions import BaseAPIException 6 | 7 | __all__ = ('BaseAuthException', 'PermissionDeniedException', ) 8 | 9 | 10 | class BaseAuthException(BaseAPIException): 11 | default_detail = u"Error occurred in the authentication process." 12 | 13 | 14 | class PermissionDeniedException(BaseAuthException): 15 | default_detail = u"Permission denied: user doesn't have enough rights." 16 | -------------------------------------------------------------------------------- /aiorest_ws/storages/backends.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Backend classes for storages. 4 | """ 5 | __all__ = ('BaseStorageBackend', ) 6 | 7 | 8 | class BaseStorageBackend(object): 9 | """ 10 | Base interface for storage. 11 | """ 12 | def get(self, *args, **kwargs): 13 | """ 14 | Get object from the storage. 15 | """ 16 | pass 17 | 18 | def save(self, *args, **kwargs): 19 | """ 20 | Save object in the storage. 21 | """ 22 | pass 23 | -------------------------------------------------------------------------------- /tests/storage/test_storage.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from aiorest_ws.storages.backends import BaseStorageBackend 5 | 6 | 7 | class BaseStorageBackendTestCase(unittest.TestCase): 8 | 9 | def setUp(self): 10 | super(BaseStorageBackendTestCase, self).setUp() 11 | self.backend = BaseStorageBackend() 12 | 13 | def test_get(self): 14 | self.assertEqual(self.backend.get(), None) 15 | 16 | def test_save(self): 17 | self.assertEqual(self.backend.save(), None) 18 | -------------------------------------------------------------------------------- /aiorest_ws/db/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Functions for processing data from raw SQL queries. 4 | """ 5 | __all__ = ('convert_db_row_to_dict', ) 6 | 7 | 8 | def convert_db_row_to_dict(row, mapped_fields): 9 | """ 10 | Convert row of database to dictionary. 11 | 12 | :param row: row of database. 13 | :param mapped_fields: list of tuple, which means field names. 14 | """ 15 | data = {} 16 | for field, value in zip(mapped_fields, row): 17 | data[field] = value 18 | return data 19 | -------------------------------------------------------------------------------- /tests/utils/test_websockets.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from autobahn.websocket.compress import PerMessageDeflateOffer, \ 5 | PerMessageDeflateOfferAccept 6 | 7 | from aiorest_ws.utils.websocket import deflate_offer_accept 8 | 9 | 10 | @pytest.mark.parametrize("offers, expected", [ 11 | ([PerMessageDeflateOffer(), ], PerMessageDeflateOfferAccept), 12 | ([None, ], type(None)), 13 | ([], type(None)), 14 | ]) 15 | def test_deflate_offer_accept(offers, expected): 16 | assert type(deflate_offer_accept(offers)) is expected 17 | -------------------------------------------------------------------------------- /examples/django_orm/server/app/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.routers import SimpleRouter 3 | 4 | from app.views import ManufacturerListView, ManufacturerView, CarListView, \ 5 | CarView 6 | 7 | 8 | router = SimpleRouter() 9 | router.register('/manufacturer/{name}', ManufacturerView, ['GET', 'PUT'], 10 | name='manufacturer-detail') 11 | router.register('/manufacturer/', ManufacturerListView, ['POST']) 12 | router.register('/cars/{name}', CarView, ['GET', 'PUT'], 13 | name='car-detail') 14 | router.register('/cars/', CarListView, ['POST']) 15 | -------------------------------------------------------------------------------- /aiorest_ws/utils/functional.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Classes and functions, which might be used for easier development purposes. 4 | """ 5 | __all__ = ('cached_property', ) 6 | 7 | 8 | class cached_property(object): 9 | """ 10 | Decorator that transform a method with a single `self` argument into a 11 | property cached on the instance. 12 | """ 13 | def __init__(self, func): 14 | self.func = func 15 | 16 | def __get__(self, instance, cls=None): 17 | result = instance.__dict__[self.func.__name__] = self.func(instance) 18 | return result 19 | -------------------------------------------------------------------------------- /aiorest_ws/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | _ _ 4 | (_) | | 5 | __ _ _ ___ _ __ ___ ___ | |_ __ __ ___ 6 | / _` || | / _ \ | '__|/ _ \/ __|| __| ____ \ \ /\ / // __| 7 | | (_| || || (_) || | | __/\__ \| |_ |____| \ V V / \__ \ 8 | \___,_||_| \___/ |_| \___||___/\___| \_/\_/ |___/ 9 | """ 10 | __title__ = 'aiorest-ws' 11 | __version__ = '1.1.1' 12 | __author__ = 'Valeryi Savich' 13 | __license__ = 'BSD' 14 | __copyright__ = 'Copyright (c) 2016 by Valeryi Savich' 15 | 16 | VERSION = __version__ 17 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/app/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.routers import SimpleRouter 3 | 4 | from app.views import UserListView, UserView, CreateUserView, AddressView, \ 5 | CreateAddressView 6 | 7 | router = SimpleRouter() 8 | router.register('/user/list', UserListView, 'GET') 9 | router.register('/user/{id}', UserView, ['GET', 'PUT'], name='user-detail') 10 | router.register('/user/', CreateUserView, ['POST']) 11 | router.register('/address/{id}', AddressView, ['GET', 'PUT'], 12 | name='address-detail') 13 | router.register('/address/', CreateAddressView, ['POST']) 14 | -------------------------------------------------------------------------------- /aiorest_ws/urls/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Special module, which provide access to all registered URLs, defined in the 4 | main application. 5 | """ 6 | __all__ = ('set_urlconf', 'get_urlconf') 7 | 8 | _urlconfs = {} 9 | 10 | 11 | def set_urlconf(urlconf_data): 12 | """ 13 | Set the _urlconf for the current thread. 14 | """ 15 | _urlconfs['data'] = urlconf_data 16 | 17 | 18 | def get_urlconf(default=None): 19 | """ 20 | Return the root data from the _urlconf variable, if it has been 21 | changed from the default one. 22 | """ 23 | return _urlconfs.get('data', default) 24 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/app/serializers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from app.db import User, Address 3 | from aiorest_ws.db.orm.sqlalchemy import serializers 4 | 5 | from sqlalchemy.orm import Query 6 | 7 | 8 | class AddressSerializer(serializers.ModelSerializer): 9 | 10 | class Meta: 11 | model = Address 12 | fields = ('id', 'email_address') 13 | 14 | 15 | class UserSerializer(serializers.ModelSerializer): 16 | addresses = serializers.PrimaryKeyRelatedField( 17 | queryset=Query(Address), many=True, required=False, 18 | ) 19 | 20 | class Meta: 21 | model = User 22 | -------------------------------------------------------------------------------- /examples/django_orm/server/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.app import Application 3 | from aiorest_ws.command_line import CommandLine 4 | from aiorest_ws.routers import SimpleRouter 5 | 6 | from app.urls import router 7 | 8 | main_router = SimpleRouter() 9 | main_router.include(router) 10 | 11 | 12 | if __name__ == '__main__': 13 | cmd = CommandLine() 14 | cmd.define('-ip', default='127.0.0.1', help='used ip', type=str) 15 | cmd.define('-port', default=8080, help='listened port', type=int) 16 | args = cmd.parse_command_line() 17 | 18 | app = Application() 19 | app.run(host=args.ip, port=args.port, router=main_router) 20 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.app import Application 3 | from aiorest_ws.command_line import CommandLine 4 | from aiorest_ws.routers import SimpleRouter 5 | 6 | from app.urls import router 7 | 8 | main_router = SimpleRouter() 9 | main_router.include(router) 10 | 11 | 12 | if __name__ == '__main__': 13 | cmd = CommandLine() 14 | cmd.define('-ip', default='127.0.0.1', help='used ip', type=str) 15 | cmd.define('-port', default=8080, help='listened port', type=int) 16 | args = cmd.parse_command_line() 17 | 18 | app = Application() 19 | app.run(host=args.ip, port=args.port, router=main_router) 20 | -------------------------------------------------------------------------------- /aiorest_ws/auth/permissions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Permission classes for authentication. 4 | """ 5 | from aiorest_ws.abstract import AbstractPermission 6 | 7 | __all__ = ('IsAuthenticated', ) 8 | 9 | 10 | class IsAuthenticated(AbstractPermission): 11 | """ 12 | Permissions used for checking authenticated users. 13 | """ 14 | @staticmethod 15 | def check(request, handler): 16 | """ 17 | Check permission method. 18 | 19 | :param request: instance of Request class. 20 | :param handler: view, invoked later for the request. 21 | """ 22 | return request.user and request.user.is_authenticated() 23 | -------------------------------------------------------------------------------- /aiorest_ws/auth/user/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Exceptions for user management. 4 | """ 5 | from aiorest_ws.exceptions import BaseAPIException 6 | 7 | __all__ = ( 8 | 'RequiredModelFieldsNotDefined', 'SearchCriteriaRequired', 9 | 'NotEnoughArguments', 10 | ) 11 | 12 | 13 | class RequiredModelFieldsNotDefined(BaseAPIException): 14 | default_detail = u"Required fields aren't defined." 15 | 16 | 17 | class SearchCriteriaRequired(BaseAPIException): 18 | default_detail = u"Criteria for WHEN statement not defined." 19 | 20 | 21 | class NotEnoughArguments(BaseAPIException): 22 | default_detail = u"Necessary specify at least one field to update." 23 | -------------------------------------------------------------------------------- /tests/db/orm/sqlalchemy/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from tests.fixtures.sqlalchemy import ENGINE 6 | 7 | Base = declarative_base() 8 | 9 | 10 | class SQLAlchemyUnitTest(unittest.TestCase): 11 | 12 | tables = [] 13 | 14 | @classmethod 15 | def setUpClass(cls): 16 | super(SQLAlchemyUnitTest, cls).setUpClass() 17 | Base.metadata.create_all(ENGINE, tables=cls.tables) 18 | 19 | @classmethod 20 | def tearDownClass(cls): 21 | super(SQLAlchemyUnitTest, cls).tearDownClass() 22 | for table in cls.tables: 23 | Base.metadata.remove(table) 24 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from aiorest_ws.exceptions import BaseAPIException 5 | 6 | 7 | class BaseAPIExceptionTestCase(unittest.TestCase): 8 | 9 | def test_init_without_message(self): 10 | exception = BaseAPIException() 11 | self.assertEqual(exception.detail, exception.default_detail) 12 | 13 | def test_init_with_message(self): 14 | exception = BaseAPIException("Some exception.") 15 | self.assertNotEqual(exception.detail, exception.default_detail) 16 | 17 | def test_str_output(self): 18 | exception = BaseAPIException() 19 | self.assertEqual(str(exception), exception.detail) 20 | -------------------------------------------------------------------------------- /aiorest_ws/utils/validators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module contains function, which using at validators.py for type 4 | checking issues. 5 | """ 6 | from inspect import isclass 7 | 8 | __all__ = ('to_str', 'get_object_type', ) 9 | 10 | 11 | def to_str(obj): 12 | """ 13 | Custom convert object to string representation. 14 | """ 15 | if isinstance(obj, (list, tuple)): 16 | string = "/".join([item.__name__ for item in obj]) 17 | else: 18 | string = obj.__name__ 19 | return string 20 | 21 | 22 | def get_object_type(value): 23 | """ 24 | Getting object type. 25 | """ 26 | return type(value) if not isclass(value) else value 27 | -------------------------------------------------------------------------------- /tests/utils/test_modify.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from aiorest_ws.wrappers import Request 5 | from aiorest_ws.utils.modify import add_property 6 | 7 | 8 | def test_add_property(): 9 | request = Request() 10 | add_property(request, 'token', None) 11 | 12 | assert '_token' in request.__dict__.keys() 13 | assert request.token is None 14 | 15 | 16 | def test_attribute_error_for_added_property(): 17 | request = Request() 18 | add_property(request, 'token', None) 19 | 20 | assert '_token' in request.__dict__.keys() 21 | with pytest.raises(AttributeError): 22 | request.token = "test_token" 23 | assert request.token is None 24 | -------------------------------------------------------------------------------- /examples/django_orm/server/app/db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db import models 3 | 4 | 5 | class Manufacturer(models.Model): 6 | name = models.CharField(max_length=30) 7 | 8 | class Meta: 9 | app_label = 'django_orm_example' 10 | 11 | def __str__(self): 12 | return '' % (self.id, self.name) 13 | 14 | 15 | class Car(models.Model): 16 | name = models.CharField(max_length=30, unique=True) 17 | manufacturer = models.ForeignKey(Manufacturer, related_name='cars') 18 | 19 | class Meta: 20 | app_label = 'django_orm_example' 21 | 22 | def __str__(self): 23 | return '' % (self.id, self.name, self.manufacturer) 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | *.manifest 27 | *.spec 28 | 29 | # Installer logs 30 | pip-log.txt 31 | pip-delete-this-directory.txt 32 | 33 | # Unit test / coverage reports 34 | htmlcov/ 35 | .tox/ 36 | .coverage 37 | .coverage.* 38 | .cache 39 | nosetests.xml 40 | coverage.xml 41 | *,cover 42 | .hypothesis/ 43 | 44 | # Translations 45 | *.mo 46 | *.pot 47 | 48 | # Documentation 49 | docs/_build/ -------------------------------------------------------------------------------- /tests/fixtures/command_line.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from aiorest_ws.command_line import CommandLine 5 | 6 | 7 | def clear_command_line(cmd): 8 | default_command_list = ['-h', '--help'] 9 | deleted_commands = [key for key in cmd.options._option_string_actions 10 | if key not in default_command_list] 11 | for key in deleted_commands: 12 | cmd.options._option_string_actions.pop(key) 13 | 14 | 15 | @pytest.fixture(scope="function", autouse=True) 16 | def default_command_line(): 17 | cmd = CommandLine() 18 | clear_command_line(cmd) 19 | cmd.define('-ip', default='127.0.0.1', help='used ip', type=str) 20 | cmd.define('-port', default=8080, help='listened port', type=int) 21 | return cmd 22 | -------------------------------------------------------------------------------- /aiorest_ws/utils/modify.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Functions and decorators used for modify classes on the fly. 4 | """ 5 | __all__ = ('add_property', ) 6 | 7 | 8 | def add_property(instance, field_name, value): 9 | """ 10 | Append property to the current instance of class. 11 | 12 | :param instance: modified object. 13 | :param field_name: attribute name in class. 14 | :param value: value, which used for initialize the field_name. 15 | """ 16 | def get_protected_field(self): 17 | return getattr(self, protected_field_name) 18 | 19 | cls = type(instance) 20 | protected_field_name = '_{0}'.format(field_name) 21 | property_func = get_protected_field 22 | setattr(instance, protected_field_name, value) 23 | setattr(cls, field_name, property(property_func)) 24 | -------------------------------------------------------------------------------- /examples/django_orm/server/create_db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from app.db import Manufacturer, Car 3 | 4 | from django.db import connections 5 | 6 | 7 | if __name__ == '__main__': 8 | # Create tables in memory 9 | conn = connections['default'] 10 | with conn.schema_editor() as editor: 11 | editor.create_model(Manufacturer) 12 | editor.create_model(Car) 13 | 14 | # Initialize the database 15 | data = { 16 | 'Audi': ['A8', 'Q5', 'TT'], 17 | 'BMW': ['M3', 'i8'], 18 | 'Mercedes-Benz': ['C43 AMG W202', 'C450 AMG 4MATIC'] 19 | } 20 | 21 | for name, models in data.items(): 22 | manufacturer = Manufacturer.objects.create(name=name) 23 | 24 | for model in models: 25 | Car.objects.create(name=model, manufacturer=manufacturer) 26 | -------------------------------------------------------------------------------- /tests/utils/date/test_formatters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from datetime import timedelta 5 | from aiorest_ws.utils.date import formatters 6 | 7 | 8 | @pytest.mark.parametrize("value, format, expected", [ 9 | (timedelta(days=1, hours=2, minutes=3, seconds=4), None, 'P1DT2H3M4S'), 10 | (timedelta(hours=1, minutes=10, seconds=20), "alt", 'PT01:10:20'), 11 | ]) 12 | def test_iso8601_repr(value, format, expected): 13 | assert formatters.iso8601_repr(value, format=format) == expected 14 | 15 | 16 | @pytest.mark.parametrize("value, format, exc_type", [ 17 | (timedelta(days=1, hours=2, minutes=3, seconds=4), "alt", ValueError), 18 | ]) 19 | def test_iso8601_repr_raises_exception(value, format, exc_type): 20 | with pytest.raises(exc_type): 21 | formatters.iso8601_repr(value, format=format) 22 | -------------------------------------------------------------------------------- /tests/utils/test_validators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from aiorest_ws.routers import SimpleRouter 5 | from aiorest_ws.endpoints import PlainEndpoint, DynamicEndpoint 6 | from aiorest_ws.utils.validators import to_str, get_object_type 7 | 8 | 9 | @pytest.mark.parametrize("value, expected", [ 10 | (PlainEndpoint, 'PlainEndpoint'), 11 | ((PlainEndpoint, ), 'PlainEndpoint'), 12 | ((PlainEndpoint, DynamicEndpoint), 'PlainEndpoint/DynamicEndpoint'), 13 | ]) 14 | def test_to_str(value, expected): 15 | assert to_str(value) == expected 16 | 17 | 18 | @pytest.mark.parametrize("value, expected", [ 19 | (PlainEndpoint, PlainEndpoint), 20 | (SimpleRouter(), SimpleRouter), 21 | (list, list), 22 | ]) 23 | def test_get_object_type(value, expected): 24 | assert get_object_type(value) is expected 25 | -------------------------------------------------------------------------------- /aiorest_ws/utils/structures.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module provide structures for data representation of received model. 4 | """ 5 | from collections import namedtuple 6 | 7 | __all__ = ('FieldInfo', 'RelationInfo') 8 | 9 | 10 | FieldInfo = namedtuple('FieldResult', [ 11 | 'pk', # Model field instance 12 | 'fields', # Dict of field name -> model field instance 13 | 'forward_relations', # Dict of field name -> RelationInfo 14 | 'reverse_relations', # Dict of field name -> RelationInfo 15 | 'fields_and_pk', # Shortcut for 'pk' + 'fields' 16 | 'relations' # Shortcut for 'forward_relations' + 'reverse_relations' 17 | ]) 18 | 19 | RelationInfo = namedtuple('RelationInfo', [ 20 | 'model_field', 21 | 'related_model', 22 | 'to_many', 23 | 'to_field', 24 | 'has_through_model' 25 | ]) 26 | -------------------------------------------------------------------------------- /tests/db/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from aiorest_ws.db.utils import convert_db_row_to_dict 5 | 6 | 7 | @pytest.mark.parametrize("row, mapped_fields", [ 8 | (('some_token', None), ('token', 'value')), 9 | ((), ()), 10 | ]) 11 | def test_convert_db_row_to_dict(row, mapped_fields): 12 | data = convert_db_row_to_dict(row, mapped_fields) 13 | 14 | assert set(row) == set(data.values()) 15 | assert set(mapped_fields) == set(data.keys()) 16 | 17 | 18 | @pytest.mark.parametrize("row, mapped_fields", [ 19 | (('value', None), ()), 20 | ((), ('field_1', 'field_2')) 21 | ]) 22 | def test_convert_db_row_to_dict_with_unfilled_data(row, mapped_fields): 23 | data = convert_db_row_to_dict(row, mapped_fields) 24 | 25 | assert set([]) == set(data.values()) 26 | assert set([]) == set(data.keys()) 27 | -------------------------------------------------------------------------------- /aiorest_ws/utils/websocket.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module contains classes and functions, used for configuration 4 | issues with websockets. 5 | """ 6 | from autobahn.websocket.compress import PerMessageDeflateOffer, \ 7 | PerMessageDeflateOfferAccept 8 | 9 | __all__ = ('deflate_offer_accept', ) 10 | 11 | 12 | def deflate_offer_accept(offers): 13 | """ 14 | Function to accept offers from the client. 15 | NOTE: For using this function you will need a "permessage-deflate" 16 | compression extension for WebSocket connections. 17 | 18 | :param offers: iterable object (list, tuple), where every object 19 | is instance of PerMessageDeflateOffer. 20 | """ 21 | for offer in offers: 22 | if isinstance(offer, PerMessageDeflateOffer): 23 | return PerMessageDeflateOfferAccept(offer) 24 | -------------------------------------------------------------------------------- /aiorest_ws/db/orm/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Base exception classes for serialize and validation actions. 4 | """ 5 | from aiorest_ws.exceptions import BaseAPIException, SerializerError 6 | from aiorest_ws.utils.encoding import force_text_recursive 7 | 8 | __all__ = ('ModelSerializerError', 'ValidationError', ) 9 | 10 | 11 | class ModelSerializerError(SerializerError): 12 | default_detail = u"Error has occurred inside model serializer class." 13 | 14 | 15 | class ValidationError(BaseAPIException): 16 | default_detail = u"Validation error has occurred at validation process." 17 | 18 | def __init__(self, detail): 19 | if not isinstance(detail, dict) and not isinstance(detail, list): 20 | detail = [detail, ] 21 | self.detail = force_text_recursive(detail) 22 | 23 | def __str__(self): 24 | return str(self.detail) 25 | -------------------------------------------------------------------------------- /tests/db/orm/django/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from django.apps import apps 5 | from django.db import connections 6 | 7 | 8 | class DjangoUnitTest(unittest.TestCase): 9 | 10 | models = [] 11 | apps = () 12 | 13 | @classmethod 14 | def setUpClass(cls): 15 | super(DjangoUnitTest, cls).setUpClass() 16 | if apps: 17 | apps.populate(cls.apps) 18 | 19 | conn = connections['default'] 20 | with conn.schema_editor() as editor: 21 | for model in cls.models: 22 | editor.create_model(model) 23 | 24 | @classmethod 25 | def tearDownClass(cls): 26 | super(DjangoUnitTest, cls).tearDownClass() 27 | 28 | conn = connections['default'] 29 | with conn.schema_editor() as editor: 30 | for model in cls.models: 31 | editor.delete_model(model) 32 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/create_db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from app.db import Base, User, Address 3 | from settings import SQLALCHEMY_ENGINE, SQLALCHEMY_SESSION 4 | 5 | 6 | if __name__ == '__main__': 7 | Base.metadata.create_all(SQLALCHEMY_ENGINE) 8 | session = SQLALCHEMY_SESSION() 9 | 10 | session.add_all([ 11 | User(name='ed', fullname='Ed Jones', password='edspassword'), 12 | User(name='wendy', fullname='Wendy Williams', password='foobar'), 13 | User(name='mary', fullname='Mary Contrary', password='xxg527'), 14 | User(name='fred', fullname='Fred Flinstone', password='blah') 15 | ]) 16 | 17 | jack = User(name='jack', fullname='Jack Bean', password='gjffdd') 18 | jack.addresses = [ 19 | Address(email_address='jack@google.com'), 20 | Address(email_address='j25@yahoo.com') 21 | ] 22 | session.add(jack) 23 | session.commit() 24 | -------------------------------------------------------------------------------- /tests/keys/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICYjCCAUoCAQAwHTELMAkGA1UEBhMCQlkxDjAMBgNVBAMTBVRlc3RzMIIBIjAN 3 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8izgBQMe6HVb2fjSZfk+pqT/Qu1P 4 | L+atodZsgtlVkMo/nB8cPf9yZn2NVROxZagM1L+9ajpIDkZ+PP6uyYcMRGCb0EXy 5 | yLxRiOAqLdeauCPucj31aRxNHUEPPDN35McmNPkwdSqU79afv7HiIoxNE4CeSS/s 6 | Dsn+uuoNAtodrdWpwpAUJSm8HiQJRZ5CL+5WnslL+gKnE7wLGi4i6pfFSKmCkSBt 7 | Vr/80J0ijuyADwfHQvwqSiXmwCoBBcpJX5fC0DqSeaIVMQ4LOV9haYHQSv4W93/H 8 | loo08EDo4qT3/1cHoR2XDhAYcyCzJkc7+LEhi2eK/kFISHtEOgbp+gm61wIDAQAB 9 | oAAwDQYJKoZIhvcNAQEFBQADggEBANzr+yWMB8NHfHZAiO7zjIHTtt5e+1F+tBq2 10 | qDjgPnmP69eGck0GYaoEWxTQWx8c+bkq+IGOFaDVgpipT4uueB9RpxPa/6tvugWS 11 | L85anJtPuXL8PyKkSIT4rjsttUeCB86w+X+RUL/m8zSMbwSzDnc+NI5abbcAh7jh 12 | ccm9b/yCtnDvod/srZRFkSN27GKTSilbgdzzDRy5n9fqXKHKITdLKAuGSJN5jTE3 13 | HL4ZcFWKovHFLVK6LYkPZ6X/5QPeY2mytcb/GDTbeq0U/XFB9Pakm92MSR9IoXgq 14 | rS6o9yvFeb4K5aOnl4VsL3vpU/VVldwBPvYqKKjCUtNlfvJG7Gw= 15 | -----END CERTIFICATE REQUEST----- 16 | -------------------------------------------------------------------------------- /tests/fixtures/fakes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.abstract import AbstractEndpoint 3 | from aiorest_ws.exceptions import BaseAPIException 4 | from aiorest_ws.views import MethodBasedView 5 | 6 | 7 | class InvalidEndpoint(object): 8 | pass 9 | 10 | 11 | class FakeEndpoint(AbstractEndpoint): 12 | 13 | def match(self, path): 14 | pass 15 | 16 | 17 | class FakeView(MethodBasedView): 18 | 19 | def get(self, request, *args, **kwargs): 20 | pass 21 | 22 | 23 | class FakeGetView(MethodBasedView): 24 | 25 | def get(self, request, *args, **kwargs): 26 | return "fake" 27 | 28 | 29 | class FakeTokenMiddleware(object): 30 | 31 | def process_request(self, request, handler): 32 | setattr(request, 'token', None) 33 | return request 34 | 35 | 36 | class FakeTokenMiddlewareWithExc(object): 37 | 38 | def process_request(self, request, handler): 39 | raise BaseAPIException('No token provided') 40 | -------------------------------------------------------------------------------- /tests/conf/test_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import unittest 4 | 5 | from aiorest_ws.db.backends.sqlite3.managers import SQLiteManager 6 | from aiorest_ws.conf import Settings, ENVIRONMENT_VARIABLE 7 | 8 | 9 | class SettingsTestCase(unittest.TestCase): 10 | 11 | def test_base_settings(self): 12 | settings = Settings() 13 | default_settings = [ 14 | 'DATABASES', 'DEFAULT_ENCODING', 'DEFAULT_LOGGING_SETTINGS', 15 | 'ISO_8601', 'MIDDLEWARE_CLASSES' 16 | ] 17 | 18 | for setting_name in default_settings: 19 | self.assertTrue(hasattr(settings, setting_name)) 20 | 21 | def test_setup_manager(self): 22 | os.environ[ENVIRONMENT_VARIABLE] = "tests.fixtures.example_settings" 23 | settings = Settings() 24 | 25 | self.assertTrue(hasattr(settings, 'DATABASES')) 26 | self.assertIsInstance(settings.DATABASES['default']['manager'], 27 | SQLiteManager) 28 | -------------------------------------------------------------------------------- /tests/keys/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICtjCCAZ4CCQCs43kujbgPfTANBgkqhkiG9w0BAQUFADAdMQswCQYDVQQGEwJC 3 | WTEOMAwGA1UEAxMFVGVzdHMwHhcNMTUwODEzMTMzMjA3WhcNMjUwODEwMTMzMjA3 4 | WjAdMQswCQYDVQQGEwJCWTEOMAwGA1UEAxMFVGVzdHMwggEiMA0GCSqGSIb3DQEB 5 | AQUAA4IBDwAwggEKAoIBAQDyLOAFAx7odVvZ+NJl+T6mpP9C7U8v5q2h1myC2VWQ 6 | yj+cHxw9/3JmfY1VE7FlqAzUv71qOkgORn48/q7JhwxEYJvQRfLIvFGI4Cot15q4 7 | I+5yPfVpHE0dQQ88M3fkxyY0+TB1KpTv1p+/seIijE0TgJ5JL+wOyf666g0C2h2t 8 | 1anCkBQlKbweJAlFnkIv7laeyUv6AqcTvAsaLiLql8VIqYKRIG1Wv/zQnSKO7IAP 9 | B8dC/CpKJebAKgEFyklfl8LQOpJ5ohUxDgs5X2FpgdBK/hb3f8eWijTwQOjipPf/ 10 | VwehHZcOEBhzILMmRzv4sSGLZ4r+QUhIe0Q6Bun6CbrXAgMBAAEwDQYJKoZIhvcN 11 | AQEFBQADggEBAJewg/h+LWkUZKSaRFRhoTdl82SZzGahUsjD/FaPIsYfEadfDnR6 12 | /6DgX8FWLtaNimYg79mVyOFGfNGNAD+ZoD1+D6aMA8wINCrNOYw43GS2hb2rtGbh 13 | E1xKzH9mp8zzcnZXd82uZPIjAeYP3YhWyqPdF4XZkdD/ZPQkU0NRMWxKfBOH6USs 14 | JTBwM20MgSGnMuGlGdq4CuR6zU66Fdr1oLYDGgLuZZZDgA0Qjxfwa/uIwdvGOG0+ 15 | DgaMEE0jIYh/SkVK/yYhGBVzmqsafq6mFFYi+R30e5R3M5lcw4OIwVpD+mAH9KWn 16 | trvIOjXCTJSoyRv8/4d/SykwcUALQl1up14= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /tests/keys/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICtjCCAZ4CCQCs43kujbgPfTANBgkqhkiG9w0BAQUFADAdMQswCQYDVQQGEwJC 3 | WTEOMAwGA1UEAxMFVGVzdHMwHhcNMTUwODEzMTMzMjA3WhcNMjUwODEwMTMzMjA3 4 | WjAdMQswCQYDVQQGEwJCWTEOMAwGA1UEAxMFVGVzdHMwggEiMA0GCSqGSIb3DQEB 5 | AQUAA4IBDwAwggEKAoIBAQDyLOAFAx7odVvZ+NJl+T6mpP9C7U8v5q2h1myC2VWQ 6 | yj+cHxw9/3JmfY1VE7FlqAzUv71qOkgORn48/q7JhwxEYJvQRfLIvFGI4Cot15q4 7 | I+5yPfVpHE0dQQ88M3fkxyY0+TB1KpTv1p+/seIijE0TgJ5JL+wOyf666g0C2h2t 8 | 1anCkBQlKbweJAlFnkIv7laeyUv6AqcTvAsaLiLql8VIqYKRIG1Wv/zQnSKO7IAP 9 | B8dC/CpKJebAKgEFyklfl8LQOpJ5ohUxDgs5X2FpgdBK/hb3f8eWijTwQOjipPf/ 10 | VwehHZcOEBhzILMmRzv4sSGLZ4r+QUhIe0Q6Bun6CbrXAgMBAAEwDQYJKoZIhvcN 11 | AQEFBQADggEBAJewg/h+LWkUZKSaRFRhoTdl82SZzGahUsjD/FaPIsYfEadfDnR6 12 | /6DgX8FWLtaNimYg79mVyOFGfNGNAD+ZoD1+D6aMA8wINCrNOYw43GS2hb2rtGbh 13 | E1xKzH9mp8zzcnZXd82uZPIjAeYP3YhWyqPdF4XZkdD/ZPQkU0NRMWxKfBOH6USs 14 | JTBwM20MgSGnMuGlGdq4CuR6zU66Fdr1oLYDGgLuZZZDgA0Qjxfwa/uIwdvGOG0+ 15 | DgaMEE0jIYh/SkVK/yYhGBVzmqsafq6mFFYi+R30e5R3M5lcw4OIwVpD+mAH9KWn 16 | trvIOjXCTJSoyRv8/4d/SykwcUALQl1up14= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /aiorest_ws/auth/token/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Exception classes for token authentication. 4 | """ 5 | from aiorest_ws.exceptions import BaseAPIException 6 | 7 | __all__ = ( 8 | 'BaseTokenException', 'ParsingTokenException', 'InvalidSignatureException', 9 | 'TokenNotBeforeException', 'TokenExpiredException', 10 | 'TokenNotProvidedException', 11 | ) 12 | 13 | 14 | class BaseTokenException(BaseAPIException): 15 | default_detail = u"Occurred error, when aiorest-ws processing token." 16 | 17 | 18 | class ParsingTokenException(BaseTokenException): 19 | default_detail = u"Can't parse token: mismatch format." 20 | 21 | 22 | class InvalidSignatureException(BaseTokenException): 23 | default_detail = u"Token signature is invalid." 24 | 25 | 26 | class TokenNotBeforeException(BaseTokenException): 27 | default_detail = u"Time after which the token not be accepted has passed." 28 | 29 | 30 | class TokenExpiredException(BaseTokenException): 31 | default_detail = u"Token has expired." 32 | 33 | 34 | class TokenNotProvidedException(BaseTokenException): 35 | default_detail = u"Token has not provided in the request." 36 | -------------------------------------------------------------------------------- /tests/test_command_line.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from fixtures.command_line import default_command_line as cmd # noqa 3 | 4 | 5 | def test_define(cmd): # noqa 6 | assert len(cmd.options._option_string_actions.keys()) == 4 7 | 8 | 9 | def test_define_add_args(cmd): # noqa 10 | cmd.define('-arg1', default='some value', help='arg #1', type=str) 11 | cmd.define('-arg2', default=8888, help='arg #2', type=int) 12 | assert len(cmd.options._option_string_actions.keys()) == 6 13 | 14 | 15 | def test_define_ignore_duplicates(cmd): # noqa 16 | cmd.define('-arg1', default='some value', help='arg #1', type=str) 17 | cmd.define('-arg2', default=8888, help='arg #2', type=int) 18 | assert len(cmd.options._option_string_actions.keys()) == 6 19 | 20 | # try to add one more time the same argument into parser 21 | cmd.define('-arg2', default=8888, help='arg #2', type=int) 22 | assert len(cmd.options._option_string_actions.keys()) == 6 23 | 24 | 25 | def test_define_options(cmd): # noqa 26 | cmd.define('--opt1', help='arg #1', type=int) 27 | cmd.define('--opt2', help='arg #2', type=str) 28 | assert len(cmd.options._option_string_actions.keys()) == 6 29 | -------------------------------------------------------------------------------- /aiorest_ws/auth/token/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Functions and constants, which can be used for work with Token models. 4 | """ 5 | 6 | SQL_CREATE_TOKEN_TABLE = """ 7 | CREATE TABLE IF NOT EXISTS aiorest_auth_token 8 | (id INTEGER PRIMARY KEY NOT NULL, 9 | name CHAR(64) NOT NULL, -- name of key (admin, api, etc.) 10 | token TEXT NOT NULL, 11 | created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, 12 | expired DATETIME DEFAULT NULL -- for some tokens it doesn't necessary 13 | ); 14 | """ 15 | SQL_TOKEN_GET = """ 16 | SELECT `id`, `name`, `token`, `created`, `expired`, `user_id` 17 | FROM aiorest_auth_token 18 | WHERE token=?; 19 | """ 20 | SQL_TOKEN_GET_BY_TOKEN_USERNAME = """ 21 | SELECT aiorest_auth_token.id, `name`, `token`, `created`, 22 | `expired`, `user_id` 23 | FROM aiorest_auth_token 24 | INNER JOIN aiorest_auth_user 25 | ON aiorest_auth_token.user_id=aiorest_auth_user.id 26 | WHERE name=? AND username=?; 27 | """ 28 | SQL_TOKEN_ADD = """ 29 | INSERT INTO aiorest_auth_token (`name`, `token`, `expired`, `user_id`) 30 | VALUES (?, ?, ?, ?); 31 | """ 32 | TOKEN_MODEL_FIELDS = ('id', 'name', 'token', 'created', 'expired', 'user_id') 33 | -------------------------------------------------------------------------------- /tests/test_websocket_status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from aiorest_ws.status import is_not_used, is_reserved, is_library, is_private 5 | 6 | 7 | @pytest.mark.parametrize("code, expected", [ 8 | (0, True), 9 | (999, True), 10 | (500, True), 11 | (1000, False), 12 | (-1, False), 13 | ]) 14 | def test_is_not_used(code, expected): 15 | assert is_not_used(code) is expected 16 | 17 | 18 | @pytest.mark.parametrize("code, expected", [ 19 | (1000, True), 20 | (2999, True), 21 | (2000, True), 22 | (3000, False), 23 | (999, False), 24 | ]) 25 | def test_is_reserved(code, expected): 26 | assert is_reserved(code) is expected 27 | 28 | 29 | @pytest.mark.parametrize("code, expected", [ 30 | (3000, True), 31 | (3999, True), 32 | (3500, True), 33 | (4000, False), 34 | (2999, False), 35 | ]) 36 | def test_is_library(code, expected): 37 | assert is_library(code) is expected 38 | 39 | 40 | @pytest.mark.parametrize("code, expected", [ 41 | (4000, True), 42 | (4999, True), 43 | (4500, True), 44 | (3999, False), 45 | (5000, False), 46 | ]) 47 | def test_is_private(code, expected): 48 | assert is_private(code) is expected 49 | -------------------------------------------------------------------------------- /aiorest_ws/endpoints.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Endpoint classes for aiorest-ws router. 4 | """ 5 | from aiorest_ws.abstract import AbstractEndpoint 6 | 7 | __all__ = ('PlainEndpoint', 'DynamicEndpoint', ) 8 | 9 | 10 | class PlainEndpoint(AbstractEndpoint): 11 | 12 | def match(self, path): 13 | """ 14 | Checking path on compatible. 15 | 16 | :param path: URL, which used for get access to API. 17 | """ 18 | match_result = None 19 | if self.path == path: 20 | match_result = () 21 | return match_result 22 | 23 | 24 | class DynamicEndpoint(AbstractEndpoint): 25 | 26 | def __init__(self, path, methods, handler, name, pattern): 27 | super(DynamicEndpoint, self).__init__(path, methods, handler, name) 28 | self._pattern = pattern 29 | 30 | def match(self, path): 31 | """ 32 | Checking path on compatible. 33 | 34 | :param path: URL, which used for get access to API. 35 | """ 36 | match_result = self._pattern.match(path) 37 | # If comparing has successful, then return list of parsed values 38 | if match_result: 39 | match_result = match_result.groups() 40 | return match_result 41 | -------------------------------------------------------------------------------- /tests/utils/test_field_mapping.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import unittest 4 | 5 | from aiorest_ws.utils.field_mapping import ClassLookupDict, needs_label 6 | 7 | 8 | class TestClassLookupDict(unittest.TestCase): 9 | 10 | @classmethod 11 | def setUpClass(cls): 12 | cls.instance = ClassLookupDict({str: str}) 13 | 14 | def test_get_item(self): 15 | self.assertEqual(self.instance["string"], str) 16 | 17 | def test_get_item_for_class_with_proxy(self): 18 | class ClassWithProxy(object): 19 | _proxy_class = str 20 | 21 | self.assertEqual(self.instance[ClassWithProxy], str) 22 | 23 | def test_get_item_has_failed(self): 24 | self.assertRaises(KeyError, self.instance.__getitem__, dict) 25 | 26 | def test_set_item(self): 27 | self.instance[float] = float 28 | self.assertEqual(self.instance.mapping[float], float) 29 | 30 | 31 | @pytest.mark.parametrize("model_field_verbose_name, field_name, expected", [ 32 | ('test_label', 'test label', True), 33 | ('test_label', 'custom_label', True), 34 | ('no need label', 'no_need_label', False), 35 | ]) 36 | def test_needs_label(model_field_verbose_name, field_name, expected): 37 | assert needs_label(model_field_verbose_name, field_name) == expected 38 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/app/db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from sqlalchemy.ext.declarative import declarative_base 3 | from sqlalchemy import Column, Integer, String, ForeignKey 4 | from sqlalchemy.orm import relationship, validates 5 | 6 | Base = declarative_base() 7 | 8 | 9 | class User(Base): 10 | __tablename__ = 'users' 11 | id = Column(Integer, primary_key=True) 12 | name = Column(String(50), unique=True) 13 | fullname = Column(String(50), default='Unknown') 14 | password = Column(String(512)) 15 | addresses = relationship("Address", back_populates="user") 16 | 17 | @validates('name') 18 | def validate_name(self, key, name): 19 | assert '@' not in name 20 | return name 21 | 22 | def __repr__(self): 23 | return "" % ( 24 | self.name, self.fullname, self.password 25 | ) 26 | 27 | 28 | class Address(Base): 29 | __tablename__ = 'addresses' 30 | id = Column(Integer, primary_key=True) 31 | email_address = Column(String, nullable=False) 32 | user_id = Column(Integer, ForeignKey('users.id')) 33 | user = relationship("User", back_populates="addresses") 34 | 35 | def __repr__(self): 36 | return "" % self.email_address 37 | -------------------------------------------------------------------------------- /examples/function_based_api/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.app import Application 3 | from aiorest_ws.command_line import CommandLine 4 | from aiorest_ws.routers import SimpleRouter 5 | from aiorest_ws.decorators import endpoint 6 | 7 | 8 | @endpoint(path='/hello', methods='GET') 9 | def hello_world(request, *args, **kwargs): 10 | return "Hello, world!" 11 | 12 | 13 | @endpoint(path='/sum/{digit_1}/{digit_2}', methods='GET') 14 | def summ(request, digit_1, digit_2, *args, **kwargs): 15 | 16 | def convert_to_int(digit): 17 | try: 18 | digit = int(digit) 19 | except ValueError: 20 | digit = 0 21 | return digit 22 | 23 | digit_1 = convert_to_int(digit_1) 24 | digit_2 = convert_to_int(digit_2) 25 | return "{0} + {1} = {2}".format(digit_1, digit_2, digit_1 + digit_2) 26 | 27 | 28 | router = SimpleRouter() 29 | router.register_endpoint(hello_world) 30 | router.register_endpoint(summ) 31 | 32 | 33 | if __name__ == '__main__': 34 | cmd = CommandLine() 35 | cmd.define('-ip', default='127.0.0.1', help='used ip', type=str) 36 | cmd.define('-port', default=8080, help='listened port', type=int) 37 | args = cmd.parse_command_line() 38 | 39 | app = Application() 40 | app.run(ip=args.ip, port=args.port, router=router) 41 | -------------------------------------------------------------------------------- /aiorest_ws/command_line.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Custom functions and classes, which help working with the command line. 4 | """ 5 | from argparse import ArgumentParser 6 | 7 | __all__ = ('CommandLine', ) 8 | 9 | 10 | class CommandLine(object): 11 | """ 12 | Wrapper over ArgumentParser class for working with command line. 13 | """ 14 | options = ArgumentParser() 15 | 16 | def define(self, name, default=None, help=None, type=None): 17 | """ 18 | Defines an option in the global namespace. 19 | 20 | Note: already defined argument or option has been ignored and not 21 | appended again! 22 | 23 | :param name: used argument/option in command line (e.c. -f or --foo). 24 | :param default: used value for argument or option if not specified. 25 | :param help: full description for option, which used when invoked 26 | program with -h flag. 27 | :param type: preferred type for option. 28 | """ 29 | if name not in self.options._option_string_actions.keys(): 30 | self.options.add_argument(name, default=default, help=help, 31 | type=type) 32 | 33 | def parse_command_line(self): 34 | """ 35 | Parse options from the command line. 36 | """ 37 | return self.options.parse_args() 38 | -------------------------------------------------------------------------------- /examples/method_based_api/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.app import Application 3 | from aiorest_ws.command_line import CommandLine 4 | from aiorest_ws.routers import SimpleRouter 5 | from aiorest_ws.views import MethodBasedView 6 | 7 | 8 | class HelloWorld(MethodBasedView): 9 | 10 | def get(self, request, *args, **kwargs): 11 | return "Hello, username!" 12 | 13 | 14 | class HelloWorldCustom(MethodBasedView): 15 | 16 | def get(self, request, user, id, *args, **kwargs): 17 | return "Hello, {0} with ID={1}".format(user, id) 18 | 19 | 20 | class CalculateSum(MethodBasedView): 21 | 22 | def get(self, request, *args, **kwargs): 23 | try: 24 | digits = kwargs['digits'] 25 | except KeyError: 26 | digits = [0, ] 27 | return {"sum": sum(digits)} 28 | 29 | 30 | router = SimpleRouter() 31 | router.register('/hello', HelloWorld, 'GET') 32 | router.register('/hello/{user}/{id}', HelloWorldCustom, 'GET') 33 | router.register('/calc/sum', CalculateSum, 'GET') 34 | 35 | 36 | if __name__ == '__main__': 37 | cmd = CommandLine() 38 | cmd.define('-ip', default='127.0.0.1', help='used ip', type=str) 39 | cmd.define('-port', default=8080, help='listened port', type=int) 40 | args = cmd.parse_command_line() 41 | 42 | app = Application() 43 | app.run(host=args.ip, port=args.port, router=router) 44 | -------------------------------------------------------------------------------- /tests/test_endpoints.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf -*- 2 | import re 3 | import unittest 4 | 5 | from aiorest_ws.endpoints import PlainEndpoint, DynamicEndpoint 6 | from aiorest_ws.views import MethodBasedView 7 | 8 | 9 | class PlainEndpointTestCase(unittest.TestCase): 10 | 11 | def setUp(self): 12 | super(PlainEndpointTestCase, self).setUp() 13 | self.endpoint = PlainEndpoint('/api/', MethodBasedView, 'GET', None) 14 | 15 | def test_matched_path(self): 16 | matched_path = '/api/' 17 | self.assertEqual(self.endpoint.match(matched_path), ()) 18 | 19 | def test_unmatched_path(self): 20 | unmatched_path = '/api/another' 21 | self.assertEqual(self.endpoint.match(unmatched_path), None) 22 | 23 | 24 | class DynamicEndpointTestCase(unittest.TestCase): 25 | 26 | def setUp(self): 27 | super(DynamicEndpointTestCase, self).setUp() 28 | self.endpoint = DynamicEndpoint( 29 | '/api/{another}/', MethodBasedView, 'GET', None, 30 | re.compile("^{}$".format(r'/api/(?P[^{}/]+)/')) 31 | ) 32 | 33 | def test_matched_path(self): 34 | unmatched_path = '/api/another/' 35 | self.assertEqual(self.endpoint.match(unmatched_path), ('another', )) 36 | 37 | def test_unmatched_path(self): 38 | unmatched_path = '/api/another/value' 39 | self.assertEqual(self.endpoint.match(unmatched_path), None) 40 | -------------------------------------------------------------------------------- /examples/function_based_api/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import json 4 | 5 | from random import randint 6 | from autobahn.asyncio.websocket import WebSocketClientProtocol, \ 7 | WebSocketClientFactory 8 | 9 | 10 | class HelloClientProtocol(WebSocketClientProtocol): 11 | 12 | def onOpen(self): 13 | # hello username 14 | request = { 15 | 'method': 'GET', 16 | 'url': '/hello/' 17 | } 18 | self.sendMessage(json.dumps(request).encode('utf8')) 19 | 20 | # sum endpoint with arg in URL path (two random digits) 21 | for _ in range(0, 10): 22 | digit_1 = str(randint(1, 100)) 23 | digit_2 = str(randint(1, 100)) 24 | request = { 25 | 'method': 'GET', 26 | 'url': '/sum/{0}/{1}'.format(digit_1, digit_2) 27 | } 28 | self.sendMessage(json.dumps(request).encode('utf8')) 29 | 30 | def onMessage(self, payload, isBinary): 31 | print("Result: {0}".format(payload.decode('utf8'))) 32 | 33 | 34 | if __name__ == '__main__': 35 | factory = WebSocketClientFactory("ws://localhost:8080") 36 | factory.protocol = HelloClientProtocol 37 | 38 | loop = asyncio.get_event_loop() 39 | coro = loop.create_connection(factory, '127.0.0.1', 8080) 40 | loop.run_until_complete(coro) 41 | loop.run_forever() 42 | loop.close() 43 | -------------------------------------------------------------------------------- /aiorest_ws/utils/date/formatters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __all__ = ('iso8601_repr', ) 3 | 4 | 5 | def iso8601_repr(timedelta, format=None): 6 | """ 7 | Represent a timedelta as an ISO8601 duration. 8 | http://en.wikipedia.org/wiki/ISO_8601#Durations 9 | """ 10 | years = int(timedelta.days / 365) 11 | weeks = int((timedelta.days % 365) / 7) 12 | days = timedelta.days % 7 13 | 14 | hours = int(timedelta.seconds / 3600) 15 | minutes = int((timedelta.seconds % 3600) / 60) 16 | seconds = timedelta.seconds % 60 17 | 18 | if format == 'alt': 19 | if years or weeks or days: 20 | raise ValueError('Does not support `alt` format for durations ' 21 | 'more then 1 day') 22 | return 'PT{0:02d}:{1:02d}:{2:02d}'.format(hours, minutes, seconds) 23 | 24 | formatting = ( 25 | ('P', ( 26 | ('Y', years), 27 | ('W', weeks), 28 | ('D', days), 29 | )), 30 | ('T', ( 31 | ('H', hours), 32 | ('M', minutes), 33 | ('S', seconds), 34 | )), 35 | ) 36 | 37 | result = [] 38 | for category, subcats in formatting: 39 | result += category 40 | for format, value in subcats: 41 | if value: 42 | result.append('%d%c' % (value, format)) 43 | if result[-1] == 'T': 44 | result = result[:-1] 45 | 46 | return "".join(result) 47 | -------------------------------------------------------------------------------- /tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from aiorest_ws.decorators import endpoint 5 | from aiorest_ws.exceptions import NotSupportedArgumentType 6 | from aiorest_ws.renderers import JSONRenderer 7 | 8 | 9 | class EndpointTestCase(unittest.TestCase): 10 | 11 | def test_create_handler_with_str_method_name(self): 12 | @endpoint('/api', 'GET') 13 | def fake_handler(request, *args, **kwargs): 14 | pass 15 | 16 | path, handler, methods, name = fake_handler() 17 | self.assertEqual(methods, 'GET') 18 | 19 | def test_create_handler_with_list_method_name(self): 20 | @endpoint('/api', ['GET', 'post']) 21 | def fake_handler(request, *args, **kwargs): 22 | pass 23 | 24 | path, handler, methods, name = fake_handler() 25 | self.assertEqual(methods, ['GET', 'post']) 26 | 27 | def test_set_attr_for_endpoint(self): 28 | @endpoint('/api', ['GET', 'post'], renderers=(JSONRenderer, )) 29 | def fake_handler(request, *args, **kwargs): 30 | pass 31 | 32 | path, handler, methods, name = fake_handler() 33 | self.assertEqual(handler.renderers, (JSONRenderer, )) 34 | 35 | def test_create_handler_with_invalid_method_type(self): 36 | @endpoint('/api', None) 37 | def fake_handler(request, *args, **kwargs): 38 | pass 39 | 40 | self.assertRaises(NotSupportedArgumentType, fake_handler) 41 | -------------------------------------------------------------------------------- /aiorest_ws/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Decorators and wrappers, used for routing issues. 4 | """ 5 | from aiorest_ws.validators import MethodValidator 6 | from aiorest_ws.views import MethodBasedView 7 | 8 | __all__ = ('endpoint', ) 9 | 10 | 11 | def endpoint(path, methods, name=None, **attrs): 12 | """ 13 | Decorator function, which turn handler into MethodBasedView class. 14 | 15 | :param path: URL, used for get access to APIs. 16 | :param methods: acceptable method name or list of methods. 17 | :param name: short name of endpoint. 18 | :param attrs: any other attributes, which must be initialized. 19 | """ 20 | def endpoint_decorator(func): 21 | def wrapper(): 22 | class FunctionView(MethodBasedView): 23 | def handler(self, request, *args, **kwargs): 24 | return func(request, *args, **kwargs) 25 | 26 | view = FunctionView 27 | 28 | supported_methods = methods 29 | method_validator = MethodValidator() 30 | method_validator.validate(supported_methods) 31 | 32 | if type(supported_methods) is str: 33 | supported_methods = [supported_methods, ] 34 | 35 | for method in supported_methods: 36 | setattr(view, method.lower(), view.handler) 37 | 38 | for attr in attrs: 39 | setattr(view, str(attr).lower(), attrs[attr]) 40 | 41 | return path, view, methods, name 42 | return wrapper 43 | return endpoint_decorator 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 by Valeryi Savich and contributors. See AUTHORS 2 | for more details. 3 | 4 | Some rights reserved. 5 | 6 | Redistribution and use in source and binary forms of the software as well 7 | as documentation, with or without modification, are permitted provided 8 | that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above 14 | copyright notice, this list of conditions and the following 15 | disclaimer in the documentation and/or other materials provided 16 | with the distribution. 17 | 18 | * The names of the contributors may not be used to endorse or 19 | promote products derived from this software without specific 20 | prior written permission. 21 | 22 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 24 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 26 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 29 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 31 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 32 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 33 | DAMAGE. 34 | -------------------------------------------------------------------------------- /docs/source/views.rst: -------------------------------------------------------------------------------- 1 | .. _aiorest-ws-views: 2 | 3 | Views 4 | ===== 5 | 6 | .. currentmodule:: aiorest_ws.views 7 | 8 | There you can find description about views and endpoints, which can be used 9 | by developers for implementing APIs. By this framework supported method- and 10 | function-based views. 11 | 12 | Method-based views 13 | ------------------ 14 | 15 | It is mostly preferred approach to make your APIs. For using this class 16 | enough to inherit from it and override required methods or fields. For example: 17 | 18 | .. code-block:: python 19 | 20 | from aiorest_ws.views import MethodBasedView 21 | 22 | class HelloWorld(MethodBasedView): 23 | def get(self, request, *args, **kwargs): 24 | return "Hello, world!" 25 | 26 | After that implementation, register the view 27 | 28 | .. code-block:: python 29 | 30 | from aiorest_ws.routers import SimpleRouter 31 | 32 | router = SimpleRouter() 33 | router.register('/hello', HelloWorld, 'GET') 34 | 35 | Function-based views 36 | -------------------- 37 | 38 | Also aiorest-ws support function-based views, which you can import from 39 | ``aiorest_ws.endpoints`` module. He contains special wrappers, which provide 40 | syntactic sugar for developers. Its looks like this: 41 | 42 | .. code-block:: python 43 | 44 | from aiorest_ws.decorators import endpoint 45 | 46 | @endpoint(path='/hello', methods='GET') 47 | def hello_world(request, *args, **kwargs): 48 | return "Hello, world!" 49 | 50 | And don't forget to register endpoint inside some router: 51 | 52 | .. code-block:: python 53 | 54 | from aiorest_ws.routers import SimpleRouter 55 | 56 | endpoint_router = SimpleRouter() 57 | endpoint_router.register_endpoint(hello_world) 58 | -------------------------------------------------------------------------------- /examples/method_based_api/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import json 4 | 5 | from random import randint 6 | from autobahn.asyncio.websocket import WebSocketClientProtocol, \ 7 | WebSocketClientFactory 8 | 9 | 10 | class HelloClientProtocol(WebSocketClientProtocol): 11 | 12 | def onOpen(self): 13 | # hello username 14 | request = { 15 | 'method': 'GET', 16 | 'url': '/hello/' 17 | } 18 | self.sendMessage(json.dumps(request).encode('utf8')) 19 | 20 | # hello endpoint with arg in URL path (`user` and random digit) 21 | for _ in range(0, 10): 22 | user_id = str(randint(1, 100)) 23 | request = { 24 | 'method': 'GET', 25 | 'url': '/hello/user/' + user_id 26 | } 27 | self.sendMessage(json.dumps(request).encode('utf8')) 28 | 29 | # send request and parameters are separately 30 | digits = [randint(1, 10) for _ in range(3)] 31 | print("calculate sum for {}".format(digits)) 32 | request = { 33 | 'method': 'GET', 34 | 'url': '/calc/sum', 35 | 'args': {'digits': digits} 36 | } 37 | self.sendMessage(json.dumps(request).encode('utf8')) 38 | 39 | def onMessage(self, payload, isBinary): 40 | print("Result: {0}".format(payload.decode('utf8'))) 41 | 42 | 43 | if __name__ == '__main__': 44 | factory = WebSocketClientFactory("ws://localhost:8080") 45 | factory.protocol = HelloClientProtocol 46 | 47 | loop = asyncio.get_event_loop() 48 | coro = loop.create_connection(factory, '127.0.0.1', 8080) 49 | loop.run_until_complete(coro) 50 | loop.run_forever() 51 | loop.close() 52 | -------------------------------------------------------------------------------- /tests/keys/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA8izgBQMe6HVb2fjSZfk+pqT/Qu1PL+atodZsgtlVkMo/nB8c 3 | Pf9yZn2NVROxZagM1L+9ajpIDkZ+PP6uyYcMRGCb0EXyyLxRiOAqLdeauCPucj31 4 | aRxNHUEPPDN35McmNPkwdSqU79afv7HiIoxNE4CeSS/sDsn+uuoNAtodrdWpwpAU 5 | JSm8HiQJRZ5CL+5WnslL+gKnE7wLGi4i6pfFSKmCkSBtVr/80J0ijuyADwfHQvwq 6 | SiXmwCoBBcpJX5fC0DqSeaIVMQ4LOV9haYHQSv4W93/Hloo08EDo4qT3/1cHoR2X 7 | DhAYcyCzJkc7+LEhi2eK/kFISHtEOgbp+gm61wIDAQABAoIBAEYcIbqxvZf0qeO/ 8 | ukGVV8Lsz41zoFIwySGqLv3up2vkcWfkRcvZESiEvo7bxaa1cQmCfPas6sFfPRqx 9 | bK6hLzb4dQNlzLS6eCxVIUDmQz99/4EJWOvWKqkGdb+q8tHDqdb6MUIqUrXgf50C 10 | hYIU9hdoS+rKOd/KMLmyGpQxyxfQzESZkanmq/kd7+/JKUHOGIofPH9+a5t9cM8C 11 | QdnUFZ/gY92aQam9Of2nGtoV83D2mKQtB+rw4xh+nszA/QWusRet+hCGODomVYnA 12 | bf/vO5VTmwaIuYqw7TiP1hcK94Af/tDYl2AO6E1bzlOmZeypTgY0dYRjEL3ilqdy 13 | Le90smECgYEA/XqfarEIhKU28IQIjko/Lab/cusKOzhE1SdckKe2IyTzCWFdBjsy 14 | fOCOXleDuKk3jSpYBZg8XHFu6csmASbk/v44APOiX3wkwFs0A1r9vOZIKLrS5acU 15 | Idy4iqZ/o4OO763stwpWM8RFg1v0no3oDYc4aAOOeLbwB1qHPlUld1ECgYEA9JV4 16 | 5VCqfqZpa08p/Riutco54lvTqlgLaYoEysgcWSFTCNTmlquF5Leu6AmTo3sax8LG 17 | 7Tnoz6fa4D2ZejQWbRw4RgK5F24zfm+3oQfEL9QsMNX8C0yD4P9qABjOqHY6t1fe 18 | f4gnnMMztD5Z/N1kBVsxa7HDACFHY29m/KWLVacCgYEA0uU2Q++ddbd8WWqRL8oI 19 | fLRIL1XtQk5TvsEsG9LeEKmKaFCQtEGGhS3UZhwMJRTtek8zwxvhhMyACpcSPL0e 20 | pCXhdLFAdI92iHCZev02xfcXLIQcOiTj9DSPehSDeAlaYIJlw/ketZ1kdKGKimzp 21 | GHBb9vaWkEsstAGXKr5vGLECgYAHXAgLPmYi8Xbyl6s3xmpbZf5f5Ut1MLkHL/P8 22 | 9hCEThBlj6JwnjClNTG6Ia+jl6yynhXGRZvPw8k+PhxEJRUXq2W2FZbkMZizEUGB 23 | Qu6QUd4Sih3PaTn+pzDVV+m//+68XZSAWeR3PxhrMAUdKrw7vQa7qtmnQ1kWkN+U 24 | cpbCuQKBgHal4u2vBNFuSBip78i2iZpmeyFok1mhlb2zW0VqV6wbfC0ut8fTXDxA 25 | hgk+4XrNJc5bj8E/bBeLi2qO78grL823MVlGnbVk8D5SEI9diGmVB+FFlSWP1Obn 26 | Tt1gmGHgLAIIKfuAk3oRD+/+i5Fcv//nFjsec7kqq3nS39lCsLcc 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /aiorest_ws/utils/field_mapping.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Helper functions for mapping model fields to a dictionary of default 4 | keyword arguments that should be used for their equivalent serializer fields. 5 | """ 6 | import inspect 7 | 8 | from aiorest_ws.utils.text import capfirst 9 | 10 | __all__ = ('ClassLookupDict', 'needs_label') 11 | 12 | 13 | class ClassLookupDict(object): 14 | """ 15 | Takes a dictionary with classes as keys. 16 | Lookups against this object will traverses the object's inheritance 17 | hierarchy in method resolution order, and returns the first matching value 18 | from the dictionary or raises a KeyError if nothing matches. 19 | """ 20 | def __init__(self, mapping): 21 | self.mapping = mapping 22 | 23 | def __getitem__(self, key): 24 | if hasattr(key, '_proxy_class'): 25 | # Deal with proxy classes. Ie. BoundField behaves as if it 26 | # is a Field instance when using ClassLookupDict 27 | base_class = key._proxy_class 28 | else: 29 | base_class = key.__class__ 30 | 31 | for cls in inspect.getmro(base_class): 32 | if cls in self.mapping: 33 | return self.mapping[cls] 34 | raise KeyError('Class %s not found in lookup.' % base_class.__name__) 35 | 36 | def __setitem__(self, key, value): 37 | self.mapping[key] = value 38 | 39 | 40 | def needs_label(model_field_verbose_name, field_name): 41 | """ 42 | Returns `True` if the label based on the model's verbose name is not equal 43 | to the default label it would have based on it's field name. 44 | """ 45 | default_label = field_name.replace('_', ' ').capitalize() 46 | return capfirst(model_field_verbose_name) != default_label 47 | -------------------------------------------------------------------------------- /aiorest_ws/db/backends/sqlite3/managers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Manager classes and functions, which help for work with databases via SQL. 4 | """ 5 | __all__ = ('SQLiteManager', ) 6 | 7 | import sqlite3 8 | 9 | 10 | class SQLiteManager(object): 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(SQLiteManager, self).__init__() 14 | db_path = kwargs.get('name') 15 | self.connection = sqlite3.connect(db_path) 16 | 17 | def execute_sql(self, sql, parameters=()): 18 | """ 19 | Executes a SQL statement. 20 | 21 | :param sql: SQL statement as string. 22 | :param parameters: tuple with arguments. 23 | """ 24 | return self.connection.execute(sql, parameters) 25 | 26 | def execute_sql_and_fetchone(self, sql, parameters=()): 27 | """ 28 | Executes a SQL statement with fetching one row from result. 29 | 30 | :param sql: SQL statement as string. 31 | :param parameters: tuple with arguments. 32 | """ 33 | return self.execute_sql(sql, parameters).fetchone() 34 | 35 | def execute_sql_from_file(self, filepath): 36 | """ 37 | Executes a SQL statement, which was taken from the file. 38 | 39 | :param filepath: path to file. 40 | """ 41 | with open(filepath, 'r') as f: 42 | sql = f.read() 43 | result = self.connection.executescript(sql) 44 | return result 45 | 46 | def execute_script(self, sql): 47 | """ 48 | Execute a SQL statement. This method recommended to use, when 49 | required to execute few SQL queries independent and in parallel. 50 | 51 | :param sql: SQL statement as string. 52 | """ 53 | return self.connection.executescript(sql) 54 | -------------------------------------------------------------------------------- /aiorest_ws/status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | WebSocket status codes and functions for work with them. 4 | 5 | For more details check the link below: 6 | https://tools.ietf.org/html/rfc6455 7 | """ 8 | __all__ = ( 9 | 'WS_NORMAL', 'WS_GOING_AWAY', 'WS_PROTOCOL_ERROR', 10 | 'WS_DATA_CANNOT_ACCEPT', 'WS_RESERVED', 'WS_NO_STATUS_CODE', 11 | 'WS_CLOSED_ABNORMALLY', 'WS_MESSAGE_NOT_CONSISTENT', 12 | 'WS_MESSAGE_VIOLATE_POLICY', 'WS_MESSAGE_TOO_BIG', 13 | 'WS_SERVER_DIDNT_RETURN_EXTENSIONS', 'WS_UNEXPECTED_CONDITION', 14 | 'WS_FAILURE_TLS', 15 | 'is_not_used', 'is_reserved', 'is_library', 'is_private', 16 | ) 17 | 18 | 19 | WS_NORMAL = 1000 20 | WS_GOING_AWAY = 1001 21 | WS_PROTOCOL_ERROR = 1002 22 | WS_DATA_CANNOT_ACCEPT = 1003 23 | WS_RESERVED = 1004 24 | WS_NO_STATUS_CODE = 1005 25 | WS_CLOSED_ABNORMALLY = 1006 26 | WS_MESSAGE_NOT_CONSISTENT = 1007 27 | WS_MESSAGE_VIOLATE_POLICY = 1008 28 | WS_MESSAGE_TOO_BIG = 1009 29 | WS_SERVER_DIDNT_RETURN_EXTENSIONS = 1010 30 | WS_UNEXPECTED_CONDITION = 1011 31 | WS_FAILURE_TLS = 1015 32 | 33 | 34 | def is_not_used(code): 35 | """ 36 | Checking code, that is unused. 37 | 38 | :param code: integer value. 39 | """ 40 | return 0 <= code <= 999 41 | 42 | 43 | def is_reserved(code): 44 | """ 45 | Checking code, that is reserved. 46 | 47 | :param code: integer value. 48 | """ 49 | return 1000 <= code <= 2999 50 | 51 | 52 | def is_library(code): 53 | """ 54 | Checking code, that is value, used by libraries. 55 | 56 | :param code: integer value. 57 | """ 58 | return 3000 <= code <= 3999 59 | 60 | 61 | def is_private(code): 62 | """ 63 | Checking code, that is private code. 64 | 65 | :param code: integer value. 66 | """ 67 | return 4000 <= code <= 4999 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import re 4 | import ast 5 | from setuptools import setup 6 | 7 | _version_re = re.compile(r'__version__\s+=\s+(.*)') 8 | 9 | with open('aiorest_ws/__init__.py', 'rb') as f: 10 | version = str(ast.literal_eval(_version_re.search( 11 | f.read().decode('utf-8')).group(1))) 12 | 13 | requirements = ['autobahn==0.16.0', ] 14 | test_requirements = requirements + [ 15 | 'pytest', 'pytest-asyncio', 16 | 'pytest-cov', 'pytest-xdist' 17 | ] 18 | 19 | 20 | def read(f): 21 | return open(os.path.join(os.path.dirname(__file__), f)).read().strip() 22 | 23 | 24 | def get_packages(package): 25 | """ 26 | Return root package and all sub-packages. 27 | """ 28 | return [ 29 | dirpath 30 | for dirpath, dirnames, filenames in os.walk(package) 31 | if os.path.exists(os.path.join(dirpath, '__init__.py')) 32 | ] 33 | 34 | 35 | args = dict( 36 | name='aiorest-ws', 37 | version=version, 38 | url='https://github.com/Relrin/aiorest-ws', 39 | license='BSD', 40 | author='Valeryi Savich', 41 | author_email='relrin78@gmail.com', 42 | description='REST framework with WebSockets support', 43 | packages=get_packages('aiorest_ws'), 44 | include_package_data=True, 45 | zip_safe=False, 46 | platforms='any', 47 | install_requires=requirements, 48 | tests_require=test_requirements, 49 | classifiers=[ 50 | 'License :: OSI Approved :: BSD License', 51 | 'Intended Audience :: Developers', 52 | 'Programming Language :: Python', 53 | 'Programming Language :: Python :: 3', 54 | 'Programming Language :: Python :: 3.4', 55 | 'Topic :: Internet :: WWW/HTTP' 56 | ], 57 | ) 58 | 59 | 60 | if __name__ == '__main__': 61 | setup(**args) 62 | -------------------------------------------------------------------------------- /tests/auth/user/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from aiorest_ws.auth.user.utils import generate_password_hash, \ 5 | convert_db_row_to_dict, construct_update_sql, USER_MODEL_FIELDS, \ 6 | USER_MODEL_FIELDS_WITHOUT_PK 7 | 8 | 9 | @pytest.mark.parametrize("password, expected", [ 10 | ('', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'), 11 | ('123456', 12 | '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92'), 13 | ]) 14 | def test_generate_password_hash(password, expected): 15 | assert generate_password_hash(password) == expected 16 | 17 | 18 | @pytest.mark.parametrize("row, mapped_fields", [ 19 | ((1, 'admin', '123456', 'test', 'user', 0, 0, 1, 1), USER_MODEL_FIELDS), 20 | (('admin', '123456', '', '', 0, 0, 1, 1), USER_MODEL_FIELDS_WITHOUT_PK), 21 | ]) 22 | def test_convert_db_row_to_dict(row, mapped_fields): 23 | user_data = convert_db_row_to_dict(row, mapped_fields) 24 | if mapped_fields == USER_MODEL_FIELDS: 25 | assert user_data['id'] == row[0] 26 | row = row[1:] 27 | assert user_data['username'] == row[0] 28 | assert user_data['password'] == row[1] 29 | assert user_data['first_name'] == row[2] 30 | assert user_data['last_name'] == row[3] 31 | assert user_data['is_superuser'] == row[4] 32 | assert user_data['is_staff'] == row[5] 33 | assert user_data['is_user'] == row[6] 34 | assert user_data['is_active'] == row[7] 35 | 36 | 37 | @pytest.mark.parametrize("updated_fields", [ 38 | ({'is_superuser': True, 'is_user': False}), 39 | ({'is_user': False}), 40 | ({}), 41 | ]) 42 | def test_construct_update_sql(updated_fields): 43 | update_query, query_args = construct_update_sql(**updated_fields) 44 | assert len(updated_fields) == len(query_args) 45 | -------------------------------------------------------------------------------- /tests/test_parsers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from fixtures.fakes import FakeGetView 5 | 6 | from aiorest_ws.endpoints import PlainEndpoint, DynamicEndpoint 7 | from aiorest_ws.exceptions import EndpointValueError 8 | from aiorest_ws.parsers import URLParser 9 | 10 | 11 | class URLParserTestCase(unittest.TestCase): 12 | 13 | def setUp(self): 14 | super(URLParserTestCase, self).setUp() 15 | self.parser = URLParser() 16 | 17 | def test_parse_static_url(self): 18 | route = self.parser.define_route('/api', FakeGetView, 'GET') 19 | self.assertIsInstance(route, PlainEndpoint) 20 | 21 | def test_parse_dynamic_url(self): 22 | route = self.parser.define_route('/api/{users}', FakeGetView, 'GET') 23 | self.assertIsInstance(route, DynamicEndpoint) 24 | 25 | def test_parse_invalid_url_1(self): 26 | self.assertRaises( 27 | EndpointValueError, 28 | self.parser.define_route, '/api/{users', FakeGetView, 'GET' 29 | ) 30 | 31 | def test_parse_invalid_url_2(self): 32 | self.assertRaises( 33 | EndpointValueError, 34 | self.parser.define_route, 35 | '/api/{users{}}', FakeGetView, 'GET' 36 | ) 37 | 38 | def test_parse_invalid_url_3(self): 39 | self.assertRaises( 40 | EndpointValueError, 41 | self.parser.define_route, 42 | '/api/{users{}', FakeGetView, 'GET' 43 | ) 44 | 45 | def test_parse_invalid_url_4(self): 46 | self.assertRaises( 47 | EndpointValueError, 48 | self.parser.define_route, 49 | '/api/{users"}', FakeGetView, 'GET' 50 | ) 51 | 52 | def test_parse_invalid_url_5(self): 53 | self.assertRaises( 54 | EndpointValueError, 55 | self.parser.define_route, 56 | r"/api/{users+++}", FakeGetView, 'GET' 57 | ) 58 | -------------------------------------------------------------------------------- /aiorest_ws/utils/xmlutils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | XML classes and functions, used for serializing and de-serializing. 4 | """ 5 | from xml.sax.saxutils import XMLGenerator 6 | 7 | __all__ = ('SimpleXMLGenerator', ) 8 | 9 | 10 | class SimpleXMLGenerator(XMLGenerator): 11 | """ 12 | XML generator for input data. 13 | """ 14 | root_name = 'root' 15 | item_tag_name = 'list-item' 16 | 17 | def __init__(self, stream, encoding='utf-8'): 18 | super(SimpleXMLGenerator, self).__init__(stream, encoding=encoding) 19 | 20 | def parse(self, data): 21 | """ 22 | Convert data to XML. 23 | 24 | :param data: input data. 25 | """ 26 | self.startDocument() 27 | self.startElement(self.root_name, {}) 28 | self.to_xml(self, data) 29 | self.endElement(self.root_name) 30 | self.endDocument() 31 | 32 | def to_str(self, value): 33 | """ 34 | Encode value for the string. 35 | 36 | :param value: value, which will have converted to the string. 37 | """ 38 | return str(value).encode(self._encoding) 39 | 40 | def to_xml(self, xml, data): 41 | """ 42 | Convert Python object to XML. 43 | 44 | :param xml: XML object. 45 | :param data: Python's object, which will have been converted. 46 | """ 47 | if isinstance(data, (list, tuple)): 48 | for item in data: 49 | self.startElement(self.item_tag_name, {}) 50 | self.to_xml(xml, item) 51 | self.endElement(self.item_tag_name) 52 | elif isinstance(data, dict): 53 | for key, value in data.items(): 54 | xml.startElement(key, {}) 55 | self.to_xml(xml, value) 56 | xml.endElement(key) 57 | elif data is None: 58 | pass 59 | else: 60 | xml.characters(self.to_str(data)) 61 | -------------------------------------------------------------------------------- /aiorest_ws/db/orm/sqlalchemy/mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module provides special classes (mixins) which used for getting data from 4 | SQLAlchemy ORM when work with a model serializers or fields. 5 | """ 6 | from aiorest_ws.conf import settings 7 | from aiorest_ws.db.orm.sqlalchemy.model_meta import model_pk 8 | 9 | 10 | __all__ = ('ORMSessionMixin', 'SQLAlchemyMixin', ) 11 | 12 | 13 | class ORMSessionMixin(object): 14 | """ 15 | Special wrapper around SQLALchemy querysets, when user specified them in 16 | for fields, serializers and etcetera. 17 | """ 18 | def _get_session(self): 19 | return settings.SQLALCHEMY_SESSION() 20 | 21 | def get_queryset(self): 22 | """ 23 | Return queryset, which will be executed during a session. 24 | Necessary to write queries for field in a models like: 25 | from sqlalchemy.orm.query import Query 26 | Query(User).filter(id==5).first() 27 | """ 28 | # Avoid there "connection leaks", when developer described 29 | # queries like session.query(User).filter(id==5).first() 30 | if self.queryset.session: 31 | self.queryset.session.close() 32 | 33 | self.queryset.session = self._get_session() 34 | return self.queryset 35 | 36 | 37 | class SQLAlchemyMixin(object): 38 | """ 39 | Class which provide opportunity to get primary key from the passed object. 40 | """ 41 | def _get_filter_args(self, query, data): 42 | mapper = query._bind_mapper() 43 | model = mapper.class_ 44 | pk_fields = model_pk(model) 45 | return (getattr(model, field) == data[field] for field in pk_fields) 46 | 47 | def _get_object_pk(self, obj): 48 | mapper = obj.__mapper__ 49 | model = mapper.class_ 50 | pk_fields = model_pk(model) 51 | data = {str(field): getattr(obj, field) for field in pk_fields} 52 | return data if len(pk_fields) > 1 else data[pk_fields[0]] 53 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | aiorest-ws is written and maintained by Valeryi Savich and 2 | various contributors: 3 | 4 | Project Leader: 5 | 6 | - Valeryi Savich 7 | 8 | Contributors: 9 | 10 | - Dmitry Vechorko 11 | 12 | The serializers, field, exception classes and documentation for them was 13 | taken from the Django REST Framework. The original code is BSD licensed. The 14 | following copyrights apply: 15 | 16 | - (c) 2016 Tom Christie 17 | 18 | The to_xml() function was taken from the django-rest-framework-xml module. 19 | The original code is BSD licensed with the following copyrights from that 20 | module: 21 | 22 | - (c) 2014 Jose Padilla 23 | 24 | The routing classes and URL parser code originally placed in aiohttp library. 25 | The original code is Apache 2.0 licensed. The following copyrights apply: 26 | 27 | - (c) 2016 Andrew Svetlov 28 | 29 | The View, MethodViewMeta, MethodBasedView classes are partly modified, 30 | and the base implementation was taken from the Flask project (views module). 31 | The original code is BSD licensed with the following copyrights from 32 | that module: 33 | 34 | - (c) 2016 Armin Ronacher 35 | 36 | The idea for the "global" settings with further overriding, some util modules 37 | for processing requests and accessing to fields was taken from the Django 38 | Framework. The original code is BSD licensed with the following copyrights 39 | from that module: 40 | 41 | - (c) 2016 Django Software Foundation 42 | 43 | The nice_repr (renamed to humanize_timedelta), iso8601_repr and parse 44 | (renamed to parse_timedelta) functions for processing timedelta objects and 45 | its humanize was taken from django-timedelta-field project. This functions 46 | partly modified and placed into different modules (dateparse, formatters, 47 | humanize_datetime). The original code is BSD licensed with the following 48 | copyrights from that module: 49 | 50 | - (c) 2015 Matthew Schinckel 51 | -------------------------------------------------------------------------------- /tests/db/test_sqlite_manager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import tempfile 3 | import unittest 4 | 5 | from aiorest_ws.db.backends.sqlite3.constants import IN_MEMORY 6 | from aiorest_ws.db.backends.sqlite3.managers import SQLiteManager 7 | 8 | 9 | SQL_CREATE_USER_TABLE = """ 10 | CREATE TABLE IF NOT EXISTS test_user_table 11 | (id INTEGER PRIMARY KEY NOT NULL, 12 | username CHAR(255) NOT NULL 13 | ); 14 | """ 15 | SQL_USER_ADD = """ 16 | INSERT INTO test_user_table (`username`) 17 | VALUES (?); 18 | """ 19 | SQL_USER_GET = """ 20 | SELECT `id`, `username` 21 | FROM test_user_table 22 | WHERE username=?; 23 | """ 24 | 25 | 26 | class SQLiteManagerTestCase(unittest.TestCase): 27 | 28 | def setUp(self): 29 | super(SQLiteManagerTestCase, self).setUp() 30 | self.manager = SQLiteManager(name=IN_MEMORY) 31 | 32 | def test_execute_sql(self): 33 | self.manager.execute_sql(SQL_CREATE_USER_TABLE) 34 | 35 | def test_execute_sql_and_fetchone(self): 36 | username = 'test_user' 37 | self.manager.execute_sql(SQL_CREATE_USER_TABLE) 38 | self.manager.execute_sql(SQL_USER_ADD, (username, )) 39 | row = self.manager.execute_sql_and_fetchone(SQL_USER_GET, (username, )) 40 | self.assertEqual(row, (1, username)) 41 | 42 | def test_execute_sql_from_file(self): 43 | self.manager.execute_sql(SQL_CREATE_USER_TABLE) 44 | with tempfile.NamedTemporaryFile() as tmpfile: 45 | file_sql = bytes(""" 46 | INSERT INTO test_user_table (`username`) 47 | VALUES ('test_user'); 48 | """, encoding='utf-8') 49 | tmpfile.write(file_sql) 50 | self.manager.execute_sql_from_file(tmpfile.name) 51 | 52 | def test_execute_script(self): 53 | self.manager.execute_sql(SQL_CREATE_USER_TABLE) 54 | self.manager.execute_script(""" 55 | INSERT INTO test_user_table (`username`) 56 | VALUES ('test_user'); 57 | """) 58 | -------------------------------------------------------------------------------- /examples/auth_token/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import json 4 | 5 | from autobahn.asyncio.websocket import WebSocketClientProtocol, \ 6 | WebSocketClientFactory 7 | 8 | 9 | class AuthAPIClientProtocol(WebSocketClientProtocol): 10 | 11 | def onOpen(self): 12 | # create user 13 | request = { 14 | 'method': 'POST', 'url': '/auth/user/create', 15 | 'data': { 16 | 'username': 'admin', 17 | 'password': '123456', 18 | 'is_superuser': True, 'is_staff': False, 'is_user': False 19 | } 20 | } 21 | self.sendMessage(json.dumps(request).encode('utf8')) 22 | 23 | # take there message, that this user already created 24 | self.sendMessage(json.dumps(request).encode('utf8')) 25 | 26 | # log in 27 | request = { 28 | 'method': 'POST', 29 | 'url': '/auth/login', 30 | 'data': { 31 | 'username': 'admin', 32 | 'password': '123456', 33 | } 34 | } 35 | self.sendMessage(json.dumps(request).encode('utf8')) 36 | 37 | def onMessage(self, payload, isBinary): 38 | message = json.loads(payload.decode('utf8')) 39 | print("Result: {0}".format(message)) 40 | if 'request' in message and message['request']['url'] == "/auth/login": 41 | # log out 42 | request = { 43 | 'method': 'POST', 44 | 'url': '/auth/logout', 45 | 'token': message['data']['token'] 46 | } 47 | self.sendMessage(json.dumps(request).encode('utf8')) 48 | 49 | 50 | if __name__ == '__main__': 51 | factory = WebSocketClientFactory("ws://localhost:8080") 52 | factory.protocol = AuthAPIClientProtocol 53 | 54 | loop = asyncio.get_event_loop() 55 | coro = loop.create_connection(factory, '127.0.0.1', 8080) 56 | loop.run_until_complete(coro) 57 | loop.run_forever() 58 | loop.close() 59 | -------------------------------------------------------------------------------- /tests/utils/test_xmlutils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from io import StringIO 5 | from aiorest_ws.utils.xmlutils import SimpleXMLGenerator 6 | 7 | 8 | class SimpleXMLGeneratorTestCase(unittest.TestCase): 9 | 10 | def setUp(self): 11 | super(SimpleXMLGeneratorTestCase, self).setUp() 12 | self.render = StringIO() 13 | 14 | def test_parse(self): 15 | sxml = SimpleXMLGenerator(self.render) 16 | data = [{'count': 1}, [1, 2, 3], None, 'test_string'] 17 | sxml.parse(data) 18 | result = self.render.getvalue() 19 | self.assertIn('1', result) 20 | self.assertIn('1' 21 | '23', 22 | result) 23 | self.assertIn('', result) 24 | self.assertIn('test_string', result) 25 | 26 | def test_to_str(self): 27 | sxml = SimpleXMLGenerator(self.render) 28 | self.assertEqual(sxml.to_str({'count': 1}), b"{'count': 1}") 29 | self.assertEqual(sxml.to_str(['1, 2, 3']), b"['1, 2, 3']") 30 | self.assertEqual(sxml.to_str('test_string'), b"test_string") 31 | 32 | def test_to_xml(self): 33 | sxml = SimpleXMLGenerator(self.render) 34 | data = [{'count': 1}, [1, 2, 3], None, 'test_string'] 35 | 36 | sxml.startDocument() 37 | sxml.startElement('test', {}) 38 | sxml.to_xml(sxml, data) 39 | sxml.endElement('test') 40 | sxml.endDocument() 41 | 42 | result = self.render.getvalue() 43 | self.assertIn('1', result) 44 | self.assertIn('1' 45 | '23', 46 | result) 47 | self.assertIn('', result) 48 | self.assertIn('test_string', result) 49 | -------------------------------------------------------------------------------- /aiorest_ws/db/orm/django/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The `compat` module provides support for backwards compatibility with older 4 | versions of Django, and compatibility wrappers around optional packages. 5 | """ 6 | import inspect 7 | 8 | from django.apps import apps 9 | from django.core.exceptions import ImproperlyConfigured 10 | from django.db import models 11 | 12 | 13 | try: 14 | from django.contrib.postgres import fields as postgres_fields 15 | except ImportError: 16 | postgres_fields = None 17 | 18 | 19 | try: 20 | from django.contrib.postgres.fields import JSONField 21 | except ImportError: 22 | JSONField = None 23 | 24 | 25 | __all__ = [ 26 | 'postgres_fields', 'JSONField', '_resolve_model', 27 | 'get_related_model', 'get_remote_field', 'value_from_object' 28 | ] 29 | 30 | 31 | def _resolve_model(obj): 32 | """ 33 | Resolve supplied `obj` to a Django model class. 34 | `obj` must be a Django model class itself, or a string 35 | representation of one. Useful in situations like GH #1225 where 36 | Django may not have resolved a string-based reference to a model in 37 | another model's foreign key definition. 38 | String representations should have the format: 39 | 'appname.ModelName' 40 | """ 41 | if isinstance(obj, str) and len(obj.split('.')) == 2: 42 | app_name, model_name = obj.split('.') 43 | resolved_model = apps.get_model(app_name, model_name) 44 | if resolved_model is None: 45 | msg = "Django did not return a model for {0}.{1}" 46 | raise ImproperlyConfigured(msg.format(app_name, model_name)) 47 | return resolved_model 48 | elif inspect.isclass(obj) and issubclass(obj, models.Model): 49 | return obj 50 | raise ValueError("{0} is not a Django model".format(obj)) 51 | 52 | 53 | def get_related_model(field): 54 | return field.remote_field.model 55 | 56 | 57 | def get_remote_field(field, **kwargs): 58 | return field.remote_field 59 | 60 | 61 | def value_from_object(field, obj): 62 | return field.value_from_object(obj) 63 | -------------------------------------------------------------------------------- /docs/source/auth.rst: -------------------------------------------------------------------------------- 1 | .. _aiorest-ws-auth: 2 | 3 | Authentication 4 | ============== 5 | 6 | At this section you will find description about the default user abstraction 7 | and using JSON Web Token as a basic implementation for authentication. 8 | 9 | User abstraction 10 | ---------------- 11 | 12 | :class:`User` class provide a very useful user abstraction, which used for 13 | storing information about current online user. Most basic fields are defined 14 | inside his base class – :class:`AbstractUser`. 15 | 16 | .. note:: 17 | At the current release :class:`User` model used with SQLite database, 18 | however, if necessary, you can write your own implementation for any 19 | other database (MySQL, PostgreSQL, Oracle, DB2, etc). 20 | 21 | .. autoclass:: aiorest_ws.auth.user.abstractions.User 22 | :members: 23 | :inherited-members: 24 | :show-inheritance: 25 | 26 | JSON Web Token (JWT) 27 | -------------------- 28 | 29 | JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and 30 | self-contained way for securely transmitting information between parties as a 31 | JSON object. This information can be verified and trusted because it is digitally 32 | signed. JWTs can be signed using a secret (with HMAC algorithm) or a public/private 33 | key pair using RSA. (c) jwt.io 34 | 35 | For more details about how JSON Web Token works, his advantages and 36 | why necessary to use it, you can read `there `_. 37 | 38 | Also you can look on `example `_ 39 | which implement simple user registration and log-in/log out mechanism with JSON Web Tokens. 40 | 41 | At this package provided middleware and manager classes, which used for add 42 | support JWT inside you application. 43 | 44 | .. autoclass:: aiorest_ws.auth.token.middlewares.JSONWebTokenMiddleware 45 | :members: 46 | :inherited-members: 47 | :show-inheritance: 48 | 49 | .. autoclass:: aiorest_ws.auth.token.managers.JSONWebTokenManager 50 | :members: 51 | :inherited-members: 52 | :show-inheritance: 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /tests/test_request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf -*- 2 | import json 3 | import unittest 4 | 5 | from base64 import b64encode 6 | 7 | from aiorest_ws.routers import SimpleRouter 8 | from aiorest_ws.request import RequestHandlerFactory, RequestHandlerProtocol 9 | 10 | 11 | class RequestHandlerProtocolTestCase(unittest.TestCase): 12 | 13 | def setUp(self): 14 | super(RequestHandlerProtocolTestCase, self).setUp() 15 | self.protocol = RequestHandlerProtocol() 16 | 17 | def test_encode_message(self): 18 | message = json.dumps({'key': 'value'}).encode('utf-8') 19 | self.assertEqual(self.protocol._encode_message(message), message) 20 | 21 | self.assertEqual( 22 | self.protocol._encode_message(message, isBinary=True), 23 | b64encode(message) 24 | ) 25 | 26 | def test_decode_message(self): 27 | data = {'url': '/api'} 28 | message = json.dumps(data).encode('utf-8') 29 | request = self.protocol._decode_message(message) 30 | self.assertEqual({'url': request.url}, data) 31 | 32 | def test_decode_message_binary(self): 33 | data = {'url': '/api'} 34 | message = json.dumps(data).encode('utf-8') 35 | message = b64encode(message) 36 | request = self.protocol._decode_message(message, isBinary=True) 37 | self.assertEqual({'url': request.url}, data) 38 | 39 | 40 | class RequestHandlerFactoryTestCase(unittest.TestCase): 41 | 42 | def setUp(self): 43 | super(RequestHandlerFactoryTestCase, self).setUp() 44 | self.factory = RequestHandlerFactory() 45 | 46 | def test_router_getter(self): 47 | self.assertEqual(self.factory.router, self.factory._router) 48 | 49 | def test_router_setter(self): 50 | class ImplementedRouter(SimpleRouter): 51 | pass 52 | 53 | self.factory.router = ImplementedRouter() 54 | self.assertIsInstance(self.factory.router, ImplementedRouter) 55 | 56 | def test_router_setter_with_invalid_router_class(self): 57 | class InvalidRouter(object): 58 | pass 59 | 60 | self.assertRaises(TypeError, self.factory.router, InvalidRouter()) 61 | -------------------------------------------------------------------------------- /aiorest_ws/parsers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | URL parsers, which help to define, with which endpoint router works. 4 | """ 5 | import re 6 | 7 | from aiorest_ws.endpoints import PlainEndpoint, DynamicEndpoint 8 | from aiorest_ws.exceptions import EndpointValueError 9 | 10 | __all__ = ( 11 | 'ANY_VALUE', 'DYNAMIC_PARAMETER', 'VALID_DYNAMIC_PARAMETER', 'URLParser' 12 | ) 13 | 14 | ANY_VALUE = r'[^{}/]+' 15 | DYNAMIC_PARAMETER = re.compile(r'({\s*[\w\d_]+\s*})') 16 | VALID_DYNAMIC_PARAMETER = re.compile(r'{(?P[\w][\w\d_]*)}') 17 | 18 | 19 | class URLParser(object): 20 | """ 21 | Parser over endpoints paths, which returns one of the most suitable 22 | instances of route classes. 23 | """ 24 | def define_route(self, path, handler, methods, name=None): 25 | """ 26 | Define a router as instance of BaseRoute subclass, which passed 27 | from register method in RestWSRouter. 28 | 29 | :param path: URL, which used to get access to API. 30 | :param handler: class inherited from MethodBasedView, which used for 31 | processing request. 32 | :param methods: list of available for user methods or string with 33 | concrete method name. 34 | :param name: the base to use for the URL names that are created. 35 | """ 36 | # it's PlainRoute, when don't have any "dynamic symbols" 37 | if all(symbol not in path for symbol in ['{', '}']): 38 | return PlainEndpoint(path, handler, methods, name) 39 | 40 | # Try to processing as a dynamic path 41 | pattern = '' 42 | for part in DYNAMIC_PARAMETER.split(path): 43 | match = VALID_DYNAMIC_PARAMETER.match(part) 44 | if match: 45 | pattern += '(?P<{}>{})'.format(match.group('var'), ANY_VALUE) 46 | continue 47 | 48 | if any(symbol in part for symbol in ['{', '}']): 49 | raise EndpointValueError("Invalid {} part of {} path".format(part, path)) # NOQA 50 | 51 | pattern += re.escape(part) 52 | try: 53 | compiled = re.compile("^{}$".format(pattern)) 54 | except re.error as exc: 55 | raise EndpointValueError("Bad pattern '{}': {}".format(pattern, exc)) # NOQA 56 | return DynamicEndpoint(path, handler, methods, name, compiled) 57 | -------------------------------------------------------------------------------- /tests/utils/test_encoding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import pytest 4 | from decimal import Decimal 5 | 6 | from aiorest_ws.utils.encoding import is_protected_type, force_text, \ 7 | force_text_recursive 8 | from aiorest_ws.utils.serializer_helpers import ReturnList, ReturnDict 9 | 10 | 11 | class CustomType(object): 12 | 13 | def __init__(self, data): 14 | self.data = data 15 | 16 | def __str__(self): 17 | return self.data 18 | 19 | __unicode__ = __str__ 20 | 21 | 22 | @pytest.mark.parametrize("value, expected", [ 23 | (10, False), # int 24 | ("test_string", True), # string 25 | (None, True), # NoneType 26 | (1.0, True), # float 27 | (Decimal('3.14'), True), # Decimal 28 | (datetime.datetime(year=2016, month=6, day=1), True), # datetime.datetime 29 | (datetime.date(year=2016, month=6, day=1), True), # datetime.date 30 | (datetime.time(hour=12), True), # datetime.time 31 | ]) 32 | def test_is_protected_type(value, expected): 33 | assert is_protected_type(value) is expected 34 | 35 | 36 | @pytest.mark.parametrize("value, kwargs, expected", [ 37 | ("string", {}, "string"), 38 | (bytes("string", encoding='utf-8'), {}, "string"), 39 | (datetime.time(hour=12), {"strings_only": True}, datetime.time(hour=12)), 40 | (b'test', {}, "test"), 41 | (CustomType("test"), {}, "test"), 42 | (b'\x80abc', {"encoding": 'utf-8', "errors": 'strict'}, "128 97 98 99") 43 | ]) 44 | def test_force_text(value, kwargs, expected): 45 | assert force_text(value, **kwargs) == expected 46 | 47 | 48 | @pytest.mark.parametrize("value, kwargs, exception_cls", [ 49 | (CustomType(b'\xc3\xb6\xc3\xa4\xc3\xbc'), {}, TypeError), 50 | ]) 51 | def test_force_text_failed(value, kwargs, exception_cls): 52 | with pytest.raises(exception_cls): 53 | force_text(value, **kwargs) 54 | 55 | 56 | @pytest.mark.parametrize("value, expected", [ 57 | ("string", "string"), 58 | (bytes("string", encoding='utf-8'), "string"), 59 | (["test", "string"], ["test", "string"]), 60 | ({"key": "value"}, {"key": "value"}), 61 | (ReturnDict({"key": "value"}, serializer=object()), {"key": "value"}), 62 | (ReturnList(["test", "string"], serializer=object()), ['test', 'string']) 63 | ]) 64 | def test_force_text_recursive(value, expected): 65 | assert force_text_recursive(value) == expected 66 | -------------------------------------------------------------------------------- /tests/test_views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from fixtures.fakes import FakeGetView 5 | 6 | from aiorest_ws.exceptions import NotSpecifiedHandler, \ 7 | NotSpecifiedMethodName, IncorrectMethodNameType, InvalidRenderer 8 | from aiorest_ws.renderers import JSONRenderer 9 | from aiorest_ws.views import View 10 | from aiorest_ws.wrappers import Request 11 | 12 | 13 | class ViewTestCase(unittest.TestCase): 14 | 15 | def setUp(self): 16 | super(ViewTestCase, self).setUp() 17 | self.view = View() 18 | 19 | def test_as_view(self): 20 | self.assertEqual(self.view.as_view('api'), None) 21 | 22 | 23 | class MethodBaseViewTestCase(unittest.TestCase): 24 | 25 | def setUp(self): 26 | super(MethodBaseViewTestCase, self).setUp() 27 | self.view = FakeGetView() 28 | 29 | def test_dispatch(self): 30 | data = {} 31 | request = Request(**data) 32 | self.assertRaises(NotSpecifiedMethodName, self.view.dispatch, request) 33 | 34 | def test_dispatch_2(self): 35 | data = {'method': 'GET'} 36 | request = Request(**data) 37 | self.assertEqual(self.view.dispatch(request), 'fake') 38 | 39 | def test_dispatch_failed(self): 40 | data = {'method': ['POST', ]} 41 | request = Request(**data) 42 | self.assertRaises(IncorrectMethodNameType, self.view.dispatch, 43 | request) 44 | 45 | def test_dispatch_failed_2(self): 46 | data = {'method': 'POST'} 47 | request = Request(**data) 48 | self.view.methods = ['GET', ] 49 | self.assertRaises(NotSpecifiedHandler, self.view.dispatch, request) 50 | 51 | def test_get_serializer(self): 52 | format = None 53 | self.view.renderers = () 54 | self.assertIsInstance( 55 | self.view.get_renderer(format), JSONRenderer 56 | ) 57 | 58 | def test_get_serializer_2(self): 59 | format = 'json' 60 | self.view.renderers = (JSONRenderer,) 61 | self.assertIsInstance( 62 | self.view.get_renderer(format), JSONRenderer 63 | ) 64 | 65 | def test_get_serializer_3(self): 66 | format = 'xml' 67 | self.view.renderers = (JSONRenderer,) 68 | self.assertIsInstance( 69 | self.view.get_renderer(format), JSONRenderer 70 | ) 71 | 72 | def test_get_serializer_failed(self): 73 | format = None 74 | self.view.renderers = 'JSONSerializer' 75 | self.assertRaises(InvalidRenderer, self.view.get_renderer, format) 76 | -------------------------------------------------------------------------------- /aiorest_ws/auth/token/backends.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Storage backends for save/get credentials. 4 | """ 5 | from aiorest_ws.auth.token.utils import SQL_CREATE_TOKEN_TABLE, \ 6 | SQL_TOKEN_GET, SQL_TOKEN_GET_BY_TOKEN_USERNAME, SQL_TOKEN_ADD, \ 7 | TOKEN_MODEL_FIELDS 8 | from aiorest_ws.conf import settings 9 | from aiorest_ws.db.backends.sqlite3.constants import IN_MEMORY 10 | from aiorest_ws.db.backends.sqlite3.managers import SQLiteManager 11 | from aiorest_ws.db.utils import convert_db_row_to_dict 12 | from aiorest_ws.storages.backends import BaseStorageBackend 13 | 14 | __all__ = ('InMemoryTokenBackend', ) 15 | 16 | 17 | class InMemoryTokenBackend(BaseStorageBackend): 18 | """ 19 | In memory backend (based on SQLite) for token. 20 | """ 21 | 22 | def __init__(self): 23 | super(InMemoryTokenBackend, self).__init__() 24 | if settings.DATABASES: 25 | self.db_manager = settings.DATABASES['default']['manager'] 26 | else: 27 | self.db_manager = SQLiteManager(name=IN_MEMORY) 28 | self.__create_models() 29 | 30 | def __create_models(self): 31 | self.db_manager.execute_script(SQL_CREATE_TOKEN_TABLE) 32 | 33 | def get(self, token): 34 | """ 35 | Get token from the storage. 36 | 37 | :param token: token as string. 38 | """ 39 | args = (token, ) 40 | row = self.db_manager.execute_sql_and_fetchone(SQL_TOKEN_GET, args) 41 | if row: 42 | token_object = convert_db_row_to_dict(row, TOKEN_MODEL_FIELDS) 43 | else: 44 | token_object = {} 45 | return token_object 46 | 47 | def get_token_by_username(self, token_name, username): 48 | """ 49 | Get token from the storage by token_name and username. 50 | 51 | :param token: token as string. 52 | """ 53 | args = (token_name, username) 54 | row = self.db_manager.execute_sql_and_fetchone( 55 | SQL_TOKEN_GET_BY_TOKEN_USERNAME, args 56 | ) 57 | if row: 58 | token_object = convert_db_row_to_dict(row, TOKEN_MODEL_FIELDS) 59 | else: 60 | token_object = {} 61 | return token_object 62 | 63 | def save(self, token_name, token, expired=None, user_id=None): 64 | """ 65 | Save token in the storage. 66 | 67 | :param user: instance of User class. 68 | :param token: token as string. 69 | """ 70 | args = (token_name, token, expired, user_id) 71 | self.db_manager.execute_sql(SQL_TOKEN_ADD, args) 72 | -------------------------------------------------------------------------------- /docs/source/wrappers.rst: -------------------------------------------------------------------------------- 1 | .. _aiorest-ws-wrappers: 2 | 3 | HTTP wrappers 4 | ============= 5 | 6 | .. currentmodule:: aiorest_ws.wrappers 7 | 8 | On this documentation page you will find definition of classes, which used 9 | for wrapping inner data of aiorest-ws into HTTP-like request or response 10 | objects. 11 | 12 | Request 13 | ------- 14 | 15 | Properties: 16 | 17 | - method 18 | 19 | Returns a method name as string, which specified by client at the request. 20 | 21 | - url 22 | 23 | Returns a request URL as string of described resource which necessary to take. 24 | 25 | - args 26 | 27 | Returns dictionary of arguments for request. 28 | 29 | For instance in the "classical REST" you can send the HTTP request by certain URL like 30 | http://mywebsite.com/api/user/?name=admin&password=mysecretpassword where with ``?`` and ``&`` 31 | symbols you can specify all required arguments when it is necessary. For the following example 32 | we specify all the same arguments in request at the dictionary: 33 | 34 | .. code-block:: python 35 | 36 | { 37 | "method": "GET", 38 | "url": "/api/user", 39 | "args": { 40 | "name": "admin", 41 | "password": "mysecretpassword" 42 | } 43 | } 44 | 45 | - data 46 | 47 | Returns body of the request. 48 | 49 | - event_name 50 | 51 | Returns event name, which defined by the client in the request. 52 | 53 | This string value will have appended to response and used by aiorest-ws clients 54 | dispatchers, when necessary to find the most suitable registered function, which 55 | intended for processing response. 56 | 57 | Available methods: 58 | 59 | - to_representation 60 | 61 | Returns part of request as a dictionary object. It used for serializing and 62 | appending ``event_name`` to the response object. 63 | 64 | - get_argument 65 | 66 | As an function argument take ``name``, which necessary to extract from request. 67 | If this parameter not found, returns ``None``. 68 | 69 | Response 70 | -------- 71 | 72 | Properties: 73 | 74 | - content 75 | 76 | This property return content (or result some view), which will be serialized 77 | later as response for client. 78 | 79 | Available methods: 80 | 81 | - wrap_exception 82 | 83 | As an argument take ``exception`` object (which inherited from 84 | :class:`BaseAPIException`) and set his message for ``content`` property. 85 | 86 | - append_request 87 | 88 | As an argument take ``request``, which used for appending part of :class:`Request` object 89 | to the :class:`Response` object. 90 | -------------------------------------------------------------------------------- /aiorest_ws/abstract.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Abstract classes for future implementation. 4 | """ 5 | from abc import ABCMeta, abstractmethod 6 | 7 | __all__ = ( 8 | 'AbstractEndpoint', 'AbstractRouter', 'AbstractMiddleware', 9 | 'AbstractPermission', 10 | ) 11 | 12 | 13 | class AbstractEndpoint(metaclass=ABCMeta): 14 | """ 15 | Base class for endpoints. 16 | """ 17 | path = None # URL, used for get access to API 18 | handler = None # Class/function for processing request 19 | methods = [] # List of supported methods (GET, POST, etc.) 20 | name = None # Short name for route 21 | _pattern = None # Pattern, which using for checking path on compatible 22 | 23 | def __init__(self, path, handler, methods, name): 24 | self.path = path 25 | self.handler = handler 26 | if type(methods) is str: 27 | self.methods.append(methods) 28 | else: 29 | self.methods.extend(methods) 30 | self.name = name 31 | 32 | @abstractmethod 33 | def match(self, path): 34 | """ 35 | Checking path on compatible. 36 | 37 | :param path: URL, which used for get access to API. 38 | """ 39 | pass 40 | 41 | 42 | class AbstractRouter(metaclass=ABCMeta): 43 | """ 44 | Base class for routers. 45 | """ 46 | _middlewares = [] 47 | 48 | def __init__(self, *args, **kwargs): 49 | self._urls = [] 50 | self._routes = {} 51 | 52 | @property 53 | def middlewares(self): 54 | """ 55 | Get list of used middlewares. 56 | """ 57 | return self._middlewares 58 | 59 | @abstractmethod 60 | def process_request(self, request): 61 | """ 62 | Handling received request from user. 63 | 64 | :param request: request from user. 65 | """ 66 | pass 67 | 68 | 69 | class AbstractMiddleware(metaclass=ABCMeta): 70 | """ 71 | Base class for middlewares. 72 | """ 73 | @abstractmethod 74 | def process_request(self, request, handler): 75 | """ 76 | Processing request before calling handler. 77 | 78 | :param request: instance of Request class. 79 | :param handler: view, invoked later for the request. 80 | """ 81 | pass 82 | 83 | 84 | class AbstractPermission(metaclass=ABCMeta): 85 | """ 86 | Base class for permissions. 87 | """ 88 | @staticmethod 89 | def check(request, handler): 90 | """ 91 | Check permission method. 92 | 93 | :param request: instance of Request class. 94 | :param handler: view, invoked later for the request. 95 | """ 96 | pass 97 | -------------------------------------------------------------------------------- /aiorest_ws/utils/representation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Helper functions for creating user-friendly representations of serializer 4 | classes and serializer fields. 5 | """ 6 | import re 7 | from aiorest_ws.utils.encoding import force_text 8 | 9 | __all__ = ('smart_repr', 'field_repr', 'serializer_repr', 'list_repr') 10 | 11 | 12 | def smart_repr(value): 13 | # Convert any type of data to a string 14 | value = force_text(value) 15 | 16 | # Representations like 17 | # 18 | # Should be presented as 19 | # 20 | value = re.sub(' at 0x[0-9A-Fa-f]{4,32}>', '>', value) 21 | return value 22 | 23 | 24 | def field_repr(field, force_many=False): 25 | kwargs = field._kwargs 26 | if force_many: 27 | kwargs = kwargs.copy() 28 | kwargs['many'] = True 29 | kwargs.pop('child', None) 30 | 31 | arg_string = ', '.join([smart_repr(val) for val in field._args]) 32 | kwarg_string = ', '.join([ 33 | '%s=%s' % (key, smart_repr(val)) 34 | for key, val in sorted(kwargs.items()) 35 | ]) 36 | if arg_string and kwarg_string: 37 | arg_string += ', ' 38 | 39 | if force_many: 40 | class_name = force_many.__class__.__name__ 41 | else: 42 | class_name = field.__class__.__name__ 43 | 44 | return "%s(%s%s)" % (class_name, arg_string, kwarg_string) 45 | 46 | 47 | def serializer_repr(serializer, indent, force_many=None): 48 | ret = field_repr(serializer, force_many) + ':' 49 | indent_str = ' ' * indent 50 | 51 | if force_many: 52 | fields = force_many.fields 53 | else: 54 | fields = serializer.fields 55 | 56 | for field_name, field in fields.items(): 57 | ret += '\n' + indent_str + field_name + ' = ' 58 | if hasattr(field, 'fields'): 59 | ret += serializer_repr(field, indent + 1) 60 | elif hasattr(field, 'child'): 61 | ret += list_repr(field, indent + 1) 62 | elif hasattr(field, 'child_relation'): 63 | ret += field_repr(field.child_relation, force_many=field.child_relation) # NOQA 64 | else: 65 | ret += field_repr(field) 66 | 67 | if serializer.validators: 68 | ret += '\n' + indent_str + 'class Meta:' 69 | ret += '\n' + indent_str + ' validators = ' + smart_repr(serializer.validators) # NOQA 70 | 71 | return ret 72 | 73 | 74 | def list_repr(serializer, indent): 75 | child = serializer.child 76 | if hasattr(child, 'fields'): 77 | return serializer_repr(serializer, indent, force_many=child) 78 | return field_repr(serializer) 79 | -------------------------------------------------------------------------------- /tests/test_serializers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from aiorest_ws.exceptions import SerializerError 5 | from aiorest_ws.renderers import BaseRenderer, JSONRenderer, \ 6 | XMLRenderer 7 | 8 | 9 | class BaseSerializerTestCase(unittest.TestCase): 10 | 11 | def setUp(self): 12 | super(BaseSerializerTestCase, self).setUp() 13 | self.bs = BaseRenderer() 14 | 15 | def test_serialize(self): 16 | self.assertIsNone(self.bs.render({})) 17 | 18 | 19 | class JSONSerializerTestCase(unittest.TestCase): 20 | 21 | def setUp(self): 22 | super(JSONSerializerTestCase, self).setUp() 23 | self.json = JSONRenderer() 24 | 25 | def test_serialize_invalid_data(self): 26 | self.assertRaises(SerializerError, self.json.render, object) 27 | 28 | def test_using_short_separators(self): 29 | self.json.compact = True 30 | data = {'objects': [1, 2, 3]} 31 | output = self.json.render(data) 32 | self.assertEqual(output, b'{"objects":[1,2,3]}') 33 | 34 | def test_using_long_separators(self): 35 | self.json.compact = False 36 | data = {'objects': [1, 2, 3]} 37 | output = self.json.render(data) 38 | self.assertEqual(output, b'{"objects": [1, 2, 3]}') 39 | 40 | def test_ensure_ascii_is_true(self): 41 | self.json.ensure_ascii = True 42 | self.json.compact = False 43 | data = {"last_name": u"王"} 44 | output = self.json.render(data) 45 | self.assertEqual(output, b'{"last_name": "\\u738b"}') 46 | 47 | def test_ensure_ascii_is_false(self): 48 | self.json.ensure_ascii = False 49 | self.json.compact = False 50 | data = {"last_name": u"王"} 51 | output = self.json.render(data) 52 | self.assertEqual(output, b'{"last_name": "\xe7\x8e\x8b"}') 53 | 54 | def test_bad_unicode_symbols(self): 55 | self.json.compact = False 56 | data = ["\u2028", "\u2029"] 57 | output = self.json.render(data) 58 | self.assertEqual(output, b'["\\u2028", "\\u2029"]') 59 | 60 | 61 | class XMLSerializerTestCase(unittest.TestCase): 62 | 63 | def setUp(self): 64 | super(XMLSerializerTestCase, self).setUp() 65 | self.xml = XMLRenderer() 66 | 67 | def test_serialize_invalid_data(self): 68 | self.assertRaises(SerializerError, self.xml.render, {None: 'test'}) 69 | 70 | def test_valid_serialization(self): 71 | data = {'objects': [1, 2, 3]} 72 | output = self.xml.render(data) 73 | expected = '12' \ 74 | '3' \ 75 | ''.encode('utf-8') 76 | self.assertIn(bytes(expected), output) 77 | -------------------------------------------------------------------------------- /examples/method_based_api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 74 | 75 | 76 |

NOTE: Open your browser's JavaScript console and click on the buttons (hit F12).

77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /aiorest_ws/renderers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Serializers for generated responses by the server. 4 | """ 5 | import json 6 | 7 | from io import StringIO 8 | from aiorest_ws.conf import settings 9 | from aiorest_ws.exceptions import SerializerError 10 | from aiorest_ws.utils.formatting import SHORT_SEPARATORS, LONG_SEPARATORS, \ 11 | WRONG_UNICODE_SYMBOLS 12 | from aiorest_ws.utils.xmlutils import SimpleXMLGenerator 13 | 14 | __all__ = ('BaseRenderer', 'JSONRenderer', 'XMLRenderer', ) 15 | 16 | 17 | class BaseRenderer(object): 18 | 19 | format = None 20 | charset = 'utf-8' 21 | 22 | def render(self, data): 23 | """ 24 | Render input data into another format. 25 | 26 | :param data: dictionary object. 27 | """ 28 | pass 29 | 30 | 31 | class JSONRenderer(BaseRenderer): 32 | 33 | format = 'json' 34 | # Don't set a charset because JSON is a binary encoding, that can be 35 | # encoded as utf-8, utf-16 or utf-32. 36 | # For more details see: http://www.ietf.org/rfc/rfc4627.txt 37 | # and Armin Ronacher's article http://goo.gl/MExCKv 38 | charset = None 39 | ensure_ascii = not settings.UNICODE_JSON 40 | compact = settings.COMPACT_JSON 41 | 42 | def render(self, data): 43 | """ 44 | Render input data into JSON. 45 | 46 | :param data: dictionary or list object (response). 47 | """ 48 | separators = SHORT_SEPARATORS if self.compact else LONG_SEPARATORS 49 | 50 | try: 51 | render = json.dumps( 52 | data, ensure_ascii=self.ensure_ascii, separators=separators 53 | ) 54 | 55 | # Unicode symbols \u2028 and \u2029 are invisible in JSON and 56 | # make output are invalid. To avoid this situations, necessary 57 | # replace this symbols. 58 | # For more information read this article: http://goo.gl/ImC89E 59 | for wrong_symbol, expected in WRONG_UNICODE_SYMBOLS: 60 | render = render.replace(wrong_symbol, expected) 61 | 62 | render = bytes(render.encode('utf-8')) 63 | except Exception as exc: 64 | raise SerializerError(exc) 65 | return render 66 | 67 | 68 | class XMLRenderer(BaseRenderer): 69 | 70 | format = 'xml' 71 | xml_generator = SimpleXMLGenerator 72 | 73 | def render(self, data): 74 | """ 75 | Render input data into XML. 76 | 77 | :param data: dictionary or list object (response). 78 | """ 79 | try: 80 | render = StringIO() 81 | xml = self.xml_generator(render, self.charset) 82 | xml.parse(data) 83 | render = bytes(render.getvalue().encode('utf-8')) 84 | except Exception as exc: 85 | raise SerializerError(exc) 86 | return render 87 | -------------------------------------------------------------------------------- /docs/source/request.rst: -------------------------------------------------------------------------------- 1 | .. _aiorest-ws-request: 2 | 3 | Factories and protocols 4 | ======================= 5 | 6 | .. currentmodule:: aiorest_ws.request 7 | 8 | This documentation page contains information about :class:`RequestHandlerProtocol` 9 | and :class:`RequestHandlerFactory`, which provide mechanisms for transferring data 10 | through WebSockets, and their processing. 11 | 12 | RequestHandler protocol 13 | ----------------------- 14 | 15 | :class:`RequestHandlerProtocol` instance, created for every client connection. 16 | The instance of this class (which based on the Authobahn.ws implementation) 17 | processing client request asynchronously. This means that the protocol never 18 | waits for an event, but responds to events when they arrived from the network. 19 | 20 | Base algorithm for the default implementation, described by the next 21 | scheme: 22 | 23 | 1) Get a message (which can be JSON or base64 string) from the client. 24 | 25 | 2) Decode message and wrap him into :class:`Request` instance. 26 | 27 | 3) Process request through invoke routers ``process_request`` method. 28 | 29 | 4) Get response data from :class:`Response` instance and encode message. 30 | 31 | 5) Send result data (in the same form, which had taken at the 1st step) to the client. 32 | 33 | 34 | Also what necessary to know, when you're working with this protocol: 35 | 36 | 1) Protocols can retrieve the message, why a connection was terminated. 37 | 38 | 2) You can create multiple connections to a server. 39 | 40 | RequestHandler factory 41 | ---------------------- 42 | 43 | :class:`RequestHandlerFactory` is a class, which instantiates client connections. 44 | 45 | .. note:: 46 | 47 | Persistent configuration information is not saved in the instantiated 48 | protocol. For such cases kept data in a factory classes, databases, etc. 49 | 50 | This factory also provide a access for the client handlers (:class:`RequestHandlerProtocol` 51 | instances) to the general router, which used for processing users requests. 52 | 53 | If necessary provide any access to databases, cache storages or whatever you want, 54 | strongly recommend to use properties. For example it can be something like that: 55 | 56 | .. code-block:: python 57 | 58 | from aiorest_ws.request import RequestHandlerFactory 59 | 60 | class CustomHandlerFactory(RequestHandlerFactory): 61 | """Factory, which also provide access to the cookies.""" 62 | def __init__(self, *args, **kwargs): 63 | super(CustomHandlerFactory, self).__init__(*args, **kwargs) 64 | self._cookies = kwargs.get('cookies_dump', {}) 65 | 66 | @property 67 | def cookies(self): 68 | return self._cookies 69 | 70 | For more information about factories with WebSockets support, overriding behaviour of 71 | aiorest-ws :class:`RequestHandlerFactory` factory (which rely onto Authobahn.ws 72 | implementation) look into `Autobahn.ws documentation `_. 73 | -------------------------------------------------------------------------------- /aiorest_ws/db/orm/sqlalchemy/validators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Special validator classes and functions, applied for checking passed data. 4 | """ 5 | from aiorest_ws.db.orm.exceptions import ValidationError 6 | from aiorest_ws.db.orm.sqlalchemy.mixins import ORMSessionMixin 7 | from aiorest_ws.db.orm.validators import BaseValidator, \ 8 | BaseUniqueFieldValidator 9 | 10 | __all__ = ('ORMFieldValidator', 'UniqueORMValidator', ) 11 | 12 | 13 | class ORMFieldValidator(BaseValidator): 14 | message = u"Assertion error for `{field}` field with `{value}` value." 15 | 16 | def __init__(self, validator_func, model_field, field_name, **kwargs): 17 | super(ORMFieldValidator, self).__init__(**kwargs) 18 | self.validator_func = validator_func 19 | self.model_field = model_field 20 | self.field_name = field_name 21 | 22 | def __call__(self, value): 23 | try: 24 | self.validator_func(self.model_field, self.field_name, value) 25 | except AssertionError as exc: 26 | message = exc.args[0] if exc.args else self.message.format( 27 | field=self.field_name, value=value 28 | ) 29 | raise ValidationError(message) 30 | 31 | 32 | class UniqueORMValidator(BaseUniqueFieldValidator, ORMSessionMixin): 33 | """ 34 | Validator that corresponds to `unique=True` on a model field. 35 | Should be applied to an individual field on the serializer. 36 | """ 37 | def __init__(self, model, field_name, message=None): 38 | super(UniqueORMValidator, self).__init__(model, message) 39 | self.model = model 40 | self.field_name = field_name 41 | self.instance = None 42 | 43 | def filter_queryset(self, value, queryset): 44 | filter_field = getattr(self.model, self.field_name) 45 | return queryset.filter(filter_field == value) 46 | 47 | def exclude_current_instance(self, queryset): 48 | """ 49 | If an instance is being updated, then do not include 50 | that instance itself as a uniqueness conflict. 51 | """ 52 | if self.instance: 53 | pk_fields = [ 54 | column.name 55 | for column in self.instance.__mapper__.primary_key 56 | ] 57 | filter_args = [ 58 | getattr(self.model, attr) != getattr(self.instance, attr) 59 | for attr in pk_fields 60 | ] 61 | return queryset.filter(*filter_args) 62 | return queryset 63 | 64 | def __call__(self, value): 65 | session = self._get_session() 66 | try: 67 | queryset = session.query(self.model) 68 | queryset = self.filter_queryset(value, queryset) 69 | queryset = self.exclude_current_instance(queryset) 70 | 71 | if session.query(queryset.exists()).scalar(): 72 | raise ValidationError(self.message) 73 | finally: 74 | session.close() 75 | -------------------------------------------------------------------------------- /aiorest_ws/wrappers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Wrappers, similar on HTTP requests/responses. 4 | """ 5 | from aiorest_ws.utils.modify import add_property 6 | 7 | __all__ = ('Request', 'Response', ) 8 | 9 | 10 | class Request(object): 11 | 12 | def __init__(self, *args, **kwargs): 13 | super(Request, self).__init__() 14 | self._method = kwargs.pop('method', None) 15 | self._url = kwargs.pop('url', None) 16 | self._args = kwargs.pop('args', {}) 17 | self._data = kwargs.pop('data', None) 18 | self._event_name = kwargs.pop('event_name', None) 19 | 20 | for key in kwargs.keys(): 21 | add_property(self, key, kwargs[key]) 22 | 23 | @property 24 | def method(self): 25 | """ 26 | Get method type, which defined by the user. 27 | """ 28 | return self._method 29 | 30 | @property 31 | def url(self): 32 | """ 33 | Get the APIs url, from which expected response. 34 | """ 35 | return self._url 36 | 37 | @property 38 | def args(self): 39 | """ 40 | Get dictionary of arguments, defined by the user. 41 | """ 42 | return self._args 43 | 44 | @property 45 | def data(self): 46 | """ 47 | Get request body. 48 | """ 49 | return self._data 50 | 51 | @property 52 | def event_name(self): 53 | """ 54 | Get event name, which used by the client for processing response. 55 | """ 56 | return self._event_name 57 | 58 | def to_representation(self): 59 | """ 60 | Serialize request object to dictionary object. 61 | """ 62 | return {'event_name': self.event_name} 63 | 64 | def get_argument(self, name): 65 | """ 66 | Extracting argument from the request. 67 | 68 | :param name: name of extracted argument in dictionary. 69 | """ 70 | return self.args.get(name, None) if self.args else None 71 | 72 | 73 | class Response(object): 74 | 75 | def __init__(self): 76 | super(Response, self).__init__() 77 | self._content = {} 78 | 79 | @property 80 | def content(self): 81 | """ 82 | Get content of response. 83 | """ 84 | return self._content 85 | 86 | @content.setter 87 | def content(self, value): 88 | """ 89 | Set content for the response. 90 | """ 91 | self._content['data'] = value 92 | 93 | def wrap_exception(self, exception): 94 | """ 95 | Set content of response, when taken exception. 96 | """ 97 | self._content = {'detail': exception.detail} 98 | 99 | def append_request(self, request): 100 | """ 101 | Add to the response object serialized request. 102 | """ 103 | self._content.update(request.to_representation()) 104 | -------------------------------------------------------------------------------- /aiorest_ws/utils/encoding.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Special functions for additional and safety encoding passed data. 4 | """ 5 | import datetime 6 | from decimal import Decimal 7 | 8 | from aiorest_ws.utils.serializer_helpers import ReturnList, ReturnDict 9 | 10 | __all__ = ( 11 | '_PROTECTED_TYPES', 'is_protected_type', 'force_text', 12 | 'force_text_recursive', 13 | ) 14 | 15 | _PROTECTED_TYPES = ( 16 | str, type(None), float, Decimal, datetime.datetime, datetime.date, 17 | datetime.time 18 | ) 19 | 20 | 21 | def is_protected_type(obj): 22 | """ 23 | Determine if the object instance is of a protected type. 24 | Objects of protected types are preserved as-is when passed to 25 | force_text(strings_only=True). 26 | """ 27 | return isinstance(obj, _PROTECTED_TYPES) 28 | 29 | 30 | def force_text(s, encoding='utf-8', strings_only=False, errors='strict'): 31 | """ 32 | Similar to smart_text, except that lazy instances are resolved to 33 | strings, rather than kept as lazy objects. 34 | If strings_only is True, don't convert (some) non-string-like objects. 35 | """ 36 | # Handle the common case first for performance reasons 37 | if issubclass(type(s), str): 38 | return s 39 | if strings_only and is_protected_type(s): 40 | return s 41 | try: 42 | if not issubclass(type(s), str): 43 | if isinstance(s, bytes): 44 | s = str(s, encoding, errors) 45 | else: 46 | s = str(s) 47 | else: 48 | # Note: We use .decode() here, instead of six.text_type(s, 49 | # encoding, errors), so that if s is a SafeBytes, it ends up 50 | # being a SafeText at the end 51 | s = s.decode(encoding, errors) 52 | except UnicodeDecodeError: 53 | # If we get to here, the caller has passed in an Exception 54 | # subclass populated with non-ASCII bytestring data without a 55 | # working unicode method. Try to handle this without raising a 56 | # further exception by individually forcing the exception args 57 | # to unicode 58 | s = ' '.join(force_text(arg, encoding, strings_only, errors) 59 | for arg in s) 60 | return s 61 | 62 | 63 | def force_text_recursive(data): 64 | """ 65 | Descend into a nested data structure, forcing any 66 | lazy translation strings into plain text. 67 | """ 68 | if isinstance(data, list): 69 | ret = [force_text_recursive(item) for item in data] 70 | if isinstance(data, ReturnList): 71 | return ReturnList(ret, serializer=data.serializer) 72 | return data 73 | elif isinstance(data, dict): 74 | ret = { 75 | key: force_text_recursive(value) 76 | for key, value in data.items() 77 | } 78 | if isinstance(data, ReturnDict): 79 | return ReturnDict(ret, serializer=data.serializer) 80 | return data 81 | return force_text(data) 82 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/server/app/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.conf import settings 3 | from aiorest_ws.db.orm.exceptions import ValidationError 4 | from aiorest_ws.views import MethodBasedView 5 | 6 | from app.db import User 7 | from app.serializers import AddressSerializer, UserSerializer 8 | 9 | 10 | class UserListView(MethodBasedView): 11 | 12 | def get(self, request, *args, **kwargs): 13 | session = settings.SQLALCHEMY_SESSION() 14 | users = session.query(User).all() 15 | data = UserSerializer(users, many=True).data 16 | session.close() 17 | return data 18 | 19 | def post(self, request, *args, **kwargs): 20 | if not request.data: 21 | raise ValidationError('You must provide arguments for create.') 22 | 23 | if not isinstance(request.data, list): 24 | raise ValidationError('You must provide a list of objects.') 25 | 26 | serializer = UserSerializer(data=request.data, many=True) 27 | serializer.is_valid(raise_exception=True) 28 | serializer.save() 29 | return serializer.data 30 | 31 | 32 | class UserView(MethodBasedView): 33 | 34 | def get(self, request, id, *args, **kwargs): 35 | session = settings.SQLALCHEMY_SESSION() 36 | instance = session.query(User).filter(User.id == id).first() 37 | data = UserSerializer(instance).data 38 | session.close() 39 | return data 40 | 41 | def put(self, request, id, *args, **kwargs): 42 | if not request.data: 43 | raise ValidationError('You must provide an updated instance.') 44 | 45 | session = settings.SQLALCHEMY_SESSION() 46 | instance = session.query(User).filter(User.id == id).first() 47 | if not instance: 48 | raise ValidationError('Object does not exist') 49 | 50 | serializer = UserSerializer(instance, data=request.data, partial=True) 51 | serializer.is_valid(raise_exception=True) 52 | serializer.save() 53 | data = serializer.data 54 | session.close() 55 | return data 56 | 57 | 58 | class CreateUserView(MethodBasedView): 59 | 60 | def post(self, request, *args, **kwargs): 61 | serializer = UserSerializer(data=request.data) 62 | serializer.is_valid(raise_exception=True) 63 | serializer.save() 64 | return serializer.data 65 | 66 | 67 | class AddressView(MethodBasedView): 68 | 69 | def get(self, request, id, *args, **kwargs): 70 | session = settings.SQLALCHEMY_SESSION() 71 | instance = session.query(User).filter(User.id == id).first() 72 | data = AddressSerializer(instance).data 73 | session.close() 74 | return data 75 | 76 | 77 | class CreateAddressView(MethodBasedView): 78 | 79 | def post(self, request, *args, **kwargs): 80 | serializer = AddressSerializer(data=request.data) 81 | serializer.is_valid(raise_exception=True) 82 | serializer.save() 83 | return serializer.data 84 | -------------------------------------------------------------------------------- /aiorest_ws/db/orm/django/validators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | We perform uniqueness checks explicitly on the serializer class, rather 4 | the using Django's `.full_clean()`. 5 | 6 | This gives us better separation of concerns, allows us to use single-step 7 | object creation, and makes it possible to switch between using the implicit 8 | `ModelSerializer` class and an equivalent explicit `Serializer` class. 9 | """ 10 | from django.db import DataError 11 | from django.utils.translation import ugettext_lazy as _ 12 | 13 | from aiorest_ws.db.orm.exceptions import ValidationError 14 | from aiorest_ws.utils.representation import smart_repr 15 | 16 | __all__ = ('qs_exists', 'qs_filter', 'UniqueValidator', ) 17 | 18 | 19 | def qs_exists(queryset): 20 | try: 21 | return queryset.exists() 22 | except (TypeError, ValueError, DataError): 23 | return False 24 | 25 | 26 | def qs_filter(queryset, **kwargs): 27 | try: 28 | return queryset.filter(**kwargs) 29 | except (TypeError, ValueError, DataError): 30 | return queryset.none() 31 | 32 | 33 | class UniqueValidator(object): 34 | """ 35 | Validator that corresponds to `unique=True` on a model field. 36 | Should be applied to an individual field on the serializer. 37 | """ 38 | message = _('This field must be unique.') 39 | 40 | def __init__(self, queryset, message=None): 41 | self.queryset = queryset 42 | self.serializer_field = None 43 | self.message = message or self.message 44 | 45 | def set_context(self, serializer_field): 46 | """ 47 | This hook is called by the serializer instance, 48 | prior to the validation call being made. 49 | """ 50 | # Determine the underlying model field name. This may not be the 51 | # same as the serializer field name if `source=<>` is set 52 | self.field_name = serializer_field.source_attrs[-1] 53 | # Determine the existing instance, if this is an update operation 54 | self.instance = getattr(serializer_field.parent, 'instance', None) 55 | 56 | def filter_queryset(self, value, queryset): 57 | """ 58 | Filter the queryset to all instances matching the given attribute. 59 | """ 60 | filter_kwargs = {self.field_name: value} 61 | return qs_filter(queryset, **filter_kwargs) 62 | 63 | def exclude_current_instance(self, queryset): 64 | """ 65 | If an instance is being updated, then do not include 66 | that instance itself as a uniqueness conflict. 67 | """ 68 | if self.instance is not None: 69 | return queryset.exclude(pk=self.instance.pk) 70 | return queryset 71 | 72 | def __call__(self, value): 73 | queryset = self.queryset 74 | queryset = self.filter_queryset(value, queryset) 75 | queryset = self.exclude_current_instance(queryset) 76 | if qs_exists(queryset): 77 | raise ValidationError(self.message) 78 | 79 | def __repr__(self): 80 | return repr('<%s(queryset=%s)>' % ( 81 | self.__class__.__name__, 82 | smart_repr(self.queryset) 83 | )) 84 | -------------------------------------------------------------------------------- /docs/source/routing.rst: -------------------------------------------------------------------------------- 1 | .. _aiorest-ws-routing: 2 | 3 | Routing 4 | ======= 5 | 6 | .. currentmodule:: aiorest_ws.routers 7 | 8 | Register endpoints 9 | ------------------ 10 | 11 | The basic implementation :class:`SimpleRouter` have support to register 12 | endpoints with function or method-based views. 13 | 14 | Example of using for the endpoint with method-based view: 15 | 16 | .. code-block:: python 17 | 18 | from aiorest_ws.routers import SimpleRouter 19 | from aiorest_ws.views import MethodBasedView 20 | 21 | class MethodView(MethodBasedView): 22 | def get(self, request, *args, **kwargs): 23 | return "aiorest-ws is awesome!" 24 | 25 | router = SimpleRouter() 26 | router.register('/test_view', MethodView, 'GET') 27 | 28 | and the same, but with function-based view: 29 | 30 | .. code-block:: python 31 | 32 | from aiorest_ws.decorators import endpoint 33 | from aiorest_ws.routers import SimpleRouter 34 | 35 | @endpoint(path='/test_view', methods='GET') 36 | def function_based_view(request, *args, **kwargs): 37 | return "aiorest-ws is awesome!" 38 | 39 | router = SimpleRouter() 40 | router.register(function_based_view) 41 | 42 | Request processing 43 | ------------------ 44 | 45 | When client send a request to the our API, he will have 46 | processed by the following algorithm: 47 | 48 | 1. Get the URL from request 49 | 2. Search handler by received URL 50 | 3. If handler was found, do the next steps: 51 | 52 | 3.1. Make pre-processing of request, via invoke middlewares 53 | 54 | 3.2. Search suitable serializer for generating response 55 | 56 | 3.3. Processing request by calling specified method from the view 57 | 58 | 4. Serialize response 59 | 5. Return response 60 | 61 | Merge endpoint lists 62 | -------------------- 63 | 64 | In situations, when you separate functionality of your application on a parts, 65 | can be useful to union endpoints into one router, which will have been used for 66 | processing client requests further. 67 | 68 | :class:`SimpleRouter` have ``include`` method, which provide a mechanism, which copy endpoints 69 | from another router into himself. 70 | 71 | Example of using: 72 | 73 | .. code-block:: python 74 | 75 | 76 | # hello_routing.py 77 | 78 | from aiorest_ws.routers import SimpleRouter 79 | from aiorest_ws.views import MethodBasedView 80 | 81 | class HelloWorld(MethodBasedView): 82 | def get(self, request, *args, **kwargs): 83 | return "Hello from hello_routing.py!" 84 | 85 | hello_router = SimpleRouter() 86 | hello_router.register('/hello_module/hello', HelloWorld, 'GET') 87 | 88 | # main_routing.py 89 | 90 | from hello_routing import hello_router 91 | 92 | from aiorest_ws.routers import SimpleRouter 93 | from aiorest_ws.views import MethodBasedView 94 | 95 | class HelloWorld(MethodBasedView): 96 | def get(self, request, *args, **kwargs): 97 | return return "Hello, world!" 98 | 99 | main_router = SimpleRouter() 100 | main_router.register('/hello', HelloWorld, 'GET') 101 | # after this line of code main_router will be contains 2 endpoints 102 | main_router.include(hello_router) 103 | -------------------------------------------------------------------------------- /examples/auth_token/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | os.environ.setdefault("AIORESTWS_SETTINGS_MODULE", "settings") 4 | 5 | from aiorest_ws.auth.token.managers import JSONWebTokenManager 6 | from aiorest_ws.auth.token.backends import InMemoryTokenBackend 7 | from aiorest_ws.auth.token.middlewares import JSONWebTokenMiddleware 8 | from aiorest_ws.auth.user.models import UserSQLiteModel 9 | from aiorest_ws.app import Application 10 | from aiorest_ws.command_line import CommandLine 11 | from aiorest_ws.routers import SimpleRouter 12 | from aiorest_ws.views import MethodBasedView 13 | 14 | 15 | class RegisterUser(MethodBasedView): 16 | user_manager = UserSQLiteModel() 17 | 18 | def post(self, request, *args, **kwargs): 19 | user = self.user_manager.get_user_by_username(request.data['username']) 20 | # only anonymous doesn't have username 21 | if user.is_authenticated: 22 | message = "User already created." 23 | else: 24 | self.user_manager.create_user(**request.data) 25 | message = "User created successfully." 26 | return message 27 | 28 | 29 | class LogIn(MethodBasedView): 30 | token_manager = JSONWebTokenManager() 31 | token_backend = InMemoryTokenBackend() 32 | user_manager = UserSQLiteModel() 33 | 34 | def get_or_create_token(self, user, *args, **kwargs): 35 | 36 | def get_token(user): 37 | return self.token_backend.get_token_by_username( 38 | 'admin', username=user.username 39 | ) 40 | 41 | def create_token(user, *args, **kwargs): 42 | jwt_kwargs = { 43 | "iss": "aiorest-ws", 44 | "exp": 60, 45 | "name": kwargs.get('username'), 46 | "authorized": True, 47 | "token_name": "admin" 48 | } 49 | kwargs.update(jwt_kwargs) 50 | api_token = self.token_manager.generate({}, **kwargs) 51 | self.token_backend.save('admin', api_token, user_id=user.id) 52 | return api_token 53 | 54 | return get_token(user) or create_token(user, *args, **kwargs) 55 | 56 | def post(self, request, *args, **kwargs): 57 | user = self.user_manager.get_user_by_username( 58 | request.data['username'], with_id=True 59 | ) 60 | if user.is_authenticated: 61 | api_token = self.get_or_create_token(user, *args, **kwargs) 62 | else: 63 | api_token = None 64 | return {'token': api_token} 65 | 66 | 67 | class LogOut(MethodBasedView): 68 | auth_required = True 69 | 70 | def post(self, request, *args, **kwargs): 71 | return "Successful log out." 72 | 73 | 74 | router = SimpleRouter() 75 | router.register('/auth/user/create', RegisterUser, 'POST') 76 | router.register('/auth/login', LogIn, 'POST') 77 | router.register('/auth/logout', LogOut, 'POST') 78 | 79 | 80 | if __name__ == '__main__': 81 | cmd = CommandLine() 82 | cmd.define('-ip', default='127.0.0.1', help='used ip', type=str) 83 | cmd.define('-port', default=8080, help='listened port', type=int) 84 | args = cmd.parse_command_line() 85 | 86 | app = Application(middlewares=(JSONWebTokenMiddleware, )) 87 | app.run(host=args.ip, port=args.port, router=router) 88 | -------------------------------------------------------------------------------- /tests/utils/test_fields.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | from collections import OrderedDict 4 | 5 | from aiorest_ws.utils.fields import is_simple_callable, method_overridden, \ 6 | get_attribute, set_value, to_choices_dict, flatten_choices_dict 7 | 8 | 9 | class FakeClass(object): 10 | attr = 'value' 11 | 12 | def get_object(self): 13 | return self 14 | 15 | @staticmethod 16 | def get_true(): 17 | return True 18 | 19 | def base_method(self): 20 | raise NotImplementedError() 21 | 22 | def attribute_error(self): 23 | raise AttributeError() 24 | 25 | def key_error(self): 26 | raise KeyError() 27 | 28 | 29 | class DerivedFakeClass(FakeClass): 30 | 31 | def base_method(self): 32 | return self.attr 33 | 34 | 35 | @pytest.mark.parametrize("value, expected", [ 36 | (FakeClass(), False), 37 | (FakeClass().get_object, True), 38 | (FakeClass.get_true, True), 39 | ]) 40 | def test_is_simple_callable(value, expected): 41 | assert is_simple_callable(value) == expected 42 | 43 | 44 | @pytest.mark.parametrize("method_name, cls, instance, expected", [ 45 | ('base_method', FakeClass, FakeClass(), False), 46 | ('base_method', FakeClass, DerivedFakeClass(), True), 47 | ]) 48 | def test_method_overridden(method_name, cls, instance, expected): 49 | assert method_overridden(method_name, cls, instance) == expected 50 | 51 | 52 | @pytest.mark.parametrize("instance, attrs, expected", [ 53 | (None, ['key', ], None), 54 | ('test', [], 'test'), 55 | ({'key': 'value'}, ['key', ], 'value'), 56 | ({'key': {'key2': 'value'}}, ['key', 'key2'], 'value'), 57 | (FakeClass, ['attr', ], 'value'), 58 | (FakeClass, ['get_true', ], True), 59 | ]) 60 | def test_get_attribute(instance, attrs, expected): 61 | assert get_attribute(instance, attrs) == expected 62 | 63 | 64 | @pytest.mark.parametrize("instance, attrs, exc_cls", [ 65 | (FakeClass(), ['attribute_error', ], ValueError), 66 | (FakeClass(), ['key_error', ], ValueError), 67 | ]) 68 | def test_get_attribute_failed(instance, attrs, exc_cls): 69 | with pytest.raises(exc_cls): 70 | get_attribute(instance, attrs) 71 | 72 | 73 | @pytest.mark.parametrize("dictionary, keys, value, expected", [ 74 | ({'a': 1}, [], {'b': 2}, {'a': 1, 'b': 2}), 75 | ({'a': 1}, ['x'], 2, {'a': 1, 'x': 2}), 76 | ({'a': 1}, ['x', 'y'], 2, {'a': 1, 'x': {'y': 2}}) 77 | ]) 78 | def test_set_value(dictionary, keys, value, expected): 79 | set_value(dictionary, keys, value) 80 | assert dictionary == expected 81 | 82 | 83 | @pytest.mark.parametrize("value, expected", [ 84 | ([1, 2, 3], {1: 1, 2: 2, 3: 3}), 85 | ([(1, '1st'), (2, '2nd')], {1: '1st', 2: '2nd'}), 86 | ([('Group', ((1, '1'), 2))], 87 | OrderedDict([('Group', OrderedDict([(1, '1'), (2, 2)]))]) 88 | ), 89 | ]) 90 | def test_to_choices_dict(value, expected): 91 | assert to_choices_dict(value) == expected 92 | 93 | 94 | @pytest.mark.parametrize("value, expected", [ 95 | ({1: '1st', 2: '2nd'}, {1: '1st', 2: '2nd'}), 96 | ({'Key': {1: '1st', 2: '2nd'}}, {1: '1st', 2: '2nd'}), 97 | ]) 98 | def test_flatten_choices_dict(value, expected): 99 | assert flatten_choices_dict(value) == expected 100 | -------------------------------------------------------------------------------- /examples/django_orm/server/app/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.db.orm.exceptions import ValidationError 3 | from aiorest_ws.views import MethodBasedView 4 | 5 | from django.core.exceptions import ObjectDoesNotExist 6 | 7 | from app.db import Manufacturer, Car 8 | from app.serializers import ManufacturerSerializer, CarSerializer 9 | 10 | 11 | class ManufacturerListView(MethodBasedView): 12 | 13 | def get(self, request, *args, **kwargs): 14 | instances = Manufacturer.objects.all() 15 | serializer = ManufacturerSerializer(instances, many=True) 16 | return serializer.data 17 | 18 | def post(self, request, *args, **kwargs): 19 | if not request.data: 20 | raise ValidationError('You must provide arguments for create.') 21 | 22 | serializer = ManufacturerSerializer(data=request.data) 23 | serializer.is_valid(raise_exception=True) 24 | serializer.save() 25 | return serializer.data 26 | 27 | 28 | class ManufacturerView(MethodBasedView): 29 | 30 | def get_manufacturer(self, name): 31 | try: 32 | manufacturer = Manufacturer.objects.get(name__iexact=name) 33 | except ObjectDoesNotExist: 34 | raise ValidationError("The requested object does not exist") 35 | 36 | return manufacturer 37 | 38 | def get(self, request, name, *args, **kwargs): 39 | manufacturer = self.get_manufacturer(name) 40 | serializer = ManufacturerSerializer(manufacturer) 41 | return serializer.data 42 | 43 | def put(self, request, name, *args, **kwargs): 44 | if not request.data: 45 | raise ValidationError('You must provide arguments for create.') 46 | 47 | instance = self.get_manufacturer(name) 48 | serializer = ManufacturerSerializer(instance, data=request.data, partial=True) 49 | serializer.is_valid(raise_exception=True) 50 | serializer.save() 51 | return serializer.data 52 | 53 | 54 | class CarListView(MethodBasedView): 55 | 56 | def get(self, request, *args, **kwargs): 57 | data = Car.objects.all() 58 | serializer = CarSerializer(data, many=True) 59 | return serializer.data 60 | 61 | def post(self, request, *args, **kwargs): 62 | if not request.data: 63 | raise ValidationError('You must provide arguments for create.') 64 | 65 | serializer = CarSerializer(data=request.data) 66 | serializer.is_valid(raise_exception=True) 67 | serializer.save() 68 | return serializer.data 69 | 70 | 71 | class CarView(MethodBasedView): 72 | 73 | def get_car(self, name): 74 | try: 75 | car = Car.objects.get(name__iexact=name) 76 | except ObjectDoesNotExist: 77 | raise ValidationError("The requested object does not exist") 78 | 79 | return car 80 | 81 | def get(self, request, name, *args, **kwargs): 82 | instance = self.get_car(name) 83 | serializer = CarSerializer(instance) 84 | return serializer.data 85 | 86 | def put(self, request, name, *args, **kwargs): 87 | if not request.data: 88 | raise ValidationError('You must provide data for update.') 89 | 90 | instance = self.get_car(name) 91 | serializer = CarSerializer(instance, data=request.data, partial=True) 92 | serializer.is_valid(raise_exception=True) 93 | serializer.save() 94 | return serializer.data 95 | -------------------------------------------------------------------------------- /aiorest_ws/request.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Classes and function for creating and processing requests from user. 4 | """ 5 | import asyncio 6 | import json 7 | from base64 import b64encode, b64decode 8 | 9 | from autobahn.asyncio.websocket import WebSocketServerProtocol, \ 10 | WebSocketServerFactory 11 | 12 | from aiorest_ws.abstract import AbstractRouter 13 | from aiorest_ws.routers import SimpleRouter 14 | from aiorest_ws.validators import check_and_set_subclass 15 | from aiorest_ws.wrappers import Request 16 | 17 | __all__ = ('RequestHandlerProtocol', 'RequestHandlerFactory', ) 18 | 19 | 20 | class RequestHandlerProtocol(WebSocketServerProtocol): 21 | """ 22 | REST WebSocket protocol instance, creating for every client connection. 23 | This protocol describe how to process network events (users requests to 24 | APIs) asynchronously. 25 | """ 26 | def _decode_message(self, payload, isBinary=False): 27 | """ 28 | Decoding input message to Request object. 29 | 30 | :param payload: input message. 31 | :param isBinary: boolean value, means that received data had a binary 32 | format. 33 | """ 34 | # Message was taken in base64 35 | if isBinary: 36 | payload = b64decode(payload) 37 | input_data = json.loads(payload.decode('utf-8')) 38 | return Request(**input_data) 39 | 40 | def _encode_message(self, response, isBinary=False): 41 | """ 42 | Encoding output message. 43 | 44 | :param response: output message. 45 | :param isBinary: boolean value, means that received data had a binary 46 | format. 47 | """ 48 | # Encode additionally to base64 if necessary 49 | if isBinary: 50 | response = b64encode(response) 51 | return response 52 | 53 | @asyncio.coroutine 54 | def onMessage(self, payload, isBinary): 55 | """ 56 | Handler, called for every message which was sent from the some user. 57 | 58 | :param payload: input message. 59 | :param isBinary: boolean value, means that received data had a binary 60 | format. 61 | """ 62 | request = self._decode_message(payload, isBinary) 63 | response = self.factory.router.process_request(request) 64 | out_payload = self._encode_message(response, isBinary) 65 | self.sendMessage(out_payload, isBinary=isBinary) 66 | 67 | 68 | class RequestHandlerFactory(WebSocketServerFactory): 69 | """ 70 | REST WebSocket server factory, which instantiates client connections. 71 | 72 | NOTE: Persistent configuration information is not saved in the instantiated 73 | protocol. For such cases kept data in a Factory classes, databases, etc. 74 | """ 75 | def __init__(self, *args, **kwargs): 76 | super(RequestHandlerFactory, self).__init__(*args, **kwargs) 77 | self._router = kwargs.get('router', SimpleRouter(*args, **kwargs)) 78 | 79 | @property 80 | def router(self): 81 | """ 82 | Get router instance. 83 | """ 84 | return self._router 85 | 86 | @router.setter 87 | def router(self, router): 88 | """ 89 | Set router instance. 90 | 91 | :param router: instance of class, which derived from AbstractRouter. 92 | """ 93 | if router: 94 | check_and_set_subclass(self, '_router', router, AbstractRouter) 95 | -------------------------------------------------------------------------------- /aiorest_ws/urls/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Utility module for work with _urlconf variable. 4 | """ 5 | import re 6 | 7 | from aiorest_ws.parsers import DYNAMIC_PARAMETER 8 | from aiorest_ws.urls.base import get_urlconf 9 | from aiorest_ws.urls.exceptions import NoReverseMatch, NoMatch 10 | from aiorest_ws.utils.encoding import force_text 11 | 12 | __all__ = ( 13 | 'RouteMatch', '_generate_url_parameters', 'reverse', 'resolve' 14 | ) 15 | 16 | 17 | class RouteMatch(object): 18 | 19 | def __init__(self, view_name, args=(), kwargs={}): 20 | super(RouteMatch, self).__init__() 21 | self.view_name = view_name 22 | self.args = args 23 | self.kwargs = kwargs 24 | 25 | 26 | def _generate_url_parameters(parameters): 27 | format_parameters = ( 28 | "{}={}".format(key, value) 29 | for key, value in parameters.items() 30 | ) 31 | return '?' + '&'.join(format_parameters) 32 | 33 | 34 | def resolve(path, urlconf=None): 35 | """ 36 | Return the endpoint corresponding to a matched URL. 37 | """ 38 | if urlconf is None: 39 | urlconf = get_urlconf() 40 | 41 | # Convert absolute path to relative 42 | path = path.replace(urlconf['path'], '') 43 | 44 | # Iterate over the all endpoints 45 | for route in urlconf.get('urls', []): 46 | # Find match between endpoint and passed URL 47 | match = route.match(path) 48 | if match is not None: 49 | kwargs = {} 50 | if route._pattern: 51 | kwargs = route._pattern.match(path).groupdict() 52 | return RouteMatch(route.name, args=match, kwargs=kwargs) 53 | raise NoMatch() 54 | 55 | 56 | def reverse(view_name, urlconf=None, args=[], kwargs={}, relative=False): 57 | """ 58 | Generate URL to endpoint, which processing by Application instance, based 59 | on the `view_name` and passed arguments. 60 | 61 | :param view_name: view name. 62 | :param urlconf: urlconf instance (dictionary). 63 | :param args: tuple of data, which used by handler with this URL. 64 | :param kwargs: named arguments for the defined URL. 65 | :return: generated URL with the passed arguments. 66 | """ 67 | if urlconf is None: 68 | urlconf = get_urlconf() 69 | 70 | root_path = '' 71 | api_path = '' 72 | try: 73 | # Get root path, when necessary to generate absolute URL 74 | root_path = '' if relative else urlconf['path'].strip('/') 75 | 76 | # Get path to a specific endpoint 77 | route = urlconf['routes'][view_name] 78 | api_path = route.path.strip('/') 79 | 80 | # Replace parameters in 'api_url' on the passed args 81 | dynamic_parameters = re.findall(DYNAMIC_PARAMETER, api_path) 82 | if len(dynamic_parameters) != len(args): 83 | raise ValueError( 84 | "Endpoint '{path}' must take {valid_count} parameters, " 85 | "but passed {invalid_count}.".format( 86 | path=api_path, 87 | valid_count=len(dynamic_parameters), 88 | invalid_count=len(args) 89 | ) 90 | ) 91 | for parameter, value in zip(dynamic_parameters, args): 92 | api_path = api_path.replace(parameter, value) 93 | except KeyError: 94 | raise NoReverseMatch() 95 | 96 | url_parameters = _generate_url_parameters(kwargs) if kwargs else "" 97 | url = '/'.join([root_path, api_path, url_parameters]) 98 | return force_text(url) 99 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Instructions for contributors 5 | ----------------------------- 6 | 7 | Go to [aiorest-ws](https://github.com/Relrin/aiorest-ws) project repository and press ["Fork"](https://github.com/Relrin/aiorest-ws#fork-destination-box) button on the upper-right menu of the web page. 8 | 9 | I'm hope, that most of all, who read this "How to ..." knows how to work with GitHub. 10 | 11 | Workflow is very simple: 12 | 13 | > 1. Clone the aiorest-ws repository 14 | > 2. Make a changes in your own repository 15 | > 3. Make sure all tests passed 16 | > 4. Commit changes to own aiorest-ws clone 17 | > 5. Make pull request from GitHub page for your clone 18 | 19 | Creating your own enviroment for development 20 | -------------------------------------------- 21 | 22 | Before starting development necessary install Python 3, pip and some packages. 23 | 24 | We expect you to use a python virtual environment to run our tests. 25 | 1) Install Python 3 with pip 26 | - Linux 27 | ```bash 28 | wget https://www.python.org/ftp/python/3.4.3/Python-3.4.3.tar.xz 29 | tar xf Python-3.* 30 | cd Python-3.* 31 | ./configure 32 | make 33 | make install 34 | pip install virtualenv 35 | ``` 36 | - Mac OS X 37 | ```bash 38 | brew update 39 | brew install python3 40 | pip3 install virtualenv 41 | ``` 42 | - Windows 43 | Download lastest stable release of Python 3rd branch from [Python language website](https://www.python.org/) and install it. 44 | 45 | 2) Create virtual environment via console or terminal (for more details read [official documentation](https://virtualenv.pypa.io/en/latest/) for virtualenv) 46 | 47 | We have a several ways to create a virtual environment. 48 | 49 | For **virtualenv** use this: 50 | ```bash 51 | cd aiorest_ws 52 | virtualenv --python=`which python3` venv 53 | ``` 54 | For standard python **venv**: 55 | ```bash 56 | cd aiorest_ws 57 | python3 -m venv venv 58 | ``` 59 | For **virtualenvwrapper**: 60 | ```bash 61 | cd aiorest_ws 62 | mkvirtualenv --python=`which python3` aiorest_ws 63 | ``` 64 | 3) Activate your virtual environment 65 | ```bash 66 | source bin/activate 67 | ``` 68 | P.S. For exit from the virtualenv use ```deactivate``` command. 69 | 4) Install requirements for development from the root directory of project 70 | ``` 71 | pip install -r requirements.txt 72 | ``` 73 | 5) Done! Now you can make your own changes in code. :) 74 | 75 | Debugging 76 | --------- 77 | For debug cases I prefer to use **pudb** in a pair with **ipython**, which already installed on the 4th step of making your own environment. 78 | In code, when necessary pause programm and start debug with Step In/Out, paste 79 | ```python 80 | import pudb; pudb.set_trace() 81 | ``` 82 | 83 | Testing 84 | ------- 85 | We have few ways to start test suites: 86 | 87 | 1. Using make tool 88 | a. In single thread use ```make test``` 89 | b. In parallel (4 threads) ```make test_parallel``` 90 | 2. Using ```runtests.py``` script (single-threaded): 91 | ```python 92 | python tests/runtests.py 93 | ``` 94 | 95 | P.S. For debug cases use single thread approach. 96 | 97 | 98 | For future pull requests 99 | ------------------------ 100 | For every part of code, which you will make as PR(=Pull Request), necessary to satisfy few conditions: 101 | 1. All previous tests are passed. 102 | 2. If necessary (in most situations it really does important) write test for cover your own part of code. 103 | 3. ```make flake``` doesn't print any errors/warning. 104 | -------------------------------------------------------------------------------- /examples/django_orm/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import json 4 | 5 | from hashlib import sha256 6 | from autobahn.asyncio.websocket import WebSocketClientProtocol, \ 7 | WebSocketClientFactory 8 | 9 | 10 | def hash_password(password): 11 | return sha256(password.encode('utf-8')).hexdigest() 12 | 13 | 14 | class HelloClientProtocol(WebSocketClientProtocol): 15 | 16 | def onOpen(self): 17 | # Create new manufacturer 18 | request = { 19 | 'method': 'POST', 20 | 'url': '/manufacturer/', 21 | 'data': { 22 | "name": 'Ford' 23 | }, 24 | 'event_name': 'create-manufacturer' 25 | } 26 | self.sendMessage(json.dumps(request).encode('utf8')) 27 | 28 | # Get information about Audi 29 | request = { 30 | 'method': 'GET', 31 | 'url': '/manufacturer/Audi/', 32 | 'event_name': 'get-manufacturer-detail' 33 | } 34 | self.sendMessage(json.dumps(request).encode('utf8')) 35 | 36 | # Get cars list 37 | request = { 38 | 'method': 'GET', 39 | 'url': '/cars/', 40 | 'event_name': 'get-cars-list' 41 | } 42 | self.sendMessage(json.dumps(request).encode('utf8')) 43 | 44 | # Create new car 45 | request = { 46 | 'method': 'POST', 47 | 'url': '/cars/', 48 | 'data': { 49 | 'name': 'M5', 50 | 'manufacturer': 2 51 | }, 52 | 'event_name': 'create-car' 53 | } 54 | self.sendMessage(json.dumps(request).encode('utf8')) 55 | 56 | # Trying to create new car with same info, but we have taken an error 57 | self.sendMessage(json.dumps(request).encode('utf8')) 58 | 59 | # # Update existing object 60 | request = { 61 | 'method': 'PUT', 62 | 'url': '/cars/Q5/', 63 | 'data': { 64 | 'name': 'Q7' 65 | }, 66 | 'event_name': 'partial-update-car' 67 | } 68 | self.sendMessage(json.dumps(request).encode('utf8')) 69 | 70 | # Get the list of manufacturers 71 | request = { 72 | 'method': 'GET', 73 | 'url': '/manufacturer/', 74 | 'event_name': 'get-manufacturer-list' 75 | } 76 | self.sendMessage(json.dumps(request).encode('utf8')) 77 | 78 | # Update manufacturer 79 | request = { 80 | 'method': 'PUT', 81 | 'url': '/manufacturer/Audi/', 82 | 'data': { 83 | 'name': 'Not Audi' 84 | }, 85 | 'event_name': 'update-manufacturer' 86 | } 87 | self.sendMessage(json.dumps(request).encode('utf8')) 88 | 89 | # Get car by name 90 | request = { 91 | 'method': 'GET', 92 | 'url': '/cars/TT/', 93 | 'event_name': 'get-car-detail' 94 | } 95 | self.sendMessage(json.dumps(request).encode('utf8')) 96 | 97 | def onMessage(self, payload, isBinary): 98 | print("Result: {0}".format(payload.decode('utf8'))) 99 | 100 | 101 | if __name__ == '__main__': 102 | factory = WebSocketClientFactory("ws://localhost:8080") 103 | factory.protocol = HelloClientProtocol 104 | 105 | loop = asyncio.get_event_loop() 106 | coro = loop.create_connection(factory, '127.0.0.1', 8080) 107 | loop.run_until_complete(coro) 108 | loop.run_forever() 109 | loop.close() 110 | -------------------------------------------------------------------------------- /aiorest_ws/conf/global_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Default settings for configuration behaviour of aiorest-ws framework. 4 | """ 5 | 6 | # ----------------------------------------------- 7 | # Basic settings 8 | # ----------------------------------------------- 9 | # Default datetime input and output formats 10 | ISO_8601 = 'iso-8601' 11 | DATE_FORMAT = ISO_8601 12 | DATE_INPUT_FORMATS = (ISO_8601, ) 13 | 14 | DATETIME_FORMAT = ISO_8601 15 | DATETIME_INPUT_FORMATS = (ISO_8601, ) 16 | 17 | TIME_FORMAT = ISO_8601 18 | TIME_INPUT_FORMATS = (ISO_8601,) 19 | 20 | # Local time zone for this installation. All choices can be found here: 21 | # https://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all 22 | # systems may support all possibilities). When USE_TZ is True, this is 23 | # interpreted as the default user time zone 24 | TIME_ZONE = 'America/Chicago' 25 | 26 | # If you set this to True, Django will use timezone-aware datetimes 27 | USE_TZ = False 28 | 29 | # Encoding charset for string, files, etc. 30 | DEFAULT_ENCODING = 'utf-8' 31 | 32 | UNICODE_JSON = True 33 | COMPACT_JSON = True 34 | COERCE_DECIMAL_TO_STRING = True 35 | UPLOADED_FILES_USE_URL = True 36 | 37 | # ----------------------------------------------- 38 | # Database 39 | # ----------------------------------------------- 40 | # This dictionary very useful, cause we can customize it and set required 41 | # drivers, paths, credentials, for every user database 42 | DATABASES = {} 43 | USE_ORM_ENGINE = False 44 | 45 | # SQLAlchemy ORM variables 46 | SQLALCHEMY_ENGINE = None 47 | SQLALCHEMY_SESSION = None 48 | 49 | # ----------------------------------------------- 50 | # REST configuration 51 | # ----------------------------------------------- 52 | REST_CONFIG = { 53 | # Exception handling 54 | 'NON_FIELD_ERRORS_KEY': 'non_field_errors', 55 | 56 | # Hyperlink settings 57 | 'URL_FIELD_NAME': 'url', 58 | } 59 | 60 | # ----------------------------------------------- 61 | # Middleware 62 | # ----------------------------------------------- 63 | # List of middleware classes, which assigned for the main router. All 64 | # middlewares will be used in the order of enumeration. Keep in mind, when 65 | # use this feature 66 | MIDDLEWARE_CLASSES = () 67 | 68 | # ----------------------------------------------- 69 | # Logging 70 | # ----------------------------------------------- 71 | DEFAULT_LOGGING_SETTINGS = { 72 | 'version': 1, 73 | 'disable_existing_loggers': False, 74 | 'formatters': { 75 | 'standard': { 76 | 'format': "[%(asctime)s] [%(levelname)s] %(message)s", 77 | 'datefmt': "%d/%b/%Y %H:%M:%S" 78 | }, 79 | 'verbose': { 80 | 'format': "[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)s] " 81 | "%(message)s", 82 | 'datefmt': "%d/%b/%Y %H:%M:%S" 83 | }, 84 | }, 85 | 'handlers': { 86 | 'default': { 87 | 'level': 'INFO', 88 | 'class': 'logging.StreamHandler', 89 | 'formatter': 'standard', 90 | }, 91 | 'debug': { 92 | 'level': 'DEBUG', 93 | 'class': 'logging.StreamHandler', 94 | 'formatter': 'verbose', 95 | }, 96 | }, 97 | 'loggers': { 98 | 'aiorest-ws': { 99 | 'handlers': ['default', ], 100 | 'level': 'INFO', 101 | 'propagate': False 102 | }, 103 | 'debug': { 104 | 'handlers': ['debug', ], 105 | 'level': 'DEBUG', 106 | 'propagate': False 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /aiorest_ws/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Handled exceptions raised by aiorest-ws framework, which inspired under 4 | Django REST framework. 5 | """ 6 | from aiorest_ws.status import WS_PROTOCOL_ERROR, WS_DATA_CANNOT_ACCEPT 7 | from aiorest_ws.utils.encoding import force_text 8 | 9 | __all__ = ( 10 | 'ImproperlyConfigured', 'BaseAPIException', 'EndpointValueError', 11 | 'IncorrectArgument', 'IncorrectMethodNameType', 'InvalidHandler', 12 | 'InvalidPathArgument', 'InvalidRenderer', 'NotImplementedMethod', 13 | 'NotSpecifiedError', 'NotSpecifiedHandler', 'NotSpecifiedMethodName', 14 | 'NotSpecifiedURL', 'NotSupportedArgumentType', 'SerializerError', 15 | ) 16 | 17 | 18 | class ImproperlyConfigured(Exception): 19 | """ 20 | Exception for the cases, when user has configured some class, setting or 21 | any other required parameter improperly. 22 | """ 23 | pass 24 | 25 | 26 | class BaseAPIException(Exception): 27 | """ 28 | Base class for aiorest-ws framework exceptions. 29 | 30 | All subclasses should provide `.status_code` and `.default_detail` 31 | properties. 32 | """ 33 | status_code = WS_PROTOCOL_ERROR 34 | default_detail = u"A server error occurred." 35 | 36 | def __init__(self, detail=None): 37 | """ 38 | Create an instance of exception with users detail information if 39 | it is passed. 40 | 41 | :param detail: users detail information (string). 42 | """ 43 | if detail is not None: 44 | self.detail = force_text(detail) 45 | else: 46 | self.detail = force_text(self.default_detail) 47 | 48 | def __str__(self): 49 | return self.detail 50 | 51 | 52 | class EndpointValueError(BaseAPIException): 53 | default_detail = u"Incorrect endpoint. Check path to your API." 54 | 55 | 56 | class IncorrectArgument(BaseAPIException): 57 | status_code = WS_DATA_CANNOT_ACCEPT 58 | default_detail = u"Check `args` in query on errors and try again." 59 | 60 | 61 | class IncorrectMethodNameType(BaseAPIException): 62 | default_detail = u"Method name should be a string type." 63 | 64 | 65 | class InvalidHandler(BaseAPIException): 66 | default_detail = u"Received handler isn't correct. It shall be function" \ 67 | u" or class, inherited from the MethodBasedView class." 68 | 69 | 70 | class InvalidPathArgument(BaseAPIException): 71 | default_detail = u"Received path value not valid." 72 | 73 | 74 | class InvalidRenderer(BaseAPIException): 75 | default_detail = "Attribute `renderers` should be defined as list or " \ 76 | "tuple of inherited from BaseSerializer classes." 77 | 78 | 79 | class NotImplementedMethod(BaseAPIException): 80 | default_detail = u"Error occurred in not implemented method." 81 | 82 | 83 | class NotSpecifiedError(BaseAPIException): 84 | default_detail = u"Not specified parameter." 85 | 86 | 87 | class NotSpecifiedHandler(NotSpecifiedError): 88 | default_detail = u"For URL, typed in request, handler not specified." 89 | 90 | 91 | class NotSpecifiedMethodName(NotSpecifiedError): 92 | default_detail = u"In query not specified `method` argument." 93 | 94 | 95 | class NotSpecifiedURL(NotSpecifiedError): 96 | default_detail = u"In query not specified `url` argument." 97 | 98 | 99 | class NotSupportedArgumentType(BaseAPIException): 100 | status_code = WS_DATA_CANNOT_ACCEPT 101 | default_detail = u"Check your arguments on supported types." 102 | 103 | 104 | class SerializerError(BaseAPIException): 105 | default_detail = u"Error has occurred inside serializer class." 106 | -------------------------------------------------------------------------------- /examples/sqlalchemy_orm/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import json 4 | 5 | from hashlib import sha256 6 | from autobahn.asyncio.websocket import WebSocketClientProtocol, \ 7 | WebSocketClientFactory 8 | 9 | 10 | def hash_password(password): 11 | return sha256(password.encode('utf-8')).hexdigest() 12 | 13 | 14 | class HelloClientProtocol(WebSocketClientProtocol): 15 | 16 | def onOpen(self): 17 | # Create new address 18 | request = { 19 | 'method': 'POST', 20 | 'url': '/address/', 21 | 'data': { 22 | "email_address": 'some_address@google.com' 23 | }, 24 | 'event_name': 'create-address' 25 | } 26 | self.sendMessage(json.dumps(request).encode('utf8')) 27 | 28 | # Get existing user 29 | request = { 30 | 'method': 'GET', 31 | 'url': '/user/5/', 32 | 'event_name': 'get-user-detail' 33 | } 34 | self.sendMessage(json.dumps(request).encode('utf8')) 35 | 36 | # Get users list 37 | request = { 38 | 'method': 'GET', 39 | 'url': '/user/list/', 40 | 'event_name': 'get-user-list' 41 | } 42 | self.sendMessage(json.dumps(request).encode('utf8')) 43 | 44 | # Create new user with address 45 | request = { 46 | 'method': 'POST', 47 | 'url': '/user/', 48 | 'data': { 49 | 'name': 'Neyton', 50 | 'fullname': 'Neyton Drake', 51 | 'password': hash_password('123456'), 52 | 'addresses': [{"id": 1}, ] 53 | }, 54 | 'event_name': 'create-user' 55 | } 56 | self.sendMessage(json.dumps(request).encode('utf8')) 57 | 58 | # Trying to create new user with same info, but we have taken an error 59 | self.sendMessage(json.dumps(request).encode('utf8')) 60 | 61 | # Update existing object 62 | request = { 63 | 'method': 'PUT', 64 | 'url': '/user/6/', 65 | 'data': { 66 | 'fullname': 'Definitely not Neyton Drake', 67 | 'addresses': [{"id": 1}, {"id": 2}] 68 | }, 69 | 'event_name': 'partial-update-user' 70 | } 71 | self.sendMessage(json.dumps(request).encode('utf8')) 72 | 73 | # Create few user for a one request 74 | request = { 75 | 'method': 'POST', 76 | 'url': '/user/list/', 77 | 'data': [ 78 | { 79 | 'name': 'User 1', 80 | 'fullname': 'User #1', 81 | 'password': hash_password('1q2w3e'), 82 | 'addresses': [{"id": 1}, ] 83 | }, 84 | { 85 | 'name': 'User 2', 86 | 'fullname': 'User #2', 87 | 'password': hash_password('qwerty'), 88 | 'addresses': [{"id": 2}, ] 89 | }, 90 | ], 91 | 'event_name': 'create-user-list' 92 | } 93 | self.sendMessage(json.dumps(request).encode('utf8')) 94 | 95 | 96 | def onMessage(self, payload, isBinary): 97 | print("Result: {0}".format(payload.decode('utf8'))) 98 | 99 | 100 | if __name__ == '__main__': 101 | factory = WebSocketClientFactory("ws://localhost:8080") 102 | factory.protocol = HelloClientProtocol 103 | 104 | loop = asyncio.get_event_loop() 105 | coro = loop.create_connection(factory, '127.0.0.1', 8080) 106 | loop.run_until_complete(coro) 107 | loop.run_forever() 108 | loop.close() 109 | -------------------------------------------------------------------------------- /tests/db/orm/sqlalchemy/test_mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from aiorest_ws.db.orm.sqlalchemy.mixins import ORMSessionMixin, \ 3 | SQLAlchemyMixin 4 | from aiorest_ws.test.utils import override_settings 5 | 6 | from sqlalchemy import Column, Integer, String 7 | from sqlalchemy.orm.session import Session 8 | from sqlalchemy.orm.query import Query 9 | 10 | from tests.db.orm.sqlalchemy.base import Base, SQLAlchemyUnitTest 11 | from tests.fixtures.sqlalchemy import SESSION 12 | 13 | 14 | class TestORMSessionMixin(SQLAlchemyUnitTest): 15 | settings = {'SQLALCHEMY_SESSION': SESSION} 16 | 17 | class TableForORMSessionMixin(Base): 18 | __tablename__ = 'test_orm_session_mixin' 19 | id = Column(Integer, primary_key=True) 20 | 21 | tables = [TableForORMSessionMixin.__table__, ] 22 | 23 | @classmethod 24 | def setUpClass(cls): 25 | cls._cls_overridden_context = override_settings(**cls.settings) 26 | cls._cls_overridden_context.enable() 27 | super(TestORMSessionMixin, cls).setUpClass() 28 | cls.session = SESSION() 29 | 30 | @classmethod 31 | def tearDownClass(cls): 32 | cls._cls_overridden_context.disable() 33 | super(TestORMSessionMixin, cls).tearDownClass() 34 | 35 | def test_get_session(self): 36 | mixin = ORMSessionMixin() 37 | self.assertIsInstance(mixin._get_session(), Session) 38 | 39 | def test_get_queryset_returns_new_queryset_with_session(self): 40 | mixin = ORMSessionMixin() 41 | mixin.queryset = Query(self.TableForORMSessionMixin) 42 | queryset = mixin.get_queryset() 43 | self.assertNotEqual(mixin.queryset.session, self.session) 44 | self.assertEqual(mixin.queryset, queryset) 45 | 46 | def test_get_queryset_returns_has_closed_session_and_returns_new_qs(self): 47 | mixin = ORMSessionMixin() 48 | mixin.queryset = self.session.query(self.TableForORMSessionMixin) 49 | queryset = mixin.get_queryset() 50 | self.assertNotEqual(mixin.queryset.session, self.session) 51 | self.assertEqual(mixin.queryset, queryset) 52 | 53 | 54 | class TestSQLAlchemyMixin(SQLAlchemyUnitTest): 55 | settings = {'SQLALCHEMY_SESSION': SESSION} 56 | 57 | class TableForSQLAlchemyMixin(Base): 58 | __tablename__ = 'test_sqlalchemy_orm_mixin' 59 | id = Column(Integer, primary_key=True) 60 | login = Column(String, nullable=False) 61 | 62 | tables = [TableForSQLAlchemyMixin.__table__, ] 63 | 64 | @classmethod 65 | def setUpClass(cls): 66 | cls._cls_overridden_context = override_settings(**cls.settings) 67 | cls._cls_overridden_context.enable() 68 | super(TestSQLAlchemyMixin, cls).setUpClass() 69 | cls.session = SESSION() 70 | cls.session.add_all([cls.TableForSQLAlchemyMixin(login='user'), ]) 71 | cls.session.commit() 72 | 73 | @classmethod 74 | def tearDownClass(cls): 75 | cls._cls_overridden_context.disable() 76 | super(TestSQLAlchemyMixin, cls).tearDownClass() 77 | 78 | def test_get_filter_args(self): 79 | mixin = SQLAlchemyMixin() 80 | query = Query(self.TableForSQLAlchemyMixin) 81 | filter_arguments = list(mixin._get_filter_args(query, {'id': 1})) 82 | self.assertEqual(len(filter_arguments), 1) 83 | self.assertEqual( 84 | filter_arguments[0].__str__(), 85 | (self.TableForSQLAlchemyMixin.id == 1).__str__() 86 | ) 87 | 88 | def test_get_object_pk(self): 89 | mixin = SQLAlchemyMixin() 90 | obj = self.session.query(self.TableForSQLAlchemyMixin) \ 91 | .filter(self.TableForSQLAlchemyMixin.id == 1) \ 92 | .first() 93 | self.assertEqual(mixin._get_object_pk(obj), 1) 94 | -------------------------------------------------------------------------------- /tests/utils/date/test_humanize_datetime.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | from datetime import timedelta as td 4 | 5 | from aiorest_ws.utils.date.formatters import iso8601_repr 6 | from aiorest_ws.utils.date.humanize_datetime import STRFDATETIME, \ 7 | STRFDATETIME_REPL, datetime_formats, date_formats, time_formats, \ 8 | humanize_strptime, humanize_timedelta 9 | 10 | 11 | @pytest.mark.parametrize("value, expected", [ 12 | (STRFDATETIME.match('d'), '%(d)s'), 13 | (STRFDATETIME.match('g'), '%(g)s'), 14 | (STRFDATETIME.match('G'), '%(G)s'), 15 | (STRFDATETIME.match('h'), '%(h)s'), 16 | (STRFDATETIME.match('H'), '%(H)s'), 17 | (STRFDATETIME.match('i'), '%(i)s'), 18 | (STRFDATETIME.match('s'), '%(s)s') 19 | ]) 20 | def test_STRFDATETIME_REPL(value, expected): 21 | assert STRFDATETIME_REPL(value) == expected 22 | 23 | 24 | @pytest.mark.parametrize("value, expected", [ 25 | (('iso-8601', ), 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'), 26 | ]) 27 | def test_datetime_formats(value, expected): 28 | assert datetime_formats(value) == expected 29 | 30 | 31 | @pytest.mark.parametrize("value, expected", [ 32 | (('iso-8601', ), 'YYYY[-MM[-DD]]'), 33 | (('iso-8601', '%H:%M:%S'), 'YYYY[-MM[-DD]], hh:mm:ss'), 34 | (('iso-8601', '%I:%M:%S'), 'YYYY[-MM[-DD]], hh:mm:ss'), 35 | (('iso-8601', '%I:%M:%S:%f'), 'YYYY[-MM[-DD]], hh:mm:ss:uuuuuu'), 36 | ]) 37 | def test_date_formats(value, expected): 38 | assert date_formats(value) == expected 39 | 40 | 41 | @pytest.mark.parametrize("value, expected", [ 42 | (('iso-8601', ), 'hh:mm[:ss[.uuuuuu]]'), 43 | (('%Y-%m-%d', 'iso-8601', ), 'YYYY-MM-DD, hh:mm[:ss[.uuuuuu]]'), 44 | (('%y-%b-%d', 'iso-8601', ), 'YY-[Jan-Dec]-DD, hh:mm[:ss[.uuuuuu]]') 45 | ]) 46 | def test_time_formats(value, expected): 47 | assert time_formats(value) == expected 48 | 49 | 50 | @pytest.mark.parametrize("value, expected", [ 51 | ('%Y-%m-%d', 'YYYY-MM-DD'), 52 | ('%y-%b-%d', 'YY-[Jan-Dec]-DD'), 53 | ('%y-%B-%d', 'YY-[January-December]-DD'), 54 | ('%H:%M:%S', 'hh:mm:ss'), 55 | ('%I:%M:%S', 'hh:mm:ss'), 56 | ('%I:%M:%S:%f', 'hh:mm:ss:uuuuuu'), 57 | ('%a %H:%M:%S', '[Mon-Sun] hh:mm:ss'), 58 | ('%A %H:%M:%S', '[Monday-Sunday] hh:mm:ss'), 59 | ('%A %H:%M %p', '[Monday-Sunday] hh:mm [AM|PM]'), 60 | ('%A %H:%M %z', '[Monday-Sunday] hh:mm [+HHMM|-HHMM]'), 61 | ]) 62 | def test_humanize_strptime(value, expected): 63 | assert humanize_strptime(value) == expected 64 | 65 | 66 | @pytest.mark.parametrize("value, kwargs, expected", [ 67 | (td(days=1, hours=2, minutes=3, seconds=4), {}, 68 | '1 day, 2 hours, 3 minutes, 4 seconds'), 69 | (td(days=1, seconds=1), {'display': 'minimal'}, '1d, 1s'), 70 | (td(days=1), {}, '1 day'), 71 | (td(days=0), {}, '0 seconds'), 72 | (td(seconds=1), {}, '1 second'), 73 | (td(seconds=10), {}, '10 seconds'), 74 | (td(seconds=30), {}, '30 seconds'), 75 | (td(seconds=60), {}, '1 minute'), 76 | (td(seconds=150), {}, '2 minutes, 30 seconds'), 77 | (td(seconds=1800), {}, '30 minutes'), 78 | (td(seconds=3600), {}, '1 hour'), 79 | (td(seconds=3601), {}, '1 hour, 1 second'), 80 | (td(seconds=19800), {}, '5 hours, 30 minutes'), 81 | (td(seconds=91800), {}, '1 day, 1 hour, 30 minutes'), 82 | (td(seconds=302400), {}, '3 days, 12 hours'), 83 | (td(seconds=0), {'display': 'minimal'}, '0s'), 84 | (td(seconds=0), {'display': 'short'}, '0 sec'), 85 | (td(seconds=0), {'display': 'long'}, '0 seconds'), 86 | (td(seconds=0), {'display': 'iso8601'}, iso8601_repr(td(seconds=0))), 87 | (td(hours=1, minutes=30), {'display': 'sql'}, '0 01:30:00'), 88 | (td(hours=1, minutes=30), {'display': 'h:i'}, '1:30') 89 | ]) 90 | def test_humanize_timedelta(value, kwargs, expected): 91 | assert humanize_timedelta(value, **kwargs) == expected 92 | -------------------------------------------------------------------------------- /aiorest_ws/auth/user/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Functions and constants, which can be used for work with User models. 4 | """ 5 | from aiorest_ws.db.utils import convert_db_row_to_dict 6 | from hashlib import sha256 7 | 8 | SQL_CREATE_USER_TABLE = """ 9 | CREATE TABLE IF NOT EXISTS aiorest_auth_user 10 | (id INTEGER PRIMARY KEY NOT NULL, 11 | username CHAR(255) NOT NULL, 12 | password CHAR(255) NOT NULL, 13 | last_name CHAR(255), 14 | first_name CHAR(255), 15 | is_active BOOL DEFAULT TRUE NOT NULL, 16 | is_superuser BOOL DEFAULT FALSE NOT NULL, 17 | is_staff BOOL DEFAULT FALSE NOT NULL, 18 | is_user BOOL DEFAULT TRUE NOT NULL 19 | ); 20 | """ 21 | SQL_CREATE_TOKEN_FOREIGN_KEY = """ 22 | ALTER TABLE aiorest_auth_token 23 | ADD COLUMN user_id INTEGER REFERENCES aiorest_auth_user(id); 24 | """ 25 | SQL_USER_ADD = """ 26 | INSERT INTO aiorest_auth_user (`username`, `password`, `first_name`, 27 | `last_name`, `is_superuser`, `is_staff`, `is_user`, `is_active`) 28 | VALUES (?, ?, ?, ?, ?, ?, ?, ?); 29 | """ 30 | SQL_USER_GET = """ 31 | SELECT `username`, `password`, `first_name`, `last_name`, `is_superuser`, 32 | `is_staff`, `is_user`, `is_active` 33 | FROM aiorest_auth_user 34 | WHERE id=?; 35 | """ 36 | SQL_USER_GET_WITH_ID = """ 37 | SELECT `id`, `username`, `password`, `first_name`, `last_name`, 38 | `is_superuser`, `is_staff`, `is_user`, `is_active` 39 | FROM aiorest_auth_user 40 | WHERE username=?; 41 | """ 42 | SQL_USER_GET_BY_USERNAME = """ 43 | SELECT `username`, `password`, `first_name`, `last_name`, `is_superuser`, 44 | `is_staff`, `is_user`, `is_active` 45 | FROM aiorest_auth_user 46 | WHERE username=?; 47 | """ 48 | SQL_USER_UPDATE = """ 49 | UPDATE aiorest_auth_user 50 | SET {} 51 | WHERE username=?; 52 | """ 53 | USER_MODEL_FIELDS = ( 54 | 'id', 'username', 'password', 'first_name', 'last_name', 'is_superuser', 55 | 'is_staff', 'is_user', 'is_active' 56 | ) 57 | USER_MODEL_FIELDS_WITHOUT_PK = ( 58 | 'username', 'password', 'first_name', 'last_name', 'is_superuser', 59 | 'is_staff', 'is_user', 'is_active' 60 | ) 61 | 62 | 63 | def construct_update_sql(**parameters): 64 | """ 65 | Create update SQL query for SQLite based on the data, provided by the user. 66 | 67 | :param parameters: dictionary, where key is updated field of user model. 68 | """ 69 | query_args = [] 70 | update_field_template = "{}=? " 71 | updated_fields = '' 72 | for index, (field, value) in enumerate(parameters.items()): 73 | updated_fields += update_field_template.format(field) 74 | 75 | if index < len(parameters) - 1: 76 | updated_fields += " ," 77 | 78 | query_args.append(value) 79 | sql = SQL_USER_UPDATE.format(updated_fields) 80 | return sql, query_args 81 | 82 | 83 | def convert_user_raw_data_to_dict(user_raw_data, with_id=False): 84 | """ 85 | Convert database row to the dictionary object. 86 | 87 | :param user_raw_data: database row. 88 | :param with_id: boolean flag, which means necessity to append to the result 89 | dictionary primary key of database row or not. 90 | """ 91 | if with_id: 92 | fields_tuple = USER_MODEL_FIELDS 93 | else: 94 | fields_tuple = USER_MODEL_FIELDS_WITHOUT_PK 95 | 96 | bool_fields = ('is_superuser', 'is_staff', 'is_user', 'is_active') 97 | user_data = convert_db_row_to_dict(user_raw_data, fields_tuple) 98 | for field in bool_fields: 99 | user_data[field] = bool(user_data[field]) 100 | return user_data 101 | 102 | 103 | def generate_password_hash(password): 104 | """ 105 | Generate SHA256 hash for password. 106 | 107 | :param password: password as a string. 108 | """ 109 | return sha256(password.encode('utf-8')).hexdigest() 110 | -------------------------------------------------------------------------------- /tests/db/orm/django/test_compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from unittest.mock import patch 3 | 4 | from django.core.exceptions import ImproperlyConfigured 5 | from django.db import models 6 | 7 | from aiorest_ws.db.orm.django.compat import _resolve_model, \ 8 | get_related_model, get_remote_field, value_from_object 9 | 10 | from tests.db.orm.django.base import DjangoUnitTest 11 | 12 | 13 | class CustomStringField(models.CharField): 14 | pass 15 | 16 | 17 | class TestDjangoCompatModule(DjangoUnitTest): 18 | 19 | class Manufacturer(models.Model): 20 | name = CustomStringField(max_length=30) 21 | 22 | class Meta: 23 | app_label = 'test_django_compat_module' 24 | 25 | def __str__(self): 26 | return '' % self.name 27 | 28 | class Car(models.Model): 29 | name = models.CharField(max_length=30) 30 | max_speed = models.FloatField(null=True, blank=True) 31 | manufacturer = models.ForeignKey( 32 | "test_django_compat_module.Manufacturer", related_name='cars', 33 | null=True, blank=True 34 | ) 35 | 36 | class Meta: 37 | app_label = 'test_django_compat_module' 38 | 39 | def __str__(self): 40 | return '' % (self.name, self.manufacturer) 41 | 42 | apps = ('test_django_compat_module', ) 43 | models = (Manufacturer, Car) 44 | 45 | @classmethod 46 | def setUpClass(cls): 47 | super(TestDjangoCompatModule, cls).setUpClass() 48 | user = cls.Manufacturer.objects.create(name='TestName') 49 | car = cls.Car.objects.create(name='TestCar', max_speed=350) 50 | user.cars.add(car) 51 | user.save() 52 | 53 | def test_resolve_model_return_object(self): 54 | self.assertEqual(_resolve_model(self.Manufacturer), self.Manufacturer) 55 | 56 | def test_resolve_model_return_object_by_model_as_string(self): 57 | 58 | class FakeCarConfig(object): 59 | 60 | model_mapping = { 61 | 'car': self.Car 62 | } 63 | 64 | def get_model(config, model_name): 65 | return config.model_mapping.get(model_name, None) 66 | 67 | patched_module = 'django.apps.apps.app_configs' 68 | patched_value = {'test_django_compat_module': FakeCarConfig()} 69 | with patch(patched_module, new=patched_value): 70 | value = "test_django_compat_module.car" 71 | self.assertEqual(_resolve_model(value), self.Car) 72 | 73 | def test_resolve_model_raises_improperly_configured_exception(self): 74 | 75 | class FakeConfig(object): 76 | 77 | def get_model(self, model_name): 78 | return None 79 | 80 | patched_module = 'django.apps.apps.app_configs' 81 | patched_value = {'invalid_app': FakeConfig()} 82 | with patch(patched_module, new=patched_value): 83 | value = "invalid_app.fake_model" 84 | self.assertRaises(ImproperlyConfigured, _resolve_model, value) 85 | 86 | def test_resolve_model_raises_value_error_exception(self): 87 | 88 | class NotDjangoModel(object): 89 | pass 90 | 91 | obj = NotDjangoModel() 92 | self.assertRaises(ValueError, _resolve_model, obj) 93 | 94 | def test_get_related_model(self): 95 | field = self.Car._meta.get_field('manufacturer') 96 | self.assertEqual(get_related_model(field), field.remote_field.model) 97 | 98 | def test_get_remote_field(self): 99 | field = self.Car._meta.get_field('manufacturer') 100 | self.assertEqual(get_remote_field(field), field.remote_field) 101 | 102 | def test_value_from_object(self): 103 | field = self.Manufacturer._meta.get_field('name') 104 | obj = self.Manufacturer.objects.get(name='TestName') 105 | self.assertEqual(value_from_object(field, obj), obj.name) 106 | -------------------------------------------------------------------------------- /aiorest_ws/auth/token/middlewares.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Middlewares for authentication issues. 4 | """ 5 | from aiorest_ws.abstract import AbstractMiddleware 6 | from aiorest_ws.auth.user.models import UserSQLiteModel 7 | from aiorest_ws.auth.user.abstractions import User 8 | from aiorest_ws.auth.token.backends import InMemoryTokenBackend 9 | from aiorest_ws.auth.token.exceptions import TokenNotProvidedException 10 | from aiorest_ws.auth.token.managers import JSONWebTokenManager 11 | from aiorest_ws.utils.modify import add_property 12 | 13 | __all__ = ('BaseTokenMiddleware', 'JSONWebTokenMiddleware', ) 14 | 15 | 16 | class BaseTokenMiddleware(AbstractMiddleware): 17 | """ 18 | Base token middleware class. 19 | """ 20 | 21 | def init_credentials(self, request): 22 | """ 23 | Getting credentials (user, keys, tokens) from database/cache/etc. 24 | 25 | :param request: instance of Request class. 26 | """ 27 | pass 28 | 29 | def authenticate(self, request, handler): 30 | """ 31 | Authenticate user. 32 | 33 | :param request: instance of Request class. 34 | :param handler: view, invoked later for the request. 35 | """ 36 | pass 37 | 38 | def process_request(self, request, handler): 39 | """ 40 | Processing request before calling handler. 41 | 42 | :param request: instance of Request class. 43 | :param handler: view, invoked later for the request. 44 | """ 45 | self.init_credentials(request) 46 | self.authenticate(request, handler) 47 | 48 | 49 | class JSONWebTokenMiddleware(BaseTokenMiddleware): 50 | """ 51 | The JSON Web Token middleware class. 52 | """ 53 | storage_backend = InMemoryTokenBackend 54 | manager = JSONWebTokenManager 55 | user_model = UserSQLiteModel 56 | 57 | def __init__(self): 58 | super(JSONWebTokenMiddleware, self).__init__() 59 | self.storage_backend = self.storage_backend() 60 | self.user_model = self.user_model() 61 | self.manager = self.manager() 62 | 63 | def get_user_by_token(self, token): 64 | """ 65 | Get user from the database by passed token. 66 | 67 | :param token: token as string. 68 | """ 69 | token_data = self.storage_backend.get(token) 70 | if token_data: 71 | user = self.user_model.get_user_by_token(token_data) 72 | else: 73 | user = User() 74 | return user 75 | 76 | def init_credentials(self, request): 77 | """ 78 | Getting credentials (user, keys, tokens) from database/cache/etc. 79 | 80 | :param request: instance of Request class. 81 | """ 82 | token = getattr(request, 'token', None) 83 | 84 | if token: 85 | token_payload = self.manager.verify(token) 86 | user = self.get_user_by_token(token) 87 | else: 88 | token_payload = None 89 | user = User() 90 | 91 | add_property(request, 'user', user) 92 | add_property(request, 'token_payload', token_payload) 93 | 94 | def authenticate(self, request, view): 95 | """ 96 | Authenticate user. 97 | 98 | NOTE: Authentication applied for the views, which set `auth_required` 99 | attribute to `True` value. 100 | 101 | :param request: instance of Request class. 102 | :param view: view, invoked later for the request. 103 | """ 104 | auth_required = getattr(view, 'auth_required', False) 105 | permission_classes = getattr(view, 'permission_classes', ()) 106 | 107 | if auth_required: 108 | if hasattr(request, 'token') and request.token: 109 | for permission in permission_classes: 110 | permission.check(request, view) 111 | else: 112 | raise TokenNotProvidedException() 113 | -------------------------------------------------------------------------------- /aiorest_ws/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module provide a function and class-based views and can be used 4 | with aiorest-ws routers. 5 | """ 6 | from aiorest_ws.exceptions import IncorrectMethodNameType, \ 7 | InvalidRenderer, NotSpecifiedHandler, NotSpecifiedMethodName 8 | from aiorest_ws.renderers import JSONRenderer 9 | 10 | __all__ = ('http_methods', 'View', 'MethodViewMeta', 'MethodBasedView', ) 11 | 12 | http_methods = frozenset(['get', 'post', 'head', 'options', 'delete', 'put', 13 | 'trace', 'patch']) 14 | 15 | 16 | class View(object): 17 | """ 18 | Subclass for implementing method-based views. 19 | """ 20 | @classmethod 21 | def as_view(cls, name, *class_args, **class_kwargs): 22 | """Converts the class into an actual view function that can be used 23 | with the routing system. 24 | """ 25 | pass 26 | 27 | 28 | class MethodViewMeta(type): 29 | """ 30 | Metaclass, which helps to define list of supported methods in 31 | class-based views. 32 | """ 33 | def __new__(cls, name, bases, attrs): 34 | obj = type.__new__(cls, name, bases, attrs) 35 | # If not defined 'method' attribute, then make and append him to 36 | # our class-based view 37 | if 'methods' not in attrs: 38 | methods = set(obj.methods if hasattr(obj, 'methods') else []) 39 | for key in attrs: 40 | if key in http_methods: 41 | methods.add(key.lower()) 42 | # This action necessary for appending list of supported methods 43 | obj.methods = sorted(methods) 44 | return obj 45 | 46 | 47 | class MethodBasedView(View, metaclass=MethodViewMeta): 48 | """ 49 | Method-based view for aiorest-ws framework. 50 | """ 51 | renderers = () 52 | 53 | def dispatch(self, request, *args, **kwargs): 54 | """ 55 | Search the most suitable handler for request. 56 | 57 | :param request: passed request from user. 58 | """ 59 | method = request.method 60 | 61 | # Invoked, when user not specified method in query (e.c. get, post) 62 | if not method: 63 | raise NotSpecifiedMethodName() 64 | 65 | # Invoked, when user specified method name as not a string 66 | if not isinstance(method, str): 67 | raise IncorrectMethodNameType() 68 | 69 | # Trying to find the most suitable handler. For that what we are doing: 70 | # 1) Make string in lowercase (e.c. 'GET' -> 'get') 71 | # 2) Look into class and try to get handler with this name 72 | # 3) If extracting is successful, then invoke handler with arguments 73 | method = method.lower().strip() 74 | handler = getattr(self, method, None) 75 | if not handler: 76 | raise NotSpecifiedHandler() 77 | return handler(request, *args, **kwargs) 78 | 79 | def get_renderer(self, preferred_format, *args, **kwargs): 80 | """ 81 | Get serialize class, which using to converting response to 82 | some users format. 83 | 84 | :param preferred_format: string, which means serializing response to 85 | required format (e.c. json, xml). 86 | """ 87 | if self.renderers: 88 | if type(self.renderers) not in (list, tuple): 89 | raise InvalidRenderer() 90 | 91 | # By default we are take first serializer from list/tuple 92 | renderer = self.renderers[0] 93 | 94 | if preferred_format: 95 | # Try to find suitable serializer 96 | for renderer_class in self.renderers: 97 | if renderer_class.format == preferred_format: 98 | renderer = renderer_class 99 | break 100 | 101 | return renderer() 102 | else: 103 | return JSONRenderer() 104 | -------------------------------------------------------------------------------- /tests/utils/test_representation.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from aiorest_ws.db.orm import fields, serializers, relations 5 | from aiorest_ws.db.orm.validators import BaseValidator 6 | from aiorest_ws.utils.representation import smart_repr, field_repr, \ 7 | serializer_repr, list_repr 8 | 9 | from tests.fixtures.fakes import FakeView 10 | 11 | 12 | class FakePkField(relations.PrimaryKeyRelatedField, relations.RelatedField): 13 | many_related_field = relations.ManyRelatedField 14 | 15 | 16 | class FakeModelSerializer(serializers.ModelSerializer): 17 | child = None 18 | fields = {'pk': fields.IntegerField()} 19 | 20 | 21 | class FakeListModelSerializer(serializers.ListSerializer): 22 | child = FakeModelSerializer() 23 | 24 | 25 | class FakeNestedModelSerializer(serializers.ModelSerializer): 26 | child = None 27 | fields = {'nested': FakeModelSerializer()} 28 | 29 | 30 | class FakeNestedModelSerializerWithList(serializers.ModelSerializer): 31 | child = None 32 | fields = {'nested': FakeListModelSerializer()} 33 | 34 | 35 | class FakeModelSerializerWithRelatedField(serializers.ModelSerializer): 36 | child = None 37 | fields = {'related': relations.ManyRelatedField( 38 | child_relation=relations.RelatedField(read_only=True) 39 | )} 40 | 41 | 42 | class FakeModelSerializerWithValidators(serializers.ModelSerializer): 43 | child = None 44 | fields = {'pk': fields.IntegerField()} 45 | validators = [BaseValidator, ] 46 | 47 | 48 | @pytest.mark.parametrize("value, expected", [ 49 | ("test_string", "test_string"), 50 | (["value_1", "value_2"], "['value_1', 'value_2']"), 51 | ({"key": "value"}, "{'key': 'value'}"), 52 | (FakeView(), "") 53 | ]) 54 | def test_smart_repr(value, expected): 55 | assert smart_repr(value) == expected 56 | 57 | 58 | @pytest.mark.parametrize("field, many, expected", [ 59 | (fields.IntegerField(), False, 'IntegerField()'), 60 | (fields.LargeBinaryField(object, allow_null=True), False, 61 | "LargeBinaryField(, allow_null=True)"), 62 | (FakePkField(many=True, read_only=True), True, 63 | "bool(child_relation=, " 64 | "many=True, read_only=True)") 65 | ]) 66 | def test_field_repr(field, many, expected): 67 | assert field_repr(field, force_many=many) == expected 68 | 69 | 70 | @pytest.mark.parametrize("serializer, indent, force_many, expected", [ 71 | (FakeModelSerializer(), 1, False, 72 | "FakeModelSerializer():\n" 73 | " pk = IntegerField()"), 74 | (FakeNestedModelSerializer(), 1, False, 75 | "FakeNestedModelSerializer():\n" 76 | " nested = FakeModelSerializer():\n" 77 | " pk = IntegerField()"), 78 | (FakeNestedModelSerializerWithList(), 1, False, 79 | "FakeNestedModelSerializerWithList():\n" 80 | " nested = FakeModelSerializer(many=True):\n" 81 | " pk = IntegerField()"), 82 | (FakeModelSerializerWithRelatedField(), 1, False, 83 | "FakeModelSerializerWithRelatedField():\n" 84 | " related = RelatedField(many=True, read_only=True)"), 85 | (FakeModelSerializerWithValidators(), 1, False, 86 | "FakeModelSerializerWithValidators():\n" 87 | " pk = IntegerField()\n" 88 | " class Meta:\n" 89 | " validators = []"), 91 | ]) 92 | def test_serializer_repr(serializer, indent, force_many, expected): 93 | assert serializer_repr(serializer, indent, force_many) == expected 94 | 95 | 96 | @pytest.mark.parametrize("serializer, indent, expected", [ 97 | (FakeModelSerializer(), 1, "FakeModelSerializer()"), 98 | (FakeListModelSerializer(), 1, 99 | "FakeModelSerializer(many=True):\n" 100 | " pk = IntegerField()") 101 | ]) 102 | def test_list_repr(serializer, indent, expected): 103 | assert list_repr(serializer, indent) == expected 104 | -------------------------------------------------------------------------------- /tests/auth/token/test_backends.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from aiorest_ws.auth.token.backends import InMemoryTokenBackend 5 | from aiorest_ws.auth.user.models import UserSQLiteModel 6 | from aiorest_ws.conf import settings 7 | 8 | from tests.fixtures.example_settings import DATABASES 9 | 10 | 11 | class InMemoryTokenBackendTestCase(unittest.TestCase): 12 | 13 | def _set_user_settings(self): 14 | settings.DATABASES = DATABASES 15 | settings._create_database_managers() 16 | 17 | def _create_simple_user(self, user_model): 18 | kwargs = { 19 | 'username': 'testuser', 20 | 'password': '123456', 21 | 'first_name': 'test', 22 | 'last_name': 'user', 23 | 'is_active': True, 24 | 'is_superuser': False, 25 | 'is_staff': False, 26 | 'is_user': True, 27 | } 28 | user_model.create_user(**kwargs) 29 | user = user_model.get_user_by_username('testuser', with_id=True) 30 | return user 31 | 32 | def test_init_by_default(self): 33 | settings.DATABASES = {} 34 | backend = InMemoryTokenBackend() # NOQA 35 | 36 | def test_init_with_user_settings(self): 37 | self._set_user_settings() 38 | backend = InMemoryTokenBackend() # NOQA 39 | 40 | def test_get(self): 41 | self._set_user_settings() 42 | token_backend = InMemoryTokenBackend() 43 | user_model = UserSQLiteModel() 44 | user = self._create_simple_user(user_model) 45 | token_data = { 46 | 'name': 'api', 47 | 'token': 'my_test_token', 48 | 'user_id': user.id 49 | } 50 | token_backend.save( 51 | token_data['name'], 52 | token_data['token'], 53 | user_id=token_data['user_id'] 54 | ) 55 | db_token = token_backend.get(token_data['token']) 56 | self.assertEqual(db_token['name'], token_data['name']) 57 | self.assertEqual(db_token['token'], token_data['token']) 58 | self.assertEqual(db_token['user_id'], token_data['user_id']) 59 | 60 | def test_get_not_existed_token(self): 61 | self._set_user_settings() 62 | backend = InMemoryTokenBackend() 63 | user_model = UserSQLiteModel() # NOQA 64 | db_token = backend.get('unknown_token') 65 | self.assertEqual(db_token, {}) 66 | 67 | def test_get_token_by_username(self): 68 | self._set_user_settings() 69 | token_backend = InMemoryTokenBackend() 70 | user_model = UserSQLiteModel() 71 | user = self._create_simple_user(user_model) 72 | token_data = { 73 | 'name': 'api', 74 | 'token': 'my_test_token', 75 | 'user_id': user.id 76 | } 77 | token_backend.save( 78 | token_data['name'], 79 | token_data['token'], 80 | user_id=token_data['user_id'] 81 | ) 82 | db_token = token_backend.get_token_by_username('api', user.username) 83 | self.assertEqual(db_token['name'], token_data['name']) 84 | self.assertEqual(db_token['token'], token_data['token']) 85 | self.assertEqual(db_token['user_id'], token_data['user_id']) 86 | 87 | def test_get_token_by_not_existed_username(self): 88 | self._set_user_settings() 89 | token_backend = InMemoryTokenBackend() 90 | user_model = UserSQLiteModel() 91 | user = self._create_simple_user(user_model) 92 | token_backend.save('api', 'my_test_token', user_id=user.id) 93 | token = token_backend.get_token_by_username('unknown', user.username) 94 | self.assertEqual(token, {}) 95 | 96 | def test_save(self): 97 | self._set_user_settings() 98 | token_backend = InMemoryTokenBackend() 99 | user_model = UserSQLiteModel() 100 | user = self._create_simple_user(user_model) 101 | token_backend.save('api', 'my_test_token', user_id=user.id) 102 | -------------------------------------------------------------------------------- /tests/test/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | 4 | from aiorest_ws.conf import settings 5 | from aiorest_ws.test.utils import BaseContextDecorator, override_settings 6 | 7 | 8 | class TestBaseContextDecorator(unittest.TestCase): 9 | 10 | @classmethod 11 | def setUpClass(cls): 12 | super(TestBaseContextDecorator, cls).setUpClass() 13 | cls.instance = BaseContextDecorator() 14 | 15 | def test_enable(self): 16 | with self.assertRaises(NotImplementedError): 17 | self.instance.enable() 18 | 19 | def test_disable(self): 20 | with self.assertRaises(NotImplementedError): 21 | self.instance.disable() 22 | 23 | def test_enter(self): 24 | with self.assertRaises(NotImplementedError): 25 | self.instance.__enter__() 26 | 27 | def test_disable_for_with_operator(self): 28 | with self.assertRaises(NotImplementedError): 29 | self.instance.__exit__(ValueError, 'error', None) 30 | 31 | def test_decorate_class(self): 32 | 33 | @BaseContextDecorator() 34 | class TestClass(unittest.TestCase): 35 | pass 36 | 37 | test_instance = TestClass() 38 | 39 | with self.assertRaises(NotImplementedError): 40 | test_instance.setUp() 41 | 42 | with self.assertRaises(NotImplementedError): 43 | test_instance.tearDown() 44 | 45 | def test_decorate_class_with_overriden_enable_and_disable(self): 46 | class OverriddenContextDecorator(BaseContextDecorator): 47 | def enable(self): 48 | pass 49 | 50 | def disable(self): 51 | pass 52 | 53 | @OverriddenContextDecorator(attr_name='test') 54 | class TestClass(unittest.TestCase): 55 | pass 56 | 57 | test_instance = TestClass() 58 | test_instance.setUp() 59 | test_instance.tearDown() 60 | 61 | def test_decorate_class_raises_type_error(self): 62 | with self.assertRaises(TypeError): 63 | 64 | @BaseContextDecorator() 65 | class DecoratedCls(object): 66 | pass 67 | 68 | def test_decorate_callable(self): 69 | @BaseContextDecorator() 70 | def decorated_func(): 71 | pass 72 | 73 | with self.assertRaises(NotImplementedError): 74 | decorated_func() 75 | 76 | def test_decorate_callable_with_kwargs(self): 77 | class OverriddenContextDecorator(BaseContextDecorator): 78 | def enable(self): 79 | pass 80 | 81 | @OverriddenContextDecorator(kwarg_name='context') 82 | def decorated_func(context=None): 83 | pass 84 | 85 | with self.assertRaises(NotImplementedError): 86 | decorated_func() 87 | 88 | 89 | class TestOverrideSettings(unittest.TestCase): 90 | 91 | def test_enable_and_disable_override_setting(self): 92 | with override_settings(UNKNOWN_KEY='value'): 93 | self.assertTrue(hasattr(settings, 'UNKNOWN_KEY')) 94 | self.assertFalse(hasattr(settings, 'UNKNOWN_KEY')) 95 | 96 | def test_decorate_class_must_set_options(self): 97 | 98 | @override_settings(key='value') 99 | class TestClass(unittest.TestCase): 100 | _overridden_settings = None 101 | 102 | test_instance = TestClass() 103 | test_instance.setUp() 104 | test_instance.tearDown() 105 | self.assertIsNotNone(test_instance._overridden_settings) 106 | 107 | def test_decorate_class_must_override_options(self): 108 | 109 | @override_settings(key='value') 110 | class TestClass(unittest.TestCase): 111 | _overridden_settings = {'db': 'PostgreSQL'} 112 | 113 | test_instance = TestClass() 114 | test_instance.setUp() 115 | test_instance.tearDown() 116 | self.assertIsNotNone(test_instance._overridden_settings) 117 | self.assertEqual( 118 | test_instance._overridden_settings, 119 | {'db': 'PostgreSQL', 'key': 'value'} 120 | ) 121 | --------------------------------------------------------------------------------