├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── _config.yml ├── django_rester ├── __init__.py ├── client.py ├── config.py ├── decorators.py ├── exceptions.py ├── fields.py ├── permission.py ├── rester_jwt │ ├── __init__.py │ ├── auth.py │ └── settings.py ├── settings.py ├── singleton.py ├── status.py └── views.py ├── django_rester_demo ├── .gitignore ├── api │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── db.sqlite3 ├── django_rester_demo │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── manage.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | .idea/ 103 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '3.5' 3 | addons: 4 | apt_packages: 5 | - pandoc 6 | branches: 7 | only: 8 | - master 9 | - develop 10 | install: 11 | - pip install pypandoc 12 | script: 13 | - echo "build and deploy" 14 | deploy: 15 | provider: pypi 16 | user: lexycore 17 | password: 18 | secure: qd+aKe/UM4m+Ya3Xhtt0vsIIS6PWuNDcVPFb+Ur902ZZJ0yweD4dsgQcGWLy/dXIwAlrCuahVx4U9KPZXHa5M6pM1NwjHOhv5vTprZZAVML2qgeup/jm2ehJxwG3grK8nGaVwRNJsbSf6ac0BDK6igGfPZhwpHCzZtBvTuikHtmq6NXk/KHpUcWuHCJVBkJ6cZlmzU2a4+0zeg/yT99NNBvb3CBA3yB0Hd8VIGojDCS6fDUCvNJrTijdrfxT7DtWsJ3pJNmFt+XWMwyj8g9NF8SoT1fIfs0/6cLZnSrQx2ejvm2JnNOkf5w68T4tzHgmR3dtC/9sxhwv/zPFZ8Pdi9DdQzkrbHaD40/RNaus9AjrLYYMnPTmY7TvCvA4wvXIO8sGi2v6jKuzhfbZSlSpSbHRYYByXRsnrykBrb0dTsaVqxKUG8odBMbVxD2BJxUI4M8xjzoRb3eKt4VVHur9zNYS/GRdFGAjZ6Vh4ApCsrqHKw4xzsZcc/6zMHcZA6+uCVgPsh5rFmpgLK/K5jAQVvYg0YePCFLOmRvS6/pRZhNC1mYIy8rDRGPqN4jOc7hHEmAeoTJesbxhP4TO3PFjz9jooH9SLRSbQzmBGrjELHYom7xj9/zfn6XWhqxwed1zlXthC0avP/P86xQaQDRjNvWoxTW+ITh95y3EoSrj1O0= 19 | distributions: sdist bdist_wheel 20 | skip_cleanup: true 21 | on: 22 | all_branches: true 23 | env: 24 | global: 25 | secure: XHoNCfNSD7F3JuQgsyZTRFm89XO76YtyO+8djQwt/n1gutzR01a72aJoCcRAsZPOG8zhDXLmERU+xWIyhJ+L+ORso22TU58v+MkeDlmc1bHCLf6/ILntyrZYCgIhID9HmEKcHrsCATEunWYWLCDaYIPVgjizl5MENczmYyanwA1ll0/zbXmNOdlIURX09OanQ+9Fm/+69BmHByYmVV3hjYwjVQ5egq1FVIdlkA1M4uk5tdMuNTXwRpNnpGxgPETir1Mz/7Ehw5cSRh7bt+9OpNJssF7yOCt5goJvpwTJodYvQ3VM64rt+iNOXhYg0S4ARiO7Uiam58Bo8GzPGqUTg3DfLKWY0v1NeTmCjRTavtO7gHWcgO6RJXVfY5BEGmHOL8hkIvF0RXUJi1HazUMjNNLkTwQnZ6pHPSZpX3Od6ivcDSO294b7rjXWpmtdI0pgPV5FYc7gfme52OwFemJLrxvUsvql2yaHrnAccKns6AYXE+T2Z3GV9WCpNkzy7Gg7M4PjkBovqZQsdHlQJNLhbz0qI01X9aTiQ/9Yc2fKiRLtcSULD2VbqhHQw6udMvxuRSLzSCJ2LAMHdrI7XeIyVFGiCzqaW0U5JIV1pfg5brDJH/oZBkZKBAzhHxihReo8Je82we4QFvReYhhfyFKHO0YNZG0E7jLia2ObNhHAFRQ= 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 LexyCore, Ltd. http://lexycore.github.io/django-rester 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django-Rester 2 | ============= 3 | 4 | [![build](https://travis-ci.org/lexycore/django-rester.svg?branch=master)](https://travis-ci.org/lexycore/django-rester) 5 | [![codacy](https://api.codacy.com/project/badge/Grade/dee291831b0b43158e2d2301726e2c00)](https://www.codacy.com/app/lexycore/django-rester/dashboard) 6 | [![pypi](https://img.shields.io/pypi/v/django-rester.svg)](https://pypi.python.org/pypi/django-rester) 7 | [![license](https://img.shields.io/pypi/l/django-rester.svg)](https://github.com/lexycore/django-rester/blob/master/LICENSE) 8 | 9 | ### Package for creating API with built-in validation and authentication 10 | 11 | This product is designed to build API endpoints of varying complexity and nesting. 12 | 13 | The core is a view class - BaseApiView (the inheritor of the standard django view) 14 | 15 | *** 16 | ##### 1. requirements 17 | 18 | 1. Python 3+ 19 | 20 | 2. Django 1.11+ 21 | 22 | *** 23 | ##### 2. settings 24 | 25 | DEFAULT settings (may be overridden): 26 | ```python 27 | DJANGO_RESTER = { 28 | 'AUTH_BACKEND': 'django_rester.rester_jwt', 29 | 'RESPONSE_STRUCTURE': False, 30 | 'CORS_ACCESS': False, 31 | 'FIELDS_CHECK_EXCLUDED_METHODS': ['OPTIONS', 'HEAD'], 32 | 'SOFT_RESPONSE_VALIDATION': False, 33 | } 34 | 35 | DJANGO_RESTER_JWT: { 36 | 'SECRET': 'secret_key', 37 | 'EXPIRE': 60 * 60 * 24 * 14, # seconds 38 | 'AUTH_HEADER': 'Authorization', 39 | 'AUTH_HEADER_PREFIX': 'jwt', 40 | 'ALGORITHM': 'HS256', 41 | 'PAYLOAD_LIST': ['username'], 42 | 'USE_REDIS': False, # here can be an int value (redis db number) 43 | 'LOGIN_FIELD': 'username', # as default django login field 44 | } 45 | ``` 46 | 47 | **DJANGO_RESTER** - django-rester settings: 48 | 49 |      **AUTH_BACKEND** - authentication backend* 50 | 51 |      **RESPONSE_STRUCTURE** - Either False or a dict with 'success', 'message' and 'data' as a values 52 | 53 |      **CORS_ACCESS** - CORS control: True, False, '*', hosts_string 54 | 55 |      **FIELDS_CHECK_EXCLUDED_METHODS** - methods, which will not be processed with body structure checks 56 | 57 |      **SOFT_RESPONSE_VALIDATION** - if True, response will not be cut off if it will contain additional to response_structure fields 58 | 59 | **DJANGO_RESTER_JWT** - JWT authentication settings (in case of 'RESTER_AUTH_BACKEND' = 'django_rester.rester_jwt')*: 60 | 61 |      **SECRET** - JWT secret key 62 | 63 |      **EXPIRE** - token expiration time (datetime.now() + RESTER_EXPIRATION_DELTA) 64 | 65 |      **AUTH_HEADER** - HTTP headed, which will be used for auth token. 66 | 67 |      **AUTH_HEADER_PREFIX** - prefix for auth token ("Authorization:\ \") 68 | 69 |      **ALGORITHM** - cypher algorithm 70 | 71 |      **PAYLOAD_LIST** - payload list for token encode (will take specified **user** attributes to create token) 72 | 73 |      **USE_REDIS** - use redis-server to store tokens or not 74 | 75 |      **LOGIN_FIELD** - user login field (default is 'username' as in django) 76 | *** 77 | 78 | ##### 3. built-in statuses 79 | 80 | ```from django_rester.status import ...``` 81 |


82 | slightly modified status.py from [DRF](http://www.django-rest-framework.org/), it's simple and easy to understand. 83 | 84 | Any statuses used in this documentation are described in that file. 85 | *** 86 | ##### 4. built-in exceptions: 87 | 88 | 89 | ```from django_rester.exceptions import ...``` 90 |


91 | Exceptions, which will help you to recognise errors related to django-rester 92 | 93 | **class ResterException(Exception)** 94 | 95 |     base django-rester exception, standard Exception inheritor 96 | 97 | **class ResponseError(Exception)** 98 | 99 |     ResponseError inheritor, added response status - HTTP_500_INTERNAL_SERVER_ERROR 100 | 101 | **class ResponseBadRequest(ResponseError)** 102 | 103 |     ResponseError inheritor, response status changed to HTTP_400_BAD_REQUEST 104 | 105 | **class ResponseServerError(ResponseError)** 106 | 107 |     ResponseError inheritor 108 | 109 | **class ResponseAuthError(ResponseError)** 110 | 111 |     ResponseError inheritor, response status changed to HTTP_401_UNAUTHORIZED 112 | 113 | **class ResponseOkMessage(ResponseError)** 114 | 115 |     ResponseError inheritor 116 | 117 |     acceptable arguments: *, message='', data=None, status=HTTP_200_OK 118 | 119 | **class ResponseFailMessage(ResponseError)** 120 | 121 |     ResponseError inheritor 122 | 123 |     acceptable arguments: *, message='', data=None, status=HTTP_500_INTERNAL_SERVER_ERROR 124 | 125 | **class ResponseBadRequestMsgList(ResponseError)** 126 | 127 |     ResponseError inheritor 128 | 129 |     acceptable arguments: *, messages=None, status=HTTP_400_BAD_REQUEST 130 | 131 |     messages could be list, tuple or string. 132 | 133 | **class JSONFieldError(ResterException)** 134 | 135 |     ResterException inheritor, base JSONField exception 136 | 137 | **class JSONFieldModelTypeError(JSONFieldError)** 138 | 139 |     JSONField exception, raises when type of model parameter is not valid 140 | 141 | **class JSONFieldModelError(JSONFieldError)** 142 | 143 |     JSONField exception, raises when value of model parameter is not valid 144 | 145 | **class JSONFieldTypeError(JSONFieldError)** 146 | 147 |     JSONField exception, simple TypeError inside JSONField class 148 | 149 | **class JSONFieldValueError(JSONFieldError)** 150 | 151 |     JSONField exception, simple ValueError inside JSONField class 152 | 153 | **class BaseAPIViewException(Exception)** 154 | 155 |     BaseAPIView exception class 156 | 157 | **class RequestStructureException(BaseAPIViewException)** 158 | 159 |     raise if request structure is invalid 160 | 161 | **class ResponseStructureException(RequestStructureException)** 162 | 163 |     raise if response structure is invalid 164 | *** 165 | ##### 5. permission classes 166 | 167 | ```from django_rester.permission import ...``` 168 |


169 | Permission classes created to interact wih **@permissions()** decorator (good example of usage), or in any other way you want 170 | 171 | All permission classes accepts only one argument on **init** - django view **request** object. 172 | 173 | All permission classes has 2 attributes, defined on **init**: 174 | 175 | **check**: Bool - returns **True** or **False** if request.user may or may not access endpoint method 176 | 177 | **message**: could be a string or list of messages 178 |


179 | **class BasePermission** 180 | 181 |     contains all base permission methods, it is not recommended to use it directly in projects 182 | 183 | **class IsAuthenticated(BasePermission)** 184 | 185 |     check = **True** if user authenticated and active, else **False** 186 | 187 | **class IsAdmin(BasePermission)** 188 | 189 |     check = **True** if user authenticated and active and is_superuser, else **False** 190 | 191 | **class AllowAny(BasePermission)** 192 | 193 |     check = **True** for any user (even anonymous) 194 | 195 | *** 196 | ##### 6. built-in decorators 197 | 198 | ```from django_rester.decorators import ...``` 199 |


200 | **@permissions()** 201 | 202 |     accepts permission class or list, tuple of classes. 203 | 204 |     if check is passed, then user will be allowed to use endpoint 205 | 206 | example: 207 | ``` 208 | class Example(BaseApiView): 209 | 210 | @permissions(IsAdmin) 211 | def post(request, request_data, *args, **kwargs): 212 | pass 213 | ``` 214 | *** 215 | 216 | ##### 7. built-in views 217 | 218 | ```from django_rester.views import ...``` 219 |


220 | **class BaseApiView(View)** 221 | 222 | inherits from standard django view. 223 | 224 | class attributes: 225 | 226 |     **auth** - authentication backend instance 227 | 228 |     **request_fields** - request validator (use JSONField to build this validator) 229 | 230 |     **response_fields** - response validator (use JSONField to build this validator) 231 | 232 |
233 | 234 | class HTTP methods (get, post, put, etc...) accepts next arguments: request, request_data, *args, **kwargs 235 | 236 |     **request** - standard django view request object 237 | 238 |     **request_data** - all received request parameters as json serialized object 239 | 240 | User authentication with selected authentication backend 241 |


242 | **class Login(BaseApiView)** 243 | 244 | Could be used to authenticate user with selected authentication backend. 245 | 246 |     Allowed method is 'POST' only. 247 | 248 |     Requires username and password in request parameters (username fieldname parameter may be set in settings) 249 | 250 |     Returns token and HTTP_200_OK status code if authentication success, error message and HTTP_401_UNAUTHORIZED if failed 251 |


252 | **class Logout(BaseApiView)** 253 | 254 | Could be used to logout (with redis support) or just to let know frontend about logout process. 255 |


256 | Any view could be used the same way, here is a **simple example**: 257 | 258 |     **app/views.py:** 259 | ```python 260 | from django_rester.views import BaseAPIView 261 | from django_rester.decorators import permissions 262 | from django_rester.exceptions import ResponseOkMessage 263 | from django_rester.permission import IsAdmin 264 | from django_rester.status import HTTP_200_OK 265 | from app.models import Model # import Model from your application 266 | from django_rester.fields import JSONField 267 | 268 | class TestView(BaseAPIView): 269 | 270 | request_fields = {"POST": { 271 | "id": JSONField(field_type=int, required=True, ), 272 | "title": JSONField(field_type=str, required=True, default='some_title'), 273 | "fk": [{"id": JSONField(field_type=int, required=True)}], 274 | }} 275 | 276 | response_fields = {"POST": { 277 | "id": JSONField(field_type=int, required=True, ), 278 | "title": JSONField(field_type=str, required=True, default='some_title'), 279 | # ... 280 | }} 281 | 282 | def retrieve_items(): 283 | return Model.objects.all() 284 | 285 | def create_item(title): 286 | item, cre = Model.objects.get_or_create(title=title) 287 | return item, cre 288 | 289 | @permissions(AllowAny) 290 | def get(self, request, request_data, *args, **kwargs): 291 | items = self.retrieve_items() 292 | response_data = {...here we should build some response structure...}*** 293 | return response_data, HTTP_200_OK 294 | 295 | @permissions(IsAdmin) 296 | def post(self, request, request_data, *args, **kwargs): 297 | title = request_data.get('title', None) 298 | # no need to check 'if title', because it is allready validated by 'available_fields' 299 | # ... here we will do some view magic with the rest request_data 300 | item, cre = self.create_item(title) 301 | if not cre: 302 | raise ResponseOkMessage(message='Item allready exists', data={'title': title}) 303 | response_data = {...here we should build some response structure...}*** 304 | 305 | return response_data 306 | ``` 307 | 308 |     **app/urls.py:** 309 | ```python 310 | from django.conf.urls import url 311 | from .views import TestView 312 | 313 | urlpatterns = [ 314 | url(r'^test/', TestView.as_view()), 315 | ] 316 | ``` 317 | *** 318 | 319 | ##### 8. built-in fields 320 | 321 | ```from django_rester.fields import ...``` 322 |


323 | **class JSONField** 324 | 325 | class attributes: 326 | 327 |     **field_type** - data type (int, float, str, bool) 328 | 329 |     **required** - field is required 330 | 331 |     **default** - default value if not specified 332 | 333 |     **blank** - may or may not be blank 334 | 335 |     **model** - model for foreign relations 336 | 337 |     **field** - field for foreign relations 338 | 339 | methods (public), with normal usage, you won't need them in your code: 340 | 341 |     **check_type** - validate type of JSONField value 342 | 343 |     **validate** - validate field value with parameters 344 | *** 345 | 346 | *- There is only one authentication backend available for now - RESTER_JWT 347 | 348 | **- BaseApiView is on active development stage, other attributes and methods could be added soon 349 | 350 | ***- automatic response structure build - one of the nearest tasks 351 | 352 | 353 | ### Installation notes 354 | 355 | 356 | ##### pycurl (Mac OS) 357 | 358 | ```bash 359 | brew remove curl 360 | brew install curl-openssl 361 | export PYCURL_SSL_LIBRARY=openssl 362 | pip install --no-cache-dir --global-option=build_ext --global-option="-L/usr/local/opt/openssl/lib" --global-option="-I/usr/local/opt/openssl/include" --compile --install-option="--with-openssl" pycurl 363 | ``` 364 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect 2 | -------------------------------------------------------------------------------- /django_rester/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexycore/django-rester/4959f8c0094cf847c29f0aba68ec730a9d68f761/django_rester/__init__.py -------------------------------------------------------------------------------- /django_rester/client.py: -------------------------------------------------------------------------------- 1 | import pycurl 2 | import logging 3 | import json as json_lib 4 | import urllib.parse as urllib_parse 5 | from urllib.parse import urljoin 6 | from io import BytesIO 7 | 8 | from django_rester.exceptions import ResterException 9 | 10 | logger = logging.getLogger('django_rester.client') 11 | 12 | DEFAULT_OPTIONS = { 13 | 'HTTPHEADER': ['Accept: application/json', 14 | 'Accept-Charset: UTF-8', 15 | 'Content-Type: application/json; charset=utf-8'], 16 | 'CONNECTTIMEOUT': 5, 17 | 'TIMEOUT': 8, 18 | 'COOKIEFILE': '', 19 | 'FAILONERROR': 0, 20 | } 21 | 22 | '''A high-level interface to the pycurl extension''' 23 | 24 | 25 | # ** mfx NOTE: the CGI class uses "black magic" using COOKIEFILE in 26 | # combination with a non-existent file name. See the libcurl docs 27 | # for more info. 28 | 29 | 30 | # We should ignore SIGPIPE when using pycurl.NOSIGNAL - see 31 | # the libcurl tutorial for more info. 32 | # try: 33 | # import signal 34 | # from signal import SIGPIPE, SIG_IGN 35 | # except ImportError: 36 | # pass 37 | # else: 38 | # signal.signal(SIGPIPE, SIG_IGN) 39 | 40 | 41 | class Curl: 42 | """High-level interface to pycurl functions.""" 43 | 44 | def __init__(self, base_url="", headers=None): 45 | self.handle = pycurl.Curl() 46 | # These members might be set. 47 | self.base_url = "" 48 | self.set_url(base_url) 49 | self.verbosity = 0 50 | self.headers = headers or [] 51 | # Nothing past here should be modified by the caller. 52 | self.payload = None 53 | self.payload_io = BytesIO() 54 | self.hdr = "" 55 | # Verify that we've got the right site; harmless on a non-SSL connect. 56 | self.set_option(pycurl.SSL_VERIFYHOST, 2) 57 | # Follow redirects in case it wants to take us to a CGI... 58 | self.set_option(pycurl.FOLLOWLOCATION, 1) 59 | self.set_option(pycurl.MAXREDIRS, 5) 60 | self.set_option(pycurl.NOSIGNAL, 1) 61 | # Setting this option with even a nonexistent file makes libcurl 62 | # handle cookie capture and playback automatically. 63 | self.set_option(pycurl.COOKIEFILE, "/dev/null") 64 | # Set timeouts to avoid hanging too long 65 | self.set_timeout(30) 66 | # Use password identification from .netrc automatically 67 | self.set_option(pycurl.NETRC, 1) 68 | self.set_option(pycurl.WRITEFUNCTION, self.payload_io.write) 69 | 70 | def header_callback(x): 71 | self.hdr += x.decode('ascii') 72 | 73 | self.set_option(pycurl.HEADERFUNCTION, header_callback) 74 | 75 | def set_timeout(self, timeout): 76 | """Set timeout for a retrieving an object""" 77 | self.set_option(pycurl.TIMEOUT, timeout) 78 | 79 | def set_url(self, url): 80 | """Set the base URL to be retrieved.""" 81 | self.base_url = url 82 | self.set_option(pycurl.URL, self.base_url) 83 | 84 | def set_option(self, *args): 85 | """Set an option on the retrieval.""" 86 | self.handle.setopt(*args) 87 | 88 | def set_verbosity(self, level): 89 | """Set verbosity to 1 to see transactions.""" 90 | self.set_option(pycurl.VERBOSE, level) 91 | 92 | def __request(self, relative_url=None): 93 | """Perform the pending request.""" 94 | if self.headers: 95 | self.set_option(pycurl.HTTPHEADER, self.headers) 96 | if relative_url: 97 | self.set_option(pycurl.URL, urljoin(self.base_url, relative_url)) 98 | self.payload = None 99 | self.payload_io.seek(0) 100 | self.payload_io.truncate() 101 | self.hdr = "" 102 | try: 103 | self.handle.perform() 104 | except ResterException: 105 | pass 106 | self.payload = self.payload_io.getvalue() 107 | return (self.payload, 108 | self.handle.getinfo(pycurl.RESPONSE_CODE), 109 | self.handle.errstr()) 110 | 111 | def get(self, url="", params=None): 112 | """Ship a GET request for a specified URL, capture the response.""" 113 | if params: 114 | url += "?" + urllib_parse.urlencode(params) 115 | self.set_option(pycurl.HTTPGET, 1) 116 | return self.__request(url) 117 | 118 | def post(self, url="", params=None): 119 | """Ship a POST request to a specified CGI, capture the response.""" 120 | self.set_option(pycurl.POST, 1) 121 | if params: 122 | self.set_option(pycurl.POSTFIELDS, urllib_parse.urlencode(params)) 123 | return self.__request(url) 124 | 125 | def body(self): 126 | """Return the body from the last response.""" 127 | return self.payload 128 | 129 | def header(self): 130 | """Return the header from the last response.""" 131 | return self.hdr 132 | 133 | def get_info(self, *args): 134 | """Get information about retrieval.""" 135 | return self.handle.getinfo(*args) 136 | 137 | def info(self): 138 | """Return a dictionary with all info on the last response.""" 139 | m = {'effective-url': self.handle.getinfo(pycurl.EFFECTIVE_URL), 140 | 'http-code': self.handle.getinfo(pycurl.HTTP_CODE), 141 | 'total-time': self.handle.getinfo(pycurl.TOTAL_TIME), 142 | 'namelookup-time': self.handle.getinfo(pycurl.NAMELOOKUP_TIME), 143 | 'connect-time': self.handle.getinfo(pycurl.CONNECT_TIME), 144 | 'pretransfer-time': self.handle.getinfo(pycurl.PRETRANSFER_TIME), 145 | 'redirect-time': self.handle.getinfo(pycurl.REDIRECT_TIME), 146 | 'redirect-count': self.handle.getinfo(pycurl.REDIRECT_COUNT), 147 | 'size-upload': self.handle.getinfo(pycurl.SIZE_UPLOAD), 148 | 'size-download': self.handle.getinfo(pycurl.SIZE_DOWNLOAD), 149 | 'speed-upload': self.handle.getinfo(pycurl.SPEED_UPLOAD), 150 | 'header-size': self.handle.getinfo(pycurl.HEADER_SIZE), 151 | 'request-size': self.handle.getinfo(pycurl.REQUEST_SIZE), 152 | 'content-length-download': self.handle.getinfo( 153 | pycurl.CONTENT_LENGTH_DOWNLOAD), 154 | 'content-length-upload': self.handle.getinfo( 155 | pycurl.CONTENT_LENGTH_UPLOAD), 156 | 'content-type': self.handle.getinfo(pycurl.CONTENT_TYPE), 157 | 'response-code': self.handle.getinfo(pycurl.RESPONSE_CODE), 158 | 'speed-download': self.handle.getinfo(pycurl.SPEED_DOWNLOAD), 159 | 'ssl-verifyresult': self.handle.getinfo(pycurl.SSL_VERIFYRESULT), 160 | 'filetime': self.handle.getinfo(pycurl.INFO_FILETIME), 161 | 'starttransfer-time': self.handle.getinfo( 162 | pycurl.STARTTRANSFER_TIME), 163 | 'http-connectcode': self.handle.getinfo(pycurl.HTTP_CONNECTCODE), 164 | 'httpauth-avail': self.handle.getinfo(pycurl.HTTPAUTH_AVAIL), 165 | 'proxyauth-avail': self.handle.getinfo(pycurl.PROXYAUTH_AVAIL), 166 | 'os-errno': self.handle.getinfo(pycurl.OS_ERRNO), 167 | 'num-connects': self.handle.getinfo(pycurl.NUM_CONNECTS), 168 | 'ssl-engines': self.handle.getinfo(pycurl.SSL_ENGINES), 169 | 'cookielist': self.handle.getinfo(pycurl.INFO_COOKIELIST), 170 | 'lastsocket': self.handle.getinfo(pycurl.LASTSOCKET), 171 | 'ftp-entry-path': self.handle.getinfo(pycurl.FTP_ENTRY_PATH)} 172 | return m 173 | 174 | def answered(self, check): 175 | """Did a given check string occur in the last payload?""" 176 | return self.payload.find(check) >= 0 177 | 178 | def close(self): 179 | """Close a session, freeing resources.""" 180 | if self.handle: 181 | self.handle.close() 182 | self.handle = None 183 | self.hdr = "" 184 | self.payload = "" 185 | 186 | def __del__(self): 187 | self.close() 188 | 189 | 190 | class ResterClient(Curl): 191 | def __init__(self, base_url='', headers=None, opts=None): 192 | super().__init__(base_url, headers or []) 193 | all_opts = dict(DEFAULT_OPTIONS) 194 | all_opts.update(opts or {}) 195 | self.set_options(all_opts) 196 | 197 | def set_options(self, opts): 198 | for key, value in opts.items(): 199 | opt_key = getattr(pycurl, key.replace('-', '_').upper(), None) 200 | if opt_key: 201 | self.set_option(opt_key, value) 202 | 203 | def get(self, url='', params=None): 204 | response, response_code, msg = super().get(url, params) 205 | return self._response_decode(response, response_code, msg) 206 | 207 | def post(self, url='', params=None, json=None): 208 | if json: 209 | body = json_lib.dumps(json) 210 | self.set_option(pycurl.POSTFIELDS, body) 211 | response, response_code, msg = super().post(url, params) 212 | return self._response_decode(response, response_code, msg) 213 | 214 | @staticmethod 215 | def _response_decode(response, response_code=200, message=''): 216 | resp_type = None 217 | data = None 218 | if response: 219 | try: 220 | data = json_lib.loads(isinstance(response, bytes) 221 | and response.decode('utf-8') 222 | or response) 223 | resp_type = 'json' 224 | except json_lib.decoder.JSONDecodeError: 225 | data = response 226 | resp_type = 'text' 227 | 228 | return {'type': resp_type, 229 | 'message': message and [message] or [], 230 | 'response_code': response_code, 231 | 'data': data} 232 | -------------------------------------------------------------------------------- /django_rester/config.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.6' 2 | -------------------------------------------------------------------------------- /django_rester/decorators.py: -------------------------------------------------------------------------------- 1 | from .permission import BasePermission 2 | from .status import HTTP_401_UNAUTHORIZED 3 | 4 | 5 | def permissions(*perms): 6 | def permissions_decorator(f): 7 | def wrapper(view, request, *args, **kwargs): 8 | checked, message = True, '' 9 | for perm_item in perms: 10 | if issubclass(perm_item, BasePermission): 11 | auth_check = perm_item(request) 12 | checked, message = auth_check.check, auth_check.message 13 | if not checked: 14 | break 15 | if checked: 16 | data = f(view, request, *args, **kwargs) 17 | else: 18 | data = message, HTTP_401_UNAUTHORIZED 19 | return data 20 | 21 | return wrapper 22 | 23 | return permissions_decorator 24 | -------------------------------------------------------------------------------- /django_rester/exceptions.py: -------------------------------------------------------------------------------- 1 | from .status import ( 2 | HTTP_500_INTERNAL_SERVER_ERROR, 3 | HTTP_400_BAD_REQUEST, 4 | HTTP_401_UNAUTHORIZED, 5 | HTTP_200_OK, 6 | ) 7 | 8 | 9 | class ResterException(Exception): 10 | pass 11 | 12 | 13 | class ResponseError(ResterException): 14 | response_status = HTTP_500_INTERNAL_SERVER_ERROR 15 | 16 | 17 | class ResponseBadRequest(ResponseError): 18 | response_status = HTTP_400_BAD_REQUEST 19 | 20 | 21 | class ResponseServerError(ResponseError): 22 | response_status = HTTP_500_INTERNAL_SERVER_ERROR 23 | 24 | 25 | class ResponseAuthError(ResponseError): 26 | response_status = HTTP_401_UNAUTHORIZED 27 | 28 | 29 | class ResponseOkMessage(ResponseError): 30 | def __init__(self, message='', data=None, status=HTTP_200_OK): 31 | self.message = message 32 | self.data = data 33 | self.response_status = status 34 | 35 | 36 | class ResponseFailMessage(ResponseError): 37 | def __init__(self, message='', data=None, 38 | status=HTTP_500_INTERNAL_SERVER_ERROR): 39 | self.message = message 40 | self.data = data 41 | self.response_status = status 42 | 43 | 44 | class ResponseBadRequestMsgList(ResponseError): 45 | def __init__(self, messages=None, status=HTTP_400_BAD_REQUEST): 46 | self.response_status = status 47 | if messages and isinstance(messages, (list, tuple)): 48 | self.messages = list(messages) 49 | elif isinstance(messages, str): 50 | self.messages = [messages] 51 | else: 52 | self.messages = [] 53 | 54 | 55 | class JSONFieldError(ResterException): 56 | # base JSONField exception 57 | pass 58 | 59 | 60 | class JSONFieldModelTypeError(JSONFieldError): 61 | # JSONField exception, raises when type of model parameter is not valid 62 | pass 63 | 64 | 65 | class JSONFieldModelError(JSONFieldError): 66 | # JSONField exception, raises when value of model parameter is not valid 67 | pass 68 | 69 | 70 | class JSONFieldTypeError(JSONFieldError): 71 | # JSONField exception, simple TypeError inside JSONField class 72 | pass 73 | 74 | 75 | class JSONFieldValueError(JSONFieldError): 76 | # JSONField exception, simple ValueError inside JSONField class 77 | pass 78 | 79 | 80 | class BaseAPIViewException(Exception): 81 | # BaseAPIView exception class 82 | pass 83 | 84 | 85 | class MsgListException(BaseAPIViewException): 86 | def __init__(self, messages=None, status=HTTP_400_BAD_REQUEST): 87 | self.response_status = status 88 | if messages and isinstance(messages, (list, tuple)): 89 | self.messages = list(messages) 90 | elif isinstance(messages, str): 91 | self.messages = [messages] 92 | else: 93 | self.messages = [] 94 | 95 | 96 | class RequestStructureException(MsgListException): 97 | # raise if request structure is invalid 98 | pass 99 | 100 | 101 | class ResponseStructureException(MsgListException): 102 | # raise if response structure is invalid 103 | pass 104 | 105 | 106 | class CustomValidationException(MsgListException): 107 | # raise if there error in custom validation 108 | pass 109 | -------------------------------------------------------------------------------- /django_rester/fields.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | from django.db.models.base import ModelBase 3 | from .exceptions import ( 4 | JSONFieldModelTypeError, 5 | JSONFieldModelError, 6 | JSONFieldValueError 7 | ) 8 | 9 | 10 | class JSONField: 11 | types = (int, float, str, bool) 12 | 13 | def __init__(self, field_type=None, required=False, default=None, 14 | blank=True, model=None, field=''): 15 | if field_type not in self.types: 16 | raise JSONFieldValueError('field_type should be one of: {}'.format( 17 | str(self.types).replace("", ''))) 18 | self.field_type = field_type 19 | self.required = required or (not blank) 20 | self.default = default 21 | self.blank = blank 22 | self.key = '' 23 | self.model = self._set_model(model) 24 | self.field = field 25 | 26 | if model and not field: 27 | self.field = 'id' 28 | 29 | # if not self.var_type: 30 | # raise ValueError('var_type should be specified') 31 | 32 | def check_type(self, value): 33 | messages = [] 34 | try: 35 | _value = self.field_type(value) 36 | except (TypeError, ValueError): 37 | messages.append('Could not treat {} value `{}` as {}'.format( 38 | self.key or '', value, self.field_type)) 39 | _value = None 40 | return _value, messages 41 | 42 | def validate(self, key, value): 43 | self.key = key 44 | messages = [] 45 | if self.required and (value is None): 46 | messages.append('`{}` value is required'.format(key)) 47 | else: 48 | if not self.blank and value == '': 49 | messages.append('`{}` value blank is not allowed'.format(key)) 50 | elif (self.default is not None) and (value is None): 51 | value = self.default 52 | if messages: 53 | value = None 54 | else: 55 | value, msg = self.check_type(value) 56 | messages += msg 57 | return value, messages 58 | 59 | @staticmethod 60 | def _set_model(model): 61 | if not model: 62 | return None 63 | elif isinstance(model, ModelBase): 64 | return model 65 | elif isinstance(model, str): 66 | try: 67 | app, _model = model.split('.') 68 | model = apps.get_model(app, _model) 69 | except (ValueError, LookupError): 70 | raise JSONFieldModelError('model name does not match pattern ' 71 | '. or model ' 72 | 'does not exist') 73 | return model 74 | else: 75 | raise JSONFieldModelTypeError('wrong model type (NoneType, string ' 76 | 'or ModelBase allowed)') 77 | 78 | 79 | class String(JSONField): 80 | def __init__(self, 81 | required=False, default=None, 82 | blank=True, model=None, field=''): 83 | super().__init__(field_type=str, required=required, default=default, 84 | blank=blank, model=model, field=field) 85 | 86 | 87 | class Int(JSONField): 88 | def __init__(self, 89 | required=False, default=None, 90 | blank=True, model=None, field=''): 91 | super().__init__(field_type=int, required=required, default=default, 92 | blank=blank, model=model, field=field) 93 | 94 | 95 | class Float(JSONField): 96 | def __init__(self, 97 | required=False, default=None, 98 | blank=True, model=None, field=''): 99 | super().__init__(field_type=float, required=required, default=default, 100 | blank=blank, model=model, field=field) 101 | 102 | 103 | class Bool(JSONField): 104 | def __init__(self, 105 | required=False, default=None, 106 | blank=True, model=None, field=''): 107 | super().__init__(field_type=bool, required=required, default=default, 108 | blank=blank, model=model, field=field) 109 | -------------------------------------------------------------------------------- /django_rester/permission.py: -------------------------------------------------------------------------------- 1 | class BasePermission: 2 | def __init__(self, request): 3 | self.check, self.message = False, '' 4 | self.request = request 5 | 6 | @staticmethod 7 | def _get_message(check, messages): 8 | return messages.get('SUCCESS' if check else 'FAIL', None) 9 | 10 | 11 | class IsAuthenticated(BasePermission): 12 | def __init__(self, request): 13 | super().__init__(request) 14 | self.check, self.message = self._is_authenticated() 15 | 16 | def _is_authenticated(self): 17 | check = (self.request.user.is_authenticated 18 | and not self.request.user.is_anonymous 19 | and self.request.user.is_active) 20 | messages = {'SUCCESS': ['Auth OK'], 21 | 'FAIL': ['Required credentials are not provided']} 22 | message = self._get_message(check, messages) 23 | return check, message 24 | 25 | 26 | class IsAdmin(BasePermission): 27 | def __init__(self, request): 28 | super().__init__(request) 29 | self.check, self.message = self._is_admin() 30 | 31 | def _is_admin(self): 32 | check = self.request.user.is_superuser and self.request.user.is_active 33 | messages = {'SUCCESS': ['Auth OK'], 34 | 'FAIL': ['Required credentials are not provided ' 35 | 'or user is not superuser']} 36 | message = self._get_message(check, messages) 37 | return check, message 38 | 39 | 40 | class AllowAny(BasePermission): 41 | def __init__(self, request): 42 | super().__init__(request) 43 | self.check, self.message = self._allow_any() 44 | 45 | def _allow_any(self): 46 | check = True 47 | messages = {'SUCCESS': ['Auth OK']} 48 | message = self._get_message(check, messages) 49 | return check, message 50 | -------------------------------------------------------------------------------- /django_rester/rester_jwt/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import Auth 2 | 3 | __all__ = ['Auth'] 4 | -------------------------------------------------------------------------------- /django_rester/rester_jwt/auth.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import jwt 3 | from django.contrib.auth import authenticate 4 | from django.contrib.auth import get_user_model 5 | from django_rediser import RedisStorage 6 | from ..status import HTTP_200_OK 7 | from .settings import rester_jwt_settings 8 | from django_rester.exceptions import ResponseAuthError 9 | 10 | redis_db = None 11 | if ( 12 | isinstance(rester_jwt_settings['USE_REDIS'], int) 13 | and not isinstance(rester_jwt_settings['USE_REDIS'], bool) 14 | ): 15 | redis_db = rester_jwt_settings['USE_REDIS'] 16 | 17 | 18 | class BaseAuth: 19 | _key = '_token' 20 | _rs = RedisStorage(db=redis_db) 21 | settings = rester_jwt_settings 22 | 23 | @classmethod 24 | def _set_payload(cls, user): 25 | exp = datetime.datetime.timestamp( 26 | datetime.datetime.now() + datetime.timedelta( 27 | seconds=cls.settings['EXPIRE'])) 28 | payload = {item: getattr(user, item, None) for item in 29 | cls.settings['PAYLOAD_LIST'] if 30 | hasattr(user, item)} 31 | payload.update({"exp": exp}) 32 | return payload 33 | 34 | def _push_token(self, token): 35 | self._rs.sadd(self._key, token) 36 | 37 | def _rem_token(self, token): 38 | return self._rs.srem(self._key, token) 39 | 40 | def _is_member(self, token): 41 | return self._rs.sismember(self._key, token) 42 | 43 | @classmethod 44 | def _get_token(cls, request): 45 | token, messages = None, [] 46 | token = str(request.META.get(cls.settings['AUTH_HEADER'], '')) 47 | if token: 48 | if cls.settings['AUTH_HEADER_PREFIX'] and token.startswith( 49 | cls.settings['AUTH_HEADER_PREFIX']): 50 | token = token[len(cls.settings['AUTH_HEADER_PREFIX']):].lstrip() 51 | else: 52 | messages.append('Wrong token prefix') 53 | return token, messages 54 | 55 | @staticmethod 56 | def _get_user(**kwargs): 57 | user_model = get_user_model() 58 | user = user_model.objects.get(**kwargs) 59 | return user 60 | 61 | def _get_user_data(self, token): 62 | is_member, data, exp_date = True, None, None 63 | user_data, user, messages = {}, None, [] 64 | if self.settings['USE_REDIS']: 65 | is_member = self._is_member(token) 66 | if is_member: 67 | try: 68 | data = jwt.decode(token, self.settings['SECRET'], 69 | algorithms=[self.settings['ALGORITHM']]) 70 | except jwt.DecodeError: 71 | messages.append('Wrong authentication token') 72 | except jwt.ExpiredSignatureError: 73 | messages.append('Authentication token expired') 74 | 75 | if data: 76 | exp_date = data.pop('exp', None) 77 | user_data = {item: data.get(item, None) for item in 78 | self.settings['PAYLOAD_LIST']} 79 | if ( 80 | exp_date 81 | and user_data 82 | and exp_date > datetime.datetime.now().timestamp() 83 | ): 84 | user = self._get_user(**user_data) 85 | else: 86 | messages.append('Authentication token is not valid or expired') 87 | 88 | return user, messages 89 | 90 | 91 | class Auth(BaseAuth): 92 | def login(self, request, request_data): 93 | login = request_data.get(self.settings['LOGIN_FIELD'], None) 94 | password = request_data.get('password', '') 95 | if login is not None: 96 | user = authenticate(username=login, password=password) 97 | else: 98 | user = None 99 | if user: 100 | payload = self._set_payload(user) 101 | token, status = jwt.encode(payload, self.settings['SECRET'], 102 | algorithm=self.settings[ 103 | 'ALGORITHM']).decode( 104 | 'utf-8'), HTTP_200_OK 105 | encoded = {'token': token} 106 | else: 107 | raise ResponseAuthError('Authentication failed') 108 | if status == HTTP_200_OK and self.settings['USE_REDIS'] and token: 109 | self._push_token(token) 110 | return encoded, status 111 | 112 | def logout(self, request, request_data): 113 | token, messages = self._get_token(request) 114 | result = None 115 | if self.settings['USE_REDIS']: 116 | result = self._rem_token(token) 117 | if result == 0: 118 | messages.append('Token not found') 119 | elif not result: 120 | messages.append('Simple logout') 121 | else: 122 | messages.append('Token logout') 123 | 124 | return messages, HTTP_200_OK 125 | 126 | def authenticate(self, request_data): 127 | user, messages = None, None 128 | token, messages = self._get_token(request_data) 129 | if token: 130 | user, messages = self._get_user_data(token) 131 | if messages: 132 | user = None 133 | return user, messages 134 | 135 | def register(self, request_data): 136 | pass 137 | -------------------------------------------------------------------------------- /django_rester/rester_jwt/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django_rester.singleton import Singleton 3 | 4 | 5 | class ResterSettings(dict, metaclass=Singleton): 6 | def __init__(self): 7 | super().__init__() 8 | _django_rester_jwt_settings = getattr(settings, 'DJANGO_RESTER_JWT', {}) 9 | username = _django_rester_jwt_settings.get('LOGIN_FIELD', 10 | self.login_field) 11 | self.update({ 12 | 'SECRET': _django_rester_jwt_settings.get('SECRET', 'secret_key'), 13 | 'EXPIRE': 14 | _django_rester_jwt_settings.get('EXPIRE', 60 * 60 * 24 * 14), 15 | 'AUTH_HEADER': self._auth_header( 16 | _django_rester_jwt_settings.get('AUTH_HEADER', 17 | 'Authorization')), 18 | 'AUTH_HEADER_PREFIX': 19 | _django_rester_jwt_settings.get('AUTH_HEADER_PREFIX', 'jwt'), 20 | 'ALGORITHM': _django_rester_jwt_settings.get('ALGORITHM', 'HS256'), 21 | 'PAYLOAD_LIST': 22 | _django_rester_jwt_settings.get('PAYLOAD_LIST', [username]), 23 | 'USE_REDIS': _django_rester_jwt_settings.get('USE_REDIS', False), 24 | 'LOGIN_FIELD': username, 25 | }) 26 | 27 | @staticmethod 28 | def _auth_header(header): 29 | http = 'HTTP_' 30 | header = str(header).strip().upper() 31 | return '{}{}'.format('' if header.startswith(http) else http, header) 32 | 33 | @property 34 | def login_field(self): 35 | try: 36 | tmp = __import__('..settings', globals(), locals(), 37 | ['rester_settings']) 38 | result = getattr(tmp, 'rester_settings').get('LOGIN_FIELD', '') 39 | except (ImportError, AttributeError, TypeError, ValueError): 40 | result = '' 41 | return result or 'username' 42 | 43 | 44 | rester_jwt_settings = ResterSettings() 45 | -------------------------------------------------------------------------------- /django_rester/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from django_rester.status import HTTP_200_OK 4 | from django_rester.singleton import Singleton 5 | 6 | 7 | class AuthMock: 8 | def login(self, request): 9 | return None, HTTP_200_OK 10 | 11 | def logout(self, request): 12 | return True, HTTP_200_OK 13 | 14 | def authenticate(self, request): 15 | return None, [] 16 | 17 | 18 | class ResterSettings(dict, metaclass=Singleton): 19 | 20 | def __init__(self): 21 | super().__init__() 22 | _django_rester_settings = getattr(settings, 'DJANGO_RESTER', {}) 23 | self.update({ 24 | 'LOGIN_FIELD': 25 | _django_rester_settings.get('LOGIN_FIELD', 'username'), 26 | 'RESPONSE_STRUCTURE': self._set_response_structure( 27 | _django_rester_settings.get('RESPONSE_STRUCTURE', False)), 28 | }) 29 | self.update({'AUTH_BACKEND': self._get_auth_backend( 30 | _django_rester_settings.get('AUTH_BACKEND', 31 | 'django_rester.rester_jwt'))}) 32 | self.update( # True, False, "*", hosts_string 33 | {'CORS_ACCESS': _django_rester_settings.get('CORS_ACCESS', False)}) 34 | self.update({'FIELDS_CHECK_EXCLUDED_METHODS': 35 | _django_rester_settings.get( 36 | 'FIELDS_CHECK_EXCLUDED_METHODS', 37 | ['OPTIONS', 'HEAD'])}) 38 | self.update({'SOFT_RESPONSE_VALIDATION': 39 | _django_rester_settings.get('SOFT_RESPONSE_VALIDATION', 40 | False)}) 41 | 42 | @staticmethod 43 | def _set_response_structure(structure): 44 | if isinstance(structure, bool) and structure: 45 | result = {'success': 'success', 46 | 'message': 'message', 47 | 'data': 'data', 48 | } 49 | elif isinstance(structure, dict): 50 | result = structure 51 | else: 52 | result = False 53 | return result 54 | 55 | @staticmethod 56 | def _get_auth_backend(auth_backend): 57 | try: 58 | tmp = __import__(auth_backend, globals(), locals(), ['Auth']) 59 | auth = getattr(tmp, 'Auth') 60 | except (ImportError, AttributeError): 61 | auth = AuthMock 62 | return auth 63 | 64 | 65 | rester_settings = ResterSettings() 66 | -------------------------------------------------------------------------------- /django_rester/singleton.py: -------------------------------------------------------------------------------- 1 | from threading import Lock 2 | 3 | 4 | class Singleton(type): 5 | #__singleton_lock = Lock() 6 | _instances = {} 7 | 8 | def __call__(cls, *args, **kwargs): 9 | if cls not in cls._instances: 10 | #with cls.__singleton_lock: 11 | cls._instances[cls] = super().__call__(*args, **kwargs) 12 | return cls._instances[cls] 13 | -------------------------------------------------------------------------------- /django_rester/status.py: -------------------------------------------------------------------------------- 1 | """ 2 | Descriptive HTTP status codes, for code readability. 3 | 4 | See RFC 2616 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html 5 | And RFC 6585 - http://tools.ietf.org/html/rfc6585 6 | And RFC 4918 - https://tools.ietf.org/html/rfc4918 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | 11 | def is_informational(code): 12 | return 100 <= code <= 199 13 | 14 | 15 | def is_success(code): 16 | return 200 <= code <= 299 17 | 18 | 19 | def is_redirect(code): 20 | return 300 <= code <= 399 21 | 22 | 23 | def is_client_error(code): 24 | return 400 <= code <= 499 25 | 26 | 27 | def is_server_error(code): 28 | return 500 <= code <= 599 29 | 30 | 31 | HTTP_100_CONTINUE = 100 32 | HTTP_101_SWITCHING_PROTOCOLS = 101 33 | HTTP_200_OK = 200 34 | HTTP_201_CREATED = 201 35 | HTTP_202_ACCEPTED = 202 36 | HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 37 | HTTP_204_NO_CONTENT = 204 38 | HTTP_205_RESET_CONTENT = 205 39 | HTTP_206_PARTIAL_CONTENT = 206 40 | HTTP_207_MULTI_STATUS = 207 41 | HTTP_300_MULTIPLE_CHOICES = 300 42 | HTTP_301_MOVED_PERMANENTLY = 301 43 | HTTP_302_FOUND = 302 44 | HTTP_303_SEE_OTHER = 303 45 | HTTP_304_NOT_MODIFIED = 304 46 | HTTP_305_USE_PROXY = 305 47 | HTTP_306_RESERVED = 306 48 | HTTP_307_TEMPORARY_REDIRECT = 307 49 | HTTP_400_BAD_REQUEST = 400 50 | HTTP_401_UNAUTHORIZED = 401 51 | HTTP_402_PAYMENT_REQUIRED = 402 52 | HTTP_403_FORBIDDEN = 403 53 | HTTP_404_NOT_FOUND = 404 54 | HTTP_405_METHOD_NOT_ALLOWED = 405 55 | HTTP_406_NOT_ACCEPTABLE = 406 56 | HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 57 | HTTP_408_REQUEST_TIMEOUT = 408 58 | HTTP_409_CONFLICT = 409 59 | HTTP_410_GONE = 410 60 | HTTP_411_LENGTH_REQUIRED = 411 61 | HTTP_412_PRECONDITION_FAILED = 412 62 | HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413 63 | HTTP_414_REQUEST_URI_TOO_LONG = 414 64 | HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 65 | HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 66 | HTTP_417_EXPECTATION_FAILED = 417 67 | HTTP_422_UNPROCESSABLE_ENTITY = 422 68 | HTTP_423_LOCKED = 423 69 | HTTP_424_FAILED_DEPENDENCY = 424 70 | HTTP_428_PRECONDITION_REQUIRED = 428 71 | HTTP_429_TOO_MANY_REQUESTS = 429 72 | HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 73 | HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS = 451 74 | HTTP_500_INTERNAL_SERVER_ERROR = 500 75 | HTTP_501_NOT_IMPLEMENTED = 501 76 | HTTP_502_BAD_GATEWAY = 502 77 | HTTP_503_SERVICE_UNAVAILABLE = 503 78 | HTTP_504_GATEWAY_TIMEOUT = 504 79 | HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 80 | HTTP_507_INSUFFICIENT_STORAGE = 507 81 | HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 82 | -------------------------------------------------------------------------------- /django_rester/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from json import JSONDecodeError 4 | 5 | from django.http import HttpResponse 6 | from django.views import View 7 | from django.views.decorators.csrf import csrf_exempt 8 | from .decorators import permissions 9 | from .permission import IsAuthenticated 10 | from .status import ( 11 | HTTP_200_OK, 12 | HTTP_500_INTERNAL_SERVER_ERROR, 13 | HTTP_400_BAD_REQUEST 14 | ) 15 | from .exceptions import ( 16 | RequestStructureException, 17 | ResponseError, 18 | ResponseBadRequestMsgList, 19 | ResponseOkMessage, 20 | ResponseFailMessage, 21 | ResponseStructureException, 22 | CustomValidationException, 23 | ) 24 | 25 | from . import fields 26 | from .settings import rester_settings 27 | 28 | logger = logging.getLogger('django_rester') 29 | 30 | 31 | class BaseAPIView(View): 32 | auth = rester_settings['AUTH_BACKEND']() 33 | request_fields, response_fields = {}, {} 34 | 35 | def __init__(self, *args, **kwargs): 36 | super().__init__(*args, **kwargs) 37 | self.request_data = None 38 | 39 | @classmethod 40 | def get_login_field(cls): 41 | return ( 42 | cls.auth.settings.get('LOGIN_FIELD') or 43 | rester_settings.get('LOGIN_FIELD') 44 | ) 45 | 46 | @classmethod 47 | def as_view(cls, **kwargs): 48 | view = super(BaseAPIView, cls).as_view() 49 | view.cls = cls 50 | return csrf_exempt(view) 51 | 52 | @property 53 | def _common_request_response_structure(self): 54 | # check if request_fields are common to any http method or not 55 | common_structure = False 56 | for key in self.request_fields.keys(): 57 | if key in self._allowed_methods(): 58 | common_structure = True 59 | break 60 | return common_structure 61 | 62 | def _set_response(self, _response): 63 | if isinstance(_response, (list, tuple)) and len(_response) == 2: 64 | response = _response[0] 65 | status = _response[1] 66 | else: 67 | response = _response 68 | status = HTTP_200_OK 69 | if isinstance(response, HttpResponse): 70 | result = response 71 | else: 72 | try: 73 | pure_response = json.dumps(response) 74 | content_type = 'application/json' 75 | except TypeError: 76 | pure_response = str(response) 77 | status = HTTP_500_INTERNAL_SERVER_ERROR 78 | content_type = 'text/plain' 79 | result = HttpResponse(pure_response, content_type=content_type, 80 | status=status) 81 | result = self._set_cors(result) 82 | return result 83 | 84 | def _data_validate(self, method, data, fields, exception, 85 | exception_message, msg_key='validate'): 86 | if fields == {}: 87 | return data 88 | if self._common_request_response_structure: 89 | structure = fields.get(method, None) 90 | else: 91 | structure = fields 92 | if not structure and method not in rester_settings.get( 93 | 'FIELDS_CHECK_EXCLUDED_METHODS', []): 94 | raise exception(exception_message) 95 | structured_data, messages = self._check_json_field(data, structure) 96 | if not messages: 97 | try: 98 | structured_data = self.custom_validation(structured_data) 99 | assert structured_data is not None, \ 100 | '.custom_validation() should return ' \ 101 | 'validated structured data' 102 | except (AssertionError, CustomValidationException) as exc: 103 | messages = ['{}'.format(exc)] 104 | if fields is self.response_fields and rester_settings.get( 105 | 'SOFT_RESPONSE_VALIDATION', False): 106 | structured_data = self._add_filtered_data(data, structured_data) 107 | if messages: 108 | messages = [exception_message, {msg_key: messages}] 109 | raise exception(messages, HTTP_500_INTERNAL_SERVER_ERROR) 110 | return structured_data 111 | 112 | def _add_filtered_data(self, data, structured_data): 113 | # recursive function, validates response_data by response_fields 114 | value = None 115 | if isinstance(data, dict): 116 | for data_key, data_value in data.items(): 117 | structured_value = structured_data.get(data_key, None) 118 | correct_value = ( 119 | structured_value 120 | if structured_value 121 | else data_value) 122 | val = self._add_filtered_data(data.get(data_key, None), 123 | correct_value) 124 | if val is not None: 125 | if value is None: 126 | value = {} 127 | value[data_key] = val 128 | 129 | elif isinstance(data, (list, tuple)): 130 | for item in data: 131 | val = self._add_filtered_data(item, structured_data[0]) 132 | if val is not None: 133 | if value is None: 134 | value = [] 135 | value.append(val) 136 | else: 137 | value = structured_data if structured_data else data 138 | return value 139 | 140 | def _check_json_field(self, data, structure, key='', messages=None): 141 | # recursive function, validates request_data by request_fields 142 | value = None 143 | if messages is None: 144 | messages = [] 145 | if isinstance(structure, fields.JSONField): 146 | value, msg = structure.validate(key, data) 147 | messages += msg 148 | elif isinstance(structure, dict): 149 | if not isinstance(data, dict): 150 | messages.append('{} should be a dict instance'.format(key)) 151 | else: 152 | for sub_key, sub_structure in structure.items(): 153 | val, msg = self._check_json_field(data.get(sub_key, None), 154 | sub_structure, sub_key) 155 | if val is not None: 156 | if value is None: 157 | value = {} 158 | value[sub_key] = val 159 | else: 160 | messages += msg 161 | elif isinstance(structure, (list, tuple)): 162 | if not isinstance(data, (list, tuple)): 163 | messages.append( 164 | '{} should be a list or a tuple instance'.format(key)) 165 | else: 166 | for item in data: 167 | val, msg = self._check_json_field(item, structure[0], key) 168 | if val is not None: 169 | if value is None: 170 | value = [] 171 | value.append(val) 172 | else: 173 | messages += msg 174 | return value, messages 175 | 176 | def _set_request_data(self, request): 177 | request_data, messages = None, [] 178 | method = request.method 179 | if method in self._allowed_methods(): 180 | try: 181 | if method == 'GET': 182 | request_data = json.loads( 183 | json.dumps(request.GET)) if request.GET else {} 184 | elif method in ('POST', 'PUT', 'PATCH'): 185 | request_data = json.loads( 186 | request.body.decode('utf-8')) if request.body else {} 187 | elif method in ('OPTIONS', 'HEAD'): 188 | request_data = {} 189 | if not isinstance(request_data, (dict, list)): 190 | raise JSONDecodeError 191 | except JSONDecodeError: 192 | messages.append('Request data is not json serializable') 193 | return request_data, messages 194 | 195 | def custom_validation(self, structured_data): 196 | # Override this method for custom validation 197 | # Only CustomValidationException should be raised here 198 | assert self # just to avoid warning about making this staticmethod 199 | return structured_data or {} 200 | 201 | def dispatch(self, request, *args, **kwargs): 202 | self.request_data, messages = self._set_request_data(request) 203 | resp, response_status = [], None 204 | if not messages: 205 | user, messages = self.auth.authenticate(request) 206 | if not messages and user: 207 | request.user = user 208 | try: 209 | self.request_data = self._data_validate( 210 | request.method, self.request_data, self.request_fields, 211 | RequestStructureException, 212 | 'request data structure is not valid, ' 213 | 'check for documentation', 214 | 'request' 215 | ) 216 | except RequestStructureException as err: 217 | messages = err.messages 218 | response_status = err.response_status 219 | method_name = request.method.lower() 220 | if not messages: 221 | if method_name in self.http_method_names: 222 | handler = getattr(self, method_name, 223 | self.http_method_not_allowed) 224 | else: 225 | handler = self.http_method_not_allowed 226 | 227 | # TODO refactor this somehow 228 | # if method_name in ('options',): 229 | # _response = handler(request, *args, **kwargs) 230 | # else: 231 | resp, response_status = \ 232 | self.try_response(handler, request, *args, **kwargs) 233 | if not resp: 234 | resp = self.set_response_structure( 235 | data=None, success=False, message=messages) 236 | response_status = HTTP_400_BAD_REQUEST 237 | resp = self._set_response((resp, response_status)) 238 | return resp 239 | 240 | @staticmethod 241 | def _set_cors(result): 242 | cors_access = rester_settings.get('CORS_ACCESS') 243 | result[ 244 | 'Access-Control-Allow-Headers'] = 'Access-Control-Allow-Origin, ' \ 245 | 'Content-Type, Authorization' 246 | # TODO: multiple domains in CORS_ACCESS 247 | """ 248 | Sounds like the recommended way to do it is to have your server read 249 | the Origin header from the client, compare that to the list of domains 250 | you would like to allow, and if it matches, echo the value of the 251 | Origin header back to the client as the Access-Control-Allow-Origin 252 | header in the response. 253 | """ 254 | if isinstance(cors_access, str): 255 | result['Access-Control-Allow-Origin'] = cors_access 256 | elif cors_access is True: 257 | result['Access-Control-Allow-Origin'] = '*' 258 | else: 259 | result['Access-Control-Allow-Headers'] = \ 260 | 'Content-Type, Authorization' 261 | return result 262 | 263 | def options(self, request, *args, **kwargs): 264 | result = super().options(request, *args, **kwargs) 265 | result = self._set_cors(result) 266 | return result 267 | 268 | def try_response(self, handler, request, *args, **kwargs): 269 | message = [] 270 | success = None 271 | data = None 272 | logger.debug('Request: [{} {}] {}'.format(request.method, request.path, 273 | self.request_data)) 274 | try: 275 | success = True 276 | data = handler(request, *args, **kwargs) 277 | if isinstance(data, HttpResponse): 278 | response_status = data.status_code 279 | else: 280 | response_status = HTTP_200_OK 281 | if isinstance(data, tuple) and len(data) == 2: 282 | response_status = data[1] 283 | data = data[0] 284 | data = self._data_validate( 285 | request.method, data, self.response_fields, 286 | ResponseStructureException, 287 | 'response data structure is not valid, ' 288 | 'check for documentation or leave blank', 289 | 'response' 290 | ) 291 | except (ResponseStructureException, ResponseError) as err: 292 | logger.exception('Error in handler for [{}]'.format(request.path)) 293 | response_status = err.response_status 294 | if isinstance(err, (ResponseBadRequestMsgList, 295 | ResponseStructureException)): 296 | response_status = err.response_status 297 | message = err.messages 298 | elif isinstance(err, (ResponseOkMessage, ResponseFailMessage)): 299 | response_status = err.response_status 300 | message = err.message 301 | data = err.data 302 | else: 303 | message = '{}'.format(err) 304 | success = isinstance(err, ResponseOkMessage) 305 | except Exception as err: 306 | logger.exception('Error in handler for [{}]'.format(request.path)) 307 | message = '{}'.format(err) 308 | response_status = HTTP_500_INTERNAL_SERVER_ERROR 309 | if success is not None: 310 | success = 200 <= response_status <= 299 311 | # message = message and [{"response": message}] or [] 312 | _response = self.set_response_structure(data, success, message) 313 | logger.debug('Response: [{}] {}'.format(response_status, _response)) 314 | return _response, response_status 315 | 316 | @staticmethod 317 | def set_response_structure(data=None, success=True, message=None): 318 | if isinstance(data, HttpResponse): 319 | _response = data 320 | else: 321 | response_structure = rester_settings.get('RESPONSE_STRUCTURE', {}) 322 | if response_structure: 323 | message = message and message or [] 324 | res_data = {'success': success, 325 | 'message': message, 326 | 'data': data, 327 | } 328 | str_data = dict(response_structure) 329 | for item in str_data.keys(): 330 | if str_data[item] in res_data: 331 | str_data[item] = res_data[item] 332 | _response = str_data 333 | else: 334 | _response = data 335 | return _response 336 | 337 | 338 | class Login(BaseAPIView): 339 | request_fields = { 340 | "POST": { 341 | BaseAPIView.get_login_field(): fields.String(required=True), 342 | "password": fields.String(required=True) 343 | } 344 | } 345 | response_fields = {"POST": {"token": fields.String(required=True)}} 346 | 347 | def post(self, request): 348 | data, status = self.auth.login(request, self.request_data) 349 | return data, status 350 | 351 | 352 | class Logout(BaseAPIView): 353 | @permissions(IsAuthenticated) 354 | def get(self, request): 355 | data, status = self.auth.logout(request, self.request_data) 356 | return data, status 357 | -------------------------------------------------------------------------------- /django_rester_demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | .idea/ 103 | -------------------------------------------------------------------------------- /django_rester_demo/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexycore/django-rester/4959f8c0094cf847c29f0aba68ec730a9d68f761/django_rester_demo/api/__init__.py -------------------------------------------------------------------------------- /django_rester_demo/api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin -------------------------------------------------------------------------------- /django_rester_demo/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DemoConfig(AppConfig): 5 | name = 'api' 6 | -------------------------------------------------------------------------------- /django_rester_demo/api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexycore/django-rester/4959f8c0094cf847c29f0aba68ec730a9d68f761/django_rester_demo/api/migrations/__init__.py -------------------------------------------------------------------------------- /django_rester_demo/api/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | -------------------------------------------------------------------------------- /django_rester_demo/api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | -------------------------------------------------------------------------------- /django_rester_demo/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, re_path 2 | from django_rester.views import Login, Logout 3 | from .views import TestView 4 | 5 | urls_accounts = [ 6 | re_path('login/?', Login.as_view(), name='login'), 7 | re_path('logout/?', Logout.as_view(), name='logout'), 8 | ] 9 | 10 | urlpatterns = [ 11 | re_path('account/?', include(urls_accounts), name='accounts'), 12 | re_path('test/?', TestView.as_view(), name='test'), 13 | ] 14 | -------------------------------------------------------------------------------- /django_rester_demo/api/views.py: -------------------------------------------------------------------------------- 1 | from django_rester.status import HTTP_200_OK 2 | from django_rester.views import BaseAPIView 3 | from django_rester import fields 4 | 5 | 6 | class TestView(BaseAPIView): 7 | request_fields = { 8 | "POST": { 9 | "id": fields.Int(required=True), 10 | "title": fields.String(required=False, default='some_title'), 11 | }, 12 | "GET": { 13 | "id": fields.Int(required=False, default=0), 14 | "title": fields.String(required=False, default='some_title'), 15 | } 16 | } 17 | 18 | response_fields = { 19 | "POST": { 20 | "id": fields.Int(required=True), 21 | "title": fields.String(required=True, default='some_title'), 22 | "fk": [{"id": fields.Int(required=True)}] 23 | }, 24 | "GET": { 25 | "id": fields.Int(required=False), 26 | "title": fields.String(required=True, default='some_title'), 27 | } 28 | } 29 | 30 | def get(self, request, *args, **kwargs): 31 | return self.request_data, HTTP_200_OK 32 | 33 | def post(self, request, *args, **kwargs): 34 | response = dict(self.request_data) 35 | response.update({ 36 | "id": "1", 37 | "jk": 'kj', 38 | "title": "kljkhjkj", 39 | "lkjljl": 657, 40 | "fk": [{"id": 233, "asd": 222}] 41 | }) 42 | return response, HTTP_200_OK 43 | -------------------------------------------------------------------------------- /django_rester_demo/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexycore/django-rester/4959f8c0094cf847c29f0aba68ec730a9d68f761/django_rester_demo/db.sqlite3 -------------------------------------------------------------------------------- /django_rester_demo/django_rester_demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexycore/django-rester/4959f8c0094cf847c29f0aba68ec730a9d68f761/django_rester_demo/django_rester_demo/__init__.py -------------------------------------------------------------------------------- /django_rester_demo/django_rester_demo/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | SECRET_KEY = 'c(bter2&eo9^v(sd6&ybs&!^$51ta!kw0kt1a4-%)o03!eft@r' 6 | 7 | DEBUG = True 8 | 9 | ALLOWED_HOSTS = ['*'] 10 | 11 | INSTALLED_APPS = [ 12 | 'django.contrib.admin', 13 | 'django.contrib.auth', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.sessions', 16 | 'django.contrib.messages', 17 | 'django.contrib.staticfiles', 18 | 'api', 19 | ] 20 | 21 | MIDDLEWARE = [ 22 | 'django.middleware.security.SecurityMiddleware', 23 | 'django.contrib.sessions.middleware.SessionMiddleware', 24 | 'django.middleware.common.CommonMiddleware', 25 | 'django.middleware.csrf.CsrfViewMiddleware', 26 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 27 | 'django.contrib.messages.middleware.MessageMiddleware', 28 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 29 | ] 30 | 31 | ROOT_URLCONF = 'django_rester_demo.urls' 32 | 33 | TEMPLATES = [ 34 | { 35 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 36 | 'DIRS': [], 37 | 'APP_DIRS': True, 38 | 'OPTIONS': { 39 | 'context_processors': [ 40 | 'django.template.context_processors.debug', 41 | 'django.template.context_processors.request', 42 | 'django.contrib.auth.context_processors.auth', 43 | 'django.contrib.messages.context_processors.messages', 44 | ], 45 | }, 46 | }, 47 | ] 48 | 49 | WSGI_APPLICATION = 'django_rester_demo.wsgi.application' 50 | 51 | DATABASES = { 52 | 'default': { 53 | 'ENGINE': 'django.db.backends.sqlite3', 54 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 55 | } 56 | } 57 | 58 | AUTH_PASSWORD_VALIDATORS = [ 59 | { 60 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 61 | }, 62 | { 63 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 64 | }, 65 | { 66 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 67 | }, 68 | { 69 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 70 | }, 71 | ] 72 | 73 | LANGUAGE_CODE = 'en-us' 74 | 75 | TIME_ZONE = 'UTC' 76 | 77 | USE_I18N = True 78 | 79 | USE_L10N = True 80 | 81 | USE_TZ = True 82 | 83 | STATIC_URL = '/static/' 84 | 85 | APPEND_SLASH = True 86 | 87 | DJANGO_RESTER = { 88 | 'RESPONSE_STRUCTURE': True, 89 | 'CORS_ACCESS': True, 90 | 'SOFT_RESPONSE_VALIDATION': True, 91 | } 92 | 93 | DJANGO_RESTER_JWT = { 94 | 'SECRET': 'SDgwFbertb245wg', 95 | } 96 | -------------------------------------------------------------------------------- /django_rester_demo/django_rester_demo/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include, re_path 3 | 4 | urlpatterns = [ 5 | re_path('admin/?', admin.site.urls), 6 | path('api/', include("api.urls"), name='api'), 7 | ] 8 | -------------------------------------------------------------------------------- /django_rester_demo/django_rester_demo/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_rester_demo.settings") 6 | 7 | application = get_wsgi_application() 8 | -------------------------------------------------------------------------------- /django_rester_demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_rester_demo.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | try: 11 | import django 12 | except ImportError: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) 18 | raise 19 | execute_from_command_line(sys.argv) 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | 5 | from setuptools import setup 6 | from django_rester import config 7 | 8 | build_number = os.getenv('TRAVIS_BUILD_NUMBER', '') 9 | branch = os.getenv('TRAVIS_BRANCH', '') 10 | travis = any((build_number, branch,)) 11 | version = config.__version__.split('.') 12 | develop_status = '4 - Beta' 13 | url = 'http://lexycore.github.io/django-rester' 14 | long_description = '' 15 | 16 | if travis: 17 | version = version[0:3] 18 | if branch == 'master': 19 | develop_status = '5 - Production/Stable' 20 | version.append(build_number) 21 | else: 22 | version.append('{}{}'.format('dev' if branch == 'develop' else branch, build_number)) 23 | else: 24 | if len(version) < 4: 25 | version.append('local') 26 | 27 | version = '.'.join(version) 28 | if travis: 29 | with open('django_rester/config.py', 'w', encoding="utf-8") as f: 30 | f.write("__version__ = '{}'".format(version)) 31 | 32 | if os.path.isfile('README.md'): 33 | try: 34 | import pypandoc 35 | 36 | print("Converting README...") 37 | long_description = pypandoc.convert('README.md', 'rst') 38 | if branch: 39 | long_description = long_description.replace('django-rester.svg?branch=master', 'django-rester.svg?branch={}'.format(branch)) 40 | 41 | except (IOError, ImportError, OSError): 42 | print("Pandoc not found. Long_description conversion failure.") 43 | with open('README.md', encoding="utf-8") as f: 44 | long_description = f.read() 45 | else: 46 | print("Saving README.rst...") 47 | try: 48 | if len(long_description) > 0: 49 | with open('README.rst', 'w', encoding="utf-8") as f: 50 | f.write(long_description) 51 | if travis: 52 | os.remove('README.md') 53 | except: 54 | print(" failed!") 55 | 56 | setup( 57 | name='django-rester', 58 | version=version, 59 | description='Django REST API build helper', 60 | license='MIT', 61 | author='Sergei Kovalev', 62 | author_email='zili.tnd@gmail.com', 63 | url=url, 64 | long_description=long_description, 65 | download_url='https://github.com/lexycore/django-rester.git', 66 | classifiers=[ 67 | 'Development Status :: {}'.format(develop_status), 68 | 'Environment :: Console', 69 | 'Intended Audience :: Developers', 70 | 'Topic :: Software Development :: Build Tools', 71 | 'License :: OSI Approved :: MIT License', 72 | 'Natural Language :: English', 73 | 'Programming Language :: Python :: 3', 74 | 'Programming Language :: Python :: 3.1', 75 | 'Programming Language :: Python :: 3.2', 76 | 'Programming Language :: Python :: 3.3', 77 | 'Programming Language :: Python :: 3.4', 78 | 'Programming Language :: Python :: 3.5', 79 | 'Programming Language :: Python :: 3.6', 80 | ], 81 | keywords=[ 82 | 'development', 83 | 'API', 84 | ], 85 | packages=[ 86 | 'django_rester', 87 | 'django_rester.rester_jwt', 88 | ], 89 | setup_requires=[ 90 | 'wheel', 91 | 'pypandoc', 92 | ], 93 | tests_require=[ 94 | 'pytest', 95 | ], 96 | install_requires=[ 97 | 'django', 98 | 'pyjwt', 99 | 'django-rediser', 100 | ], 101 | package_data={ 102 | '': [ 103 | '../LICENSE', 104 | ], 105 | }, 106 | ) --------------------------------------------------------------------------------