From 0567ad318de343bdf834e4593e5e844cf035080b Mon Sep 17 00:00:00 2001 From: miraculixx Date: Sun, 8 Nov 2015 01:08:06 +0100 Subject: [PATCH 01/14] import api module lazily previously api module had to be loaded already which is slow on startup. now the module is loaded when the resource is accessed. --- tastypie_swagger/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tastypie_swagger/views.py b/tastypie_swagger/views.py index 4da61ec..fe143dd 100644 --- a/tastypie_swagger/views.py +++ b/tastypie_swagger/views.py @@ -9,6 +9,7 @@ import tastypie from .mapping import ResourceSwaggerMapping +from importlib import import_module class TastypieApiMixin(object): @@ -33,7 +34,7 @@ def tastypie_api(self): else: path, attr = tastypie_api_module.rsplit('.', 1) try: - tastypie_api = getattr(sys.modules[path], attr, None) + tastypie_api = getattr(import_module(path), attr, None) except KeyError: raise ImproperlyConfigured("%s is not a valid python path" % path) if not tastypie_api: From 1c7c1bedc9ade4cdaef66d25f3ae9ef06de1bf84 Mon Sep 17 00:00:00 2001 From: miraculixx Date: Mon, 10 Oct 2016 20:00:18 +0200 Subject: [PATCH 02/14] add swagger V2 compliant swaggger.json output --- CONTRIBUTORS.md | 3 +- README.md | 16 ++++ requirements.txt | 4 + tastypie_swagger/mapping2.py | 142 ++++++++++++++++++++++++++++ tastypie_swagger/urls.py | 5 +- tastypie_swagger/views.py | 175 ++++++++++++++++++++++++++++++++--- 6 files changed, 330 insertions(+), 15 deletions(-) create mode 100644 tastypie_swagger/mapping2.py diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 4404997..2d74811 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -4,4 +4,5 @@ [Jonathan Liuti](https://github.com/johnraz) [David Wolever](david@wolever.net) [Joshua Kehn](https://github.com/joshkehn) -[David Miller](david@deadpansincerity.com) \ No newline at end of file +[David Miller](david@deadpansincerity.com) +[Patrick Senti](https://github.com/miraculixx) \ No newline at end of file diff --git a/README.md b/README.md index 6f50ad4..7e59bcb 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,22 @@ To declare more than one endpoint, repeat the above URL definition and change th Swagger documentation will be served up at the URL(s) you configured. +### Swagger V1 v.s. V2 + +The following URI are according to Swagger Spec 1.2: + + * `/resources/` + * `/schema/` + * `/schema/` + +The Swagger Spec V2.0 compliant `swagger.json` is served at the following URLs. +Both URLs return the same content. + + * `/specs/` + * `/specs/swagger.json` + +Note that the V2 specs are generated by mapping the V1 output to V2. This +ensures that existing Tastypie Apis continue to work unmodified. ## Contributors diff --git a/requirements.txt b/requirements.txt index 5a2bb2b..245ced9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,5 @@ wsgiref==0.1.2 +Django==1.7.11 +argparse==1.2.1 +django-tastypie==0.13.3 +-e . diff --git a/tastypie_swagger/mapping2.py b/tastypie_swagger/mapping2.py new file mode 100644 index 0000000..e3f6003 --- /dev/null +++ b/tastypie_swagger/mapping2.py @@ -0,0 +1,142 @@ +import logging +from os.path import commonprefix +from tastypie_swagger.mapping import ResourceSwaggerMapping +logger = logging.getLogger(__name__) +# Ignored POST fields +IGNORED_FIELDS = ['id', ] + + +# Enable all basic ORM filters but do not allow filtering across relationships. +ALL = 1 +# Enable all ORM filters, including across relationships +ALL_WITH_RELATIONS = 2 + +SWAGGER_V2_TYPE_MAP = { + 'List': 'array', + 'int': 'integer', + 'bool': 'boolean', +} + + +class ResourceSwagger2Mapping(ResourceSwaggerMapping): + + """ + build the 'paths' and 'definitions' entries in a swagger V2 spec + + This uses the original ResourceSwaggerMapping for swagger V1 specs + and maps its output to Swagger V2. This ensures we can produce both + valid V1 and V2 specs. + + Usage: + resource = tastypie.Resource instance + mapping = ResourceSwagger2Mapping(resource) + apis, defs = mapping.build_apis() + specs['paths'].update(apis) + specs['definitions'].update(defs) + """ + + def build_apis(self): + apis = [self.build_list_api(), self.build_detail_api()] + apis.extend(self.build_extra_apis()) + # build the swagger v2 specs + paths = {} + defs = {} + models = self.build_models() + common_path = apis[0].get('path').replace(self.resource_name, '') + common_path = common_path.replace('//', '/') + for api in apis: + uri = api.get('path').replace(common_path, '/') + path = paths[uri] = {} + for op in api.get('operations'): + responseCls = op.get('responseClass') + method = op.get('httpMethod').lower() + path[method] = { + "summary": op.get('summary'), + "responses": { + "200": { + "description": "%s object" % responseCls, + "schema": { + "$ref": self.get_model_ref(responseCls), + } + } + } + } + op_params = self.map_parameters( + method, uri, op.get('parameters'), models) + path[method]['parameters'] = op_params + for name, model in models.iteritems(): + model.pop('id') + self.map_properties(model, models) + defs[self.get_model_ref_name(name)] = model + return common_path, paths, defs + + def map_parameters(self, method, path, in_params, models): + """ + return "parameters" dictionary + """ + params = [] + for in_p in in_params: + # default "in" is query or body + if method == 'get': + param_in = 'query' + else: + param_in = 'body' + # check if the parameter name is in path as {name} + name = in_p.get('name') + if '{%s}' % name.strip() in path: + param_in = 'path' + param = { + 'name': name, + 'in': param_in, + 'required': in_p.get('required'), + } + kind = in_p.get('dataType') + if method != 'get' and kind in models: + param['schema'] = { + "$ref": self.get_model_ref(kind), + } + else: + param['type'] = SWAGGER_V2_TYPE_MAP.get(kind, kind) + params.append(param) + return params + + def map_properties(self, model, models): + """ + recursively map a model's properties to 'definitions' syntax + + This will create entries for 'definitions'. Types in their + own right are mapped using $ref references. + """ + props = model.get('properties') + def recurse(prop): + if isinstance(prop, dict): + kind = prop.get('type') + if kind in models: + prop['type'] = 'object' + prop['$ref'] = self.get_model_ref(kind) + elif kind: + prop['type'] = SWAGGER_V2_TYPE_MAP.get(kind, kind) + ref = prop.get('$ref') + if ref is not None and not ref.startswith('#'): + prop['$ref'] = self.get_model_ref(ref) + for key, subprop in prop.iteritems(): + recurse(subprop) + recurse(props) + + def get_model_ref_name(self, name): + """ + return unique ref name for definitions + + This is required because the Swagger V1 specs were on a per-resource + level, whereas the Swagger V2 specs are for multiple resources. + """ + if name in ['ListView', 'Objects', 'Meta']: + name = '%s_%s' % (self.resource_name.replace('/', '_'), + name) + return name + + def get_model_ref(self, name): + """ + return the $ref path for the given model name + """ + return "#/definitions/%s" % self.get_model_ref_name(name) diff --git a/tastypie_swagger/urls.py b/tastypie_swagger/urls.py index 2da230b..30511a5 100644 --- a/tastypie_swagger/urls.py +++ b/tastypie_swagger/urls.py @@ -1,13 +1,16 @@ + +from .views import SwaggerView, ResourcesView, SchemaView +from tastypie_swagger.views import SwaggerSpecs2View try: from django.conf.urls import patterns, include, url except ImportError: from django.conf.urls.defaults import patterns, include, url -from .views import SwaggerView, ResourcesView, SchemaView urlpatterns = patterns('', url(r'^$', SwaggerView.as_view(), name='index'), url(r'^resources/$', ResourcesView.as_view(), name='resources'), + url(r'^specs/(swagger.json)?$', SwaggerSpecs2View.as_view(), name='specs'), url(r'^schema/(?P\S+)/$', SchemaView.as_view()), url(r'^schema/$', SchemaView.as_view(), name='schema'), ) diff --git a/tastypie_swagger/views.py b/tastypie_swagger/views.py index fe143dd..4ae7ef3 100644 --- a/tastypie_swagger/views.py +++ b/tastypie_swagger/views.py @@ -1,18 +1,19 @@ -import sys +from importlib import import_module import json +import sys +from tastypie_swagger.mapping import ResourceSwaggerMapping -from django.views.generic import TemplateView -from django.http import HttpResponse, Http404 from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse - +from django.http import HttpResponse, Http404 +from django.views.generic import TemplateView import tastypie -from .mapping import ResourceSwaggerMapping -from importlib import import_module +from tastypie_swagger.mapping2 import ResourceSwagger2Mapping class TastypieApiMixin(object): + """ Provides views with a 'tastypie_api' attr representing a tastypie.api.Api instance @@ -27,7 +28,8 @@ def tastypie_api(self): tastypie_api_module = self.kwargs.get('tastypie_api_module', None) if not tastypie_api_module: - raise ImproperlyConfigured("tastypie_api_module must be defined as an extra parameters in urls.py with its value being a path to a tastypie.api.Api instance.") + raise ImproperlyConfigured( + "tastypie_api_module must be defined as an extra parameters in urls.py with its value being a path to a tastypie.api.Api instance.") if isinstance(tastypie_api_module, tastypie.api.Api): tastypie_api = tastypie_api_module @@ -36,9 +38,11 @@ def tastypie_api(self): try: tastypie_api = getattr(import_module(path), attr, None) except KeyError: - raise ImproperlyConfigured("%s is not a valid python path" % path) + raise ImproperlyConfigured( + "%s is not a valid python path" % path) if not tastypie_api: - raise ImproperlyConfigured("%s is not a valid tastypie.api.Api instance" % tastypie_api_module) + raise ImproperlyConfigured( + "%s is not a valid tastypie.api.Api instance" % tastypie_api_module) self._tastypie_api = tastypie_api @@ -46,12 +50,14 @@ def tastypie_api(self): class SwaggerApiDataMixin(object): + """ Provides required API context data """ def get_context_data(self, *args, **kwargs): - context = super(SwaggerApiDataMixin, self).get_context_data(*args, **kwargs) + context = super(SwaggerApiDataMixin, self).get_context_data( + *args, **kwargs) context.update({ 'apiVersion': self.kwargs.get('version', 'Unknown'), 'swaggerVersion': '1.2', @@ -60,6 +66,7 @@ def get_context_data(self, *args, **kwargs): class JSONView(TemplateView): + """ Simple JSON rendering """ @@ -70,7 +77,8 @@ def render_to_response(self, context, **response_kwargs): Returns a response with a template rendered with the given context. """ - # This cannot be serialized if it is a api instance and we don't need it anyway. + # This cannot be serialized if it is a api instance and we don't need + # it anyway. context.pop('tastypie_api_module', None) for k in ['params', 'view']: @@ -85,6 +93,7 @@ def render_to_response(self, context, **response_kwargs): class SwaggerView(TastypieApiMixin, TemplateView): + """ Display the swagger-ui page """ @@ -93,11 +102,13 @@ class SwaggerView(TastypieApiMixin, TemplateView): def get_context_data(self, **kwargs): context = super(SwaggerView, self).get_context_data(**kwargs) - context['discovery_url'] = reverse('%s:resources' % self.kwargs.get('namespace')) + context['discovery_url'] = reverse( + '%s:resources' % self.kwargs.get('namespace')) return context class ResourcesView(TastypieApiMixin, SwaggerApiDataMixin, JSONView): + """ Provide a top-level resource listing for swagger @@ -108,7 +119,8 @@ def get_context_data(self, *args, **kwargs): context = super(ResourcesView, self).get_context_data(*args, **kwargs) # Construct schema endpoints from resources - apis = [{'path': '/%s' % name} for name in sorted(self.tastypie_api._registry.keys())] + apis = [{'path': '/%s' % name} + for name in sorted(self.tastypie_api._registry.keys())] context.update({ 'basePath': self.request.build_absolute_uri(reverse('%s:schema' % self.kwargs.get('namespace'))).rstrip('/'), 'apis': apis, @@ -116,7 +128,144 @@ def get_context_data(self, *args, **kwargs): return context +class Schema2View(TastypieApiMixin, SwaggerApiDataMixin, JSONView): + + """ + Provide an individual resource schema for swagger + + This JSON must conform to http://swagger.io/specification/ + at Version 2.0 + + For testing see example/demo.tests, which validates a default ModelResource + to conform to this specification + """ + + def get_context_data(self, *args, **kwargs): + # Verify matching tastypie resource exists + resource_name = kwargs.get('resource', None) + if not resource_name in self.tastypie_api._registry: + raise Http404 + + # Generate mapping from tastypie.resources.Resource.build_schema + resource = self.tastypie_api._registry.get(resource_name) + mapping = ResourceSwagger2Mapping(resource) + + context = super(SchemaView, self).get_context_data(*args, **kwargs) + context.update({ + 'basePath': '/', + 'apis': mapping.build_apis(), + 'models': mapping.build_models(), + 'resourcePath': '/{0}'.format(resource._meta.resource_name) + }) + return context + + +class SwaggerSpecs2View(TastypieApiMixin, JSONView): + + """ + Provide a top-level resource listing for swagger + + This JSON must conform to https://github.com/wordnik/swagger-core/wiki/Resource-Listing + + Usage: + url(r'^api/doc/', include('tastypie_swagger.urls', + namespace='demo_api_swagger'), + kwargs={ + "tastypie_api_module":"demo.apis.api", + "namespace":"demo_api_swagger", + "version": "0.1"} + ), + + This sets up the api/doc/specs/swagger.json URI (along with the V1 URIs) + to return Swagger V2 compliant JSON. + + Note that your Api instance may contain several attributes that are + processed by SwaggerSpecs2View: + + api.title - string, defaults to api_name + api.description - string, defaults to api_name + api.version - string, defaults to '1.0' + api.basePath - string of common base URI. if not provided defaults to the + api's first Resource base path + + In addition you may override any of the 'info' attributes in the + specification by adding an api.meta dict, e.g. + + api.info = { + 'title' : 'some title', + 'license': { + 'name': 'commerical', + 'URL': 'http://example.com/api/license.html', + } + } + + Note that 'path' and 'definitions' values of the output JSON are produced + by mapping the Swagger V1 output to V2 syntax. Thus any V2 specifics are + not yet supported. + """ + + def get_resource(self, context, resource_name): + # Verify matching tastypie resource exists + if not resource_name in self.tastypie_api._registry: + raise Http404 + # Generate mapping from tastypie.resources.Resource.build_schema + resource = self.tastypie_api._registry.get(resource_name) + mapping = ResourceSwagger2Mapping(resource) + basePath, apis, defs = mapping.build_apis() + if context.get('basePath') is None: + context['basePath'] = basePath + context['paths'].update(apis) + context['definitions'].update(defs) + return context + + def get_context_data(self, *args, **kwargs): + context = super(SwaggerSpecs2View, self).get_context_data( + *args, **kwargs) + + # build meta specs + title = getattr(self.tastypie_api, 'title', self.tastypie_api.api_name) + descr = getattr( + self.tastypie_api, 'description', self.tastypie_api.api_name) + version = getattr( + self.tastypie_api, 'version', "1.0") + basePath = getattr(self.tastypie_api, 'basePath', None) + # create output + context.update({ + "swagger": "2.0", + "info": { + "title": title, + "description": descr, + "version": version, + }, + "host": self.request.get_host(), + "schemes": [ + "https" if self.request.is_secure() else 'http', + ], + "basePath": basePath, + "produces": [ + "application/json" + ], + "paths": { + }, + "definitions": { + }, + }) + + # support meta specifications in Api resource + api_info = getattr(self.tastypie_api, 'info', False) + if api_info: + context['info'].update(api_info) + + # remove invalid attributes + context.pop('namespace') + context.pop('version') + for name in sorted(self.tastypie_api._registry.keys()): + self.get_resource(context, name) + return context + + class SchemaView(TastypieApiMixin, SwaggerApiDataMixin, JSONView): + """ Provide an individual resource schema for swagger From e6d873ccfcda48e76bdf1e051813fc8af77d0fd6 Mon Sep 17 00:00:00 2001 From: miraculixx Date: Mon, 10 Oct 2016 20:02:30 +0200 Subject: [PATCH 03/14] add django add for testing purpose --- README.md | 12 +++ example/demo/__init__.py | 0 example/demo/apis.py | 28 ++++++ example/demo/migrations/0001_initial.py | 23 +++++ .../migrations/0002_auto_20161010_1722.py | 30 +++++++ example/demo/migrations/__init__.py | 0 example/demo/models.py | 12 +++ example/demo/tests.py | 22 +++++ example/example/__init__.py | 0 example/example/settings.py | 86 +++++++++++++++++++ example/example/urls.py | 15 ++++ example/example/wsgi.py | 14 +++ example/manage.py | 10 +++ 13 files changed, 252 insertions(+) create mode 100644 example/demo/__init__.py create mode 100644 example/demo/apis.py create mode 100644 example/demo/migrations/0001_initial.py create mode 100644 example/demo/migrations/0002_auto_20161010_1722.py create mode 100644 example/demo/migrations/__init__.py create mode 100644 example/demo/models.py create mode 100644 example/demo/tests.py create mode 100644 example/example/__init__.py create mode 100644 example/example/settings.py create mode 100644 example/example/urls.py create mode 100644 example/example/wsgi.py create mode 100755 example/manage.py diff --git a/README.md b/README.md index 7e59bcb..07d3a95 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,18 @@ Both URLs return the same content. Note that the V2 specs are generated by mapping the V1 output to V2. This ensures that existing Tastypie Apis continue to work unmodified. +### Testing + +The `example` folder contains a Django app to test tastypie-swagger. Run tests +as follows: + +```python +$ cd /path/to/tastypie_swagger +$ pip install -r requirements.txt +$ cd example +$ manage.py test +``` + ## Contributors Contributors to this project are listed in the CONTRIBUTORS.md file. If you contribute to this project, please add your name to the file. diff --git a/example/demo/__init__.py b/example/demo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/demo/apis.py b/example/demo/apis.py new file mode 100644 index 0000000..8ffc3d1 --- /dev/null +++ b/example/demo/apis.py @@ -0,0 +1,28 @@ +from tastypie.api import Api +from tastypie.resources import ModelResource + +from demo.models import FooModel, BarModel + + +class FooResource(ModelResource): + + class Meta: + queryset = FooModel.objects.all() + + +class BarResource(ModelResource): + + class Meta: + queryset = BarModel.objects.all() + +api = Api('v1') +api.title = 'demo API' +api.description = 'lorem ipsum' +api.version = '1.9' +api.info = { + 'license': { + 'name': 'Apache', + 'url': 'http://www.apache.org/licenses/LICENSE-2.0', + }} +api.register(FooResource()) +api.register(BarResource()) diff --git a/example/demo/migrations/0001_initial.py b/example/demo/migrations/0001_initial.py new file mode 100644 index 0000000..5fdf7eb --- /dev/null +++ b/example/demo/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='FooModel', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('text', models.CharField(default=b'', max_length=100, null=True)), + ], + options={ + }, + bases=(models.Model,), + ), + ] diff --git a/example/demo/migrations/0002_auto_20161010_1722.py b/example/demo/migrations/0002_auto_20161010_1722.py new file mode 100644 index 0000000..548acab --- /dev/null +++ b/example/demo/migrations/0002_auto_20161010_1722.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('demo', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='BarModel', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('text', models.CharField(default=b'', max_length=100, null=True, help_text=b'Text for bar')), + ], + options={ + }, + bases=(models.Model,), + ), + migrations.AlterField( + model_name='foomodel', + name='text', + field=models.CharField(default=b'', max_length=100, null=True, help_text=b'Text for foo'), + preserve_default=True, + ), + ] diff --git a/example/demo/migrations/__init__.py b/example/demo/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/demo/models.py b/example/demo/models.py new file mode 100644 index 0000000..114a064 --- /dev/null +++ b/example/demo/models.py @@ -0,0 +1,12 @@ +from django.db import models +from django.db.models import fields + + +class FooModel(models.Model): + text = fields.CharField(max_length=100, default='', null=True, + help_text='Text for foo') + + +class BarModel(models.Model): + text = fields.CharField(max_length=100, default='', null=True, + help_text='Text for bar') diff --git a/example/demo/tests.py b/example/demo/tests.py new file mode 100644 index 0000000..db8a7f4 --- /dev/null +++ b/example/demo/tests.py @@ -0,0 +1,22 @@ +from django.test.testcases import TestCase +from swagger_spec_validator.util import get_validator +from tastypie.test import ResourceTestCaseMixin + + +class TestSpecs(ResourceTestCaseMixin, TestCase): + + def setUp(self): + super(TestSpecs, self).setUp() + + def tearDown(self): + super(TestSpecs, self).tearDown() + + def test_validate_specs(self): + for uri in ['/api/doc/specs/swagger.json', + '/api/doc/specs/']: + resp = self.client.get(uri, + format='json') + self.assertHttpOK(resp) + spec_json = self.deserialize(resp) + validator = get_validator(spec_json) + validator.validate_spec(spec_json) diff --git a/example/example/__init__.py b/example/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/example/settings.py b/example/example/settings.py new file mode 100644 index 0000000..37e5c27 --- /dev/null +++ b/example/example/settings.py @@ -0,0 +1,86 @@ +""" +Django settings for example project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.7/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '6cf)=%c1ts&4gf*!%bn%&!3$oywu)ak&r3i0ilmps5mrxu9h@v' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'demo', + 'tastypie', + 'tastypie_swagger', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'example.urls' + +WSGI_APPLICATION = 'example.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.7/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +# Internationalization +# https://docs.djangoproject.com/en/1.7/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.7/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/example/example/urls.py b/example/example/urls.py new file mode 100644 index 0000000..1cb1355 --- /dev/null +++ b/example/example/urls.py @@ -0,0 +1,15 @@ +from django.conf.urls import patterns, include, url +from django.contrib import admin +from demo.apis import api + +urlpatterns = patterns('', + url(r'^api/', include(api.urls)), + url(r'^api/doc/', include('tastypie_swagger.urls', + namespace='demo_api_swagger'), + kwargs={ + "tastypie_api_module":"demo.apis.api", + "namespace":"demo_api_swagger", + "version": "0.1"} + ), + url(r'^admin/', include(admin.site.urls)), +) diff --git a/example/example/wsgi.py b/example/example/wsgi.py new file mode 100644 index 0000000..f462885 --- /dev/null +++ b/example/example/wsgi.py @@ -0,0 +1,14 @@ +""" +WSGI config for example project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ +""" + +import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + +from django.core.wsgi import get_wsgi_application +application = get_wsgi_application() diff --git a/example/manage.py b/example/manage.py new file mode 100755 index 0000000..2605e37 --- /dev/null +++ b/example/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) From 257786b160536910979b0e64482bae650d33c6c9 Mon Sep 17 00:00:00 2001 From: miraculixx Date: Tue, 11 Oct 2016 11:57:40 +0200 Subject: [PATCH 04/14] add more data types, tags --- tastypie_swagger/mapping2.py | 37 ++++++++++++++++++++++++++++++------ tastypie_swagger/views.py | 6 ++++-- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/tastypie_swagger/mapping2.py b/tastypie_swagger/mapping2.py index e3f6003..f529d68 100644 --- a/tastypie_swagger/mapping2.py +++ b/tastypie_swagger/mapping2.py @@ -12,9 +12,13 @@ ALL_WITH_RELATIONS = 2 SWAGGER_V2_TYPE_MAP = { - 'List': 'array', - 'int': 'integer', - 'bool': 'boolean', + 'List': ('array', None), + 'int': ('integer', 'int32'), + 'bool': ('boolean', None), + 'related': ('string', None), + 'datetime': ('string', 'date-time'), + 'decimal': ('number', 'double'), + 'dict': ('object', None), } @@ -41,17 +45,27 @@ def build_apis(self): # build the swagger v2 specs paths = {} defs = {} + api_tags = [] + api_tagnames = [] models = self.build_models() common_path = apis[0].get('path').replace(self.resource_name, '') common_path = common_path.replace('//', '/') for api in apis: uri = api.get('path').replace(common_path, '/') + tag_name = uri.replace('/', '').split('{')[0] + tag = { + "name": tag_name, + } + if tag_name not in api_tagnames: + api_tagnames.append(tag_name) + api_tags.append(tag) path = paths[uri] = {} for op in api.get('operations'): responseCls = op.get('responseClass') method = op.get('httpMethod').lower() path[method] = { "summary": op.get('summary'), + "tags": [tag_name], "responses": { "200": { "description": "%s object" % responseCls, @@ -68,7 +82,7 @@ def build_apis(self): model.pop('id') self.map_properties(model, models) defs[self.get_model_ref_name(name)] = model - return common_path, paths, defs + return common_path, paths, defs, api_tags def map_parameters(self, method, path, in_params, models): """ @@ -96,7 +110,7 @@ def map_parameters(self, method, path, in_params, models): "$ref": self.get_model_ref(kind), } else: - param['type'] = SWAGGER_V2_TYPE_MAP.get(kind, kind) + param.update(self.get_swagger_type(kind)) params.append(param) return params @@ -115,7 +129,7 @@ def recurse(prop): prop['type'] = 'object' prop['$ref'] = self.get_model_ref(kind) elif kind: - prop['type'] = SWAGGER_V2_TYPE_MAP.get(kind, kind) + prop.update(self.get_swagger_type(kind)) ref = prop.get('$ref') if ref is not None and not ref.startswith('#'): prop['$ref'] = self.get_model_ref(ref) @@ -140,3 +154,14 @@ def get_model_ref(self, name): return the $ref path for the given model name """ return "#/definitions/%s" % self.get_model_ref_name(name) + + def get_swagger_type(self, kind): + """ return dict of type and format as applicable """ + try: + kind, format = SWAGGER_V2_TYPE_MAP[kind] + except KeyError: + format = None + d = dict(type=kind) + if format: + d.update(format=format) + return d diff --git a/tastypie_swagger/views.py b/tastypie_swagger/views.py index 4ae7ef3..f6288d3 100644 --- a/tastypie_swagger/views.py +++ b/tastypie_swagger/views.py @@ -135,7 +135,7 @@ class Schema2View(TastypieApiMixin, SwaggerApiDataMixin, JSONView): This JSON must conform to http://swagger.io/specification/ at Version 2.0 - + For testing see example/demo.tests, which validates a default ModelResource to conform to this specification """ @@ -211,11 +211,12 @@ def get_resource(self, context, resource_name): # Generate mapping from tastypie.resources.Resource.build_schema resource = self.tastypie_api._registry.get(resource_name) mapping = ResourceSwagger2Mapping(resource) - basePath, apis, defs = mapping.build_apis() + basePath, apis, defs, tags = mapping.build_apis() if context.get('basePath') is None: context['basePath'] = basePath context['paths'].update(apis) context['definitions'].update(defs) + context['tags'].extend(tags) return context def get_context_data(self, *args, **kwargs): @@ -245,6 +246,7 @@ def get_context_data(self, *args, **kwargs): "produces": [ "application/json" ], + "tags": [], "paths": { }, "definitions": { From 9acb26f6b30e8798c6183f0a3f0e675c5c565e38 Mon Sep 17 00:00:00 2001 From: miraculixx Date: Wed, 12 Oct 2016 14:22:46 +0200 Subject: [PATCH 05/14] add description, also better commented code --- tastypie_swagger/mapping2.py | 61 +++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/tastypie_swagger/mapping2.py b/tastypie_swagger/mapping2.py index f529d68..dda45af 100644 --- a/tastypie_swagger/mapping2.py +++ b/tastypie_swagger/mapping2.py @@ -1,16 +1,6 @@ -import logging -from os.path import commonprefix from tastypie_swagger.mapping import ResourceSwaggerMapping -logger = logging.getLogger(__name__) -# Ignored POST fields -IGNORED_FIELDS = ['id', ] -# Enable all basic ORM filters but do not allow filtering across relationships. -ALL = 1 -# Enable all ORM filters, including across relationships -ALL_WITH_RELATIONS = 2 - SWAGGER_V2_TYPE_MAP = { 'List': ('array', None), 'int': ('integer', 'int32'), @@ -29,14 +19,52 @@ class ResourceSwagger2Mapping(ResourceSwaggerMapping): This uses the original ResourceSwaggerMapping for swagger V1 specs and maps its output to Swagger V2. This ensures we can produce both - valid V1 and V2 specs. + valid V1 and V2 specs from the same tastypie.Resources. Usage: resource = tastypie.Resource instance mapping = ResourceSwagger2Mapping(resource) - apis, defs = mapping.build_apis() + common_path, paths, defs, tags = mapping.build_apis() specs['paths'].update(apis) specs['definitions'].update(defs) + + Parameters: + + resource - the tastypie.Resource instance + + Returns: + tuple of (common_path, paths, defs, tags) + + common_path - the common parts of the API's URIs, use as "basePath" + paths - the "path" dict of the specs + defs - the "definitions" dict of the specs + tags - the "tags" dict of the specs + + Notes: + * Any tastypie common dataTypes referenced in the V1 specs are mapped + as "$ref" and the data type is added to the defs dict. To ensure + uniqueness among multiple resources, the ListView, Object and Meta + datatypes are prefixed with the resource name + + * any parameter's "in" attribute for GET methods are set to "query", + all other methods GET/DELETE/PUT/PATCH get "body". + + * if the parameter name appears as "{}" in the operation's + path (e.g. parameter "id" in "/category/{id}"), the "in" attribute + is set to "path" (see map_parameters) + + * types are converted from swagger V1 according to SWAGGER_V2_TYPE_MAP + (see .get_swagger_type) + + * multi-level model properties are supported, however multi-level + parameters are not unless they are data types. + + Developers: + + IF YOU UPDATE THIS, BE SURE TO RUN validation tests in + + $ cd example + $ manage.py test demo """ def build_apis(self): @@ -50,6 +78,7 @@ def build_apis(self): models = self.build_models() common_path = apis[0].get('path').replace(self.resource_name, '') common_path = common_path.replace('//', '/') + # build tags, paths and operations for api in apis: uri = api.get('path').replace(common_path, '/') tag_name = uri.replace('/', '').split('{')[0] @@ -65,6 +94,9 @@ def build_apis(self): method = op.get('httpMethod').lower() path[method] = { "summary": op.get('summary'), + # -- note "tags" is optional, yet sphinx-swagger + # requires it + # (https://github.com/unaguil/sphinx-swagger/issues/5) "tags": [tag_name], "responses": { "200": { @@ -75,9 +107,14 @@ def build_apis(self): } } } + # add optional attributes + if self.resource.__doc__ is not None: + path[method].update(description=self.resource.__doc__) + # add parameters op_params = self.map_parameters( method, uri, op.get('parameters'), models) path[method]['parameters'] = op_params + # build definitions for name, model in models.iteritems(): model.pop('id') self.map_properties(model, models) From d4e2f6d3f4409f0372aad9595f4bc1d409d96e20 Mon Sep 17 00:00:00 2001 From: miraculixx Date: Sun, 10 Dec 2017 16:26:59 +0100 Subject: [PATCH 06/14] fix swagger.json for sagger 2.0 as used in redoc --- tastypie_swagger/mapping2.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/tastypie_swagger/mapping2.py b/tastypie_swagger/mapping2.py index dda45af..d035a5c 100644 --- a/tastypie_swagger/mapping2.py +++ b/tastypie_swagger/mapping2.py @@ -118,7 +118,10 @@ def build_apis(self): for name, model in models.iteritems(): model.pop('id') self.map_properties(model, models) + # add actual definition object defs[self.get_model_ref_name(name)] = model + # need a 'type' on every level according to JsonSchema specs + defs[self.get_model_ref_name(name)]['type'] = 'object' return common_path, paths, defs, api_tags def map_parameters(self, method, path, in_params, models): @@ -172,6 +175,12 @@ def recurse(prop): prop['$ref'] = self.get_model_ref(ref) for key, subprop in prop.iteritems(): recurse(subprop) + # if a type is referenced remove 'type' and 'descriptions' + # to avoid warning 'other properties are defined at level ...' + # see https://github.com/go-swagger/go-swagger/issues/901 + if '$ref' in prop and 'type' in prop: + del prop['type'] + del prop['description'] recurse(props) def get_model_ref_name(self, name): @@ -181,7 +190,7 @@ def get_model_ref_name(self, name): This is required because the Swagger V1 specs were on a per-resource level, whereas the Swagger V2 specs are for multiple resources. """ - if name in ['ListView', 'Objects', 'Meta']: + if name in ['ListView', 'Objects', 'Meta', 'Object']: name = '%s_%s' % (self.resource_name.replace('/', '_'), name) return name @@ -202,3 +211,24 @@ def get_swagger_type(self, kind): if format: d.update(format=format) return d + + def build_models(self): + models = super(ResourceSwagger2Mapping, self).build_models() + # add extra actions models + # TODO support other models than 'Object', i.e. using response_class + # for this use different properties than those returned by + # by build_properties_from_field + if hasattr(self.resource._meta, 'extra_actions'): + for action in self.resource._meta.extra_actions: + http_method = action.get('http_method') + resource_name = '%s_%s' % (self.resource._meta.resource_name, + 'Object') + model_id = '%s_%s' % (self.resource_name, http_method) + model = self.build_model( + resource_name=resource_name, + properties=self.build_properties_from_fields( + method=http_method), + id=model_id, + ) + models.update(model) + return models From 79dbec6cef51479d0541a9f2d5d6c363f7001fb0 Mon Sep 17 00:00:00 2001 From: miraculixx Date: Mon, 22 Jan 2018 22:39:59 +0100 Subject: [PATCH 07/14] python 3 support --- tastypie_swagger/mapping2.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tastypie_swagger/mapping2.py b/tastypie_swagger/mapping2.py index d035a5c..c90a7a3 100644 --- a/tastypie_swagger/mapping2.py +++ b/tastypie_swagger/mapping2.py @@ -1,6 +1,6 @@ from tastypie_swagger.mapping import ResourceSwaggerMapping - +from six import iteritems SWAGGER_V2_TYPE_MAP = { 'List': ('array', None), 'int': ('integer', 'int32'), @@ -115,7 +115,7 @@ def build_apis(self): method, uri, op.get('parameters'), models) path[method]['parameters'] = op_params # build definitions - for name, model in models.iteritems(): + for name, model in iteritems(models): model.pop('id') self.map_properties(model, models) # add actual definition object @@ -173,7 +173,7 @@ def recurse(prop): ref = prop.get('$ref') if ref is not None and not ref.startswith('#'): prop['$ref'] = self.get_model_ref(ref) - for key, subprop in prop.iteritems(): + for key, subprop in iteritems(prop): recurse(subprop) # if a type is referenced remove 'type' and 'descriptions' # to avoid warning 'other properties are defined at level ...' From 4d150c22949c0509f587843bab1d7817a5ce6a99 Mon Sep 17 00:00:00 2001 From: miraculixx Date: Tue, 20 Feb 2018 23:18:46 +0100 Subject: [PATCH 08/14] update --- tastypie_swagger/mapping2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tastypie_swagger/mapping2.py b/tastypie_swagger/mapping2.py index c90a7a3..6fbcdd7 100644 --- a/tastypie_swagger/mapping2.py +++ b/tastypie_swagger/mapping2.py @@ -109,7 +109,8 @@ def build_apis(self): } # add optional attributes if self.resource.__doc__ is not None: - path[method].update(description=self.resource.__doc__) + description = self.resource.__doc__ + path[method].update(description=description) # add parameters op_params = self.map_parameters( method, uri, op.get('parameters'), models) From 478f8979d56afebaee241326da974983a89cf3a1 Mon Sep 17 00:00:00 2001 From: thdls55 Date: Mon, 28 Sep 2020 12:40:33 +0700 Subject: [PATCH 09/14] Django 2.2 API compat --- example/example/settings.py | 18 ++++++++++++++++-- example/example/urls.py | 10 +++++----- requirements.txt | 7 +++---- tastypie_swagger/__init__.py | 2 +- tastypie_swagger/mapping.py | 7 ++++++- tastypie_swagger/urls.py | 10 +++------- tastypie_swagger/views.py | 2 +- 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/example/example/settings.py b/example/example/settings.py index 37e5c27..0b2674d 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -29,6 +29,21 @@ # Application definition +TEMPLATES=[ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + INSTALLED_APPS = ( 'django.contrib.admin', 'django.contrib.auth', @@ -41,12 +56,11 @@ 'tastypie_swagger', ) -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) diff --git a/example/example/urls.py b/example/example/urls.py index 1cb1355..f420912 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -1,15 +1,15 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.contrib import admin from demo.apis import api -urlpatterns = patterns('', +urlpatterns = [ url(r'^api/', include(api.urls)), - url(r'^api/doc/', include('tastypie_swagger.urls', + url(r'^api/doc/', include(('tastypie_swagger.urls', 'tastypie_swagger'), namespace='demo_api_swagger'), kwargs={ "tastypie_api_module":"demo.apis.api", "namespace":"demo_api_swagger", "version": "0.1"} ), - url(r'^admin/', include(admin.site.urls)), -) + url(r'^admin/', admin.site.urls), +] diff --git a/requirements.txt b/requirements.txt index 245ced9..13d25af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ -wsgiref==0.1.2 -Django==1.7.11 -argparse==1.2.1 -django-tastypie==0.13.3 +swagger-spec-validator==2.7.3 +Django==2.2 +django-tastypie==0.14.2 -e . diff --git a/tastypie_swagger/__init__.py b/tastypie_swagger/__init__.py index c848caa..823ca1d 100644 --- a/tastypie_swagger/__init__.py +++ b/tastypie_swagger/__init__.py @@ -1 +1 @@ -VERSION = (0, 1, 3) +VERSION = (0, 1, 4) diff --git a/tastypie_swagger/mapping.py b/tastypie_swagger/mapping.py index d151ca3..45f3a15 100644 --- a/tastypie_swagger/mapping.py +++ b/tastypie_swagger/mapping.py @@ -1,7 +1,12 @@ import datetime import logging -from django.db.models.sql.constants import QUERY_TERMS +QUERY_TERMS = { + 'exact', 'iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', 'lte', 'in', + 'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'year', + 'month', 'day', 'week_day', 'hour', 'minute', 'second', 'isnull', 'search', + 'regex', 'iregex', +} try: from django.utils.encoding import force_text diff --git a/tastypie_swagger/urls.py b/tastypie_swagger/urls.py index 30511a5..60bb31b 100644 --- a/tastypie_swagger/urls.py +++ b/tastypie_swagger/urls.py @@ -1,16 +1,12 @@ - from .views import SwaggerView, ResourcesView, SchemaView from tastypie_swagger.views import SwaggerSpecs2View -try: - from django.conf.urls import patterns, include, url -except ImportError: - from django.conf.urls.defaults import patterns, include, url +from django.conf.urls import url -urlpatterns = patterns('', +urlpatterns = [ url(r'^$', SwaggerView.as_view(), name='index'), url(r'^resources/$', ResourcesView.as_view(), name='resources'), url(r'^specs/(swagger.json)?$', SwaggerSpecs2View.as_view(), name='specs'), url(r'^schema/(?P\S+)/$', SchemaView.as_view()), url(r'^schema/$', SchemaView.as_view(), name='schema'), -) +] diff --git a/tastypie_swagger/views.py b/tastypie_swagger/views.py index f6288d3..bc06e2f 100644 --- a/tastypie_swagger/views.py +++ b/tastypie_swagger/views.py @@ -4,7 +4,7 @@ from tastypie_swagger.mapping import ResourceSwaggerMapping from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import reverse +from django.urls import reverse from django.http import HttpResponse, Http404 from django.views.generic import TemplateView import tastypie From 93a30fa3202746864b96196c02880f539f25e007 Mon Sep 17 00:00:00 2001 From: thdls55 Date: Mon, 28 Sep 2020 21:29:30 +0700 Subject: [PATCH 10/14] Django 2.2 API compat --- tastypie_swagger/templates/tastypie_swagger/index.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tastypie_swagger/templates/tastypie_swagger/index.html b/tastypie_swagger/templates/tastypie_swagger/index.html index 8022d5c..8efab46 100644 --- a/tastypie_swagger/templates/tastypie_swagger/index.html +++ b/tastypie_swagger/templates/tastypie_swagger/index.html @@ -1,5 +1,3 @@ -{% load url from future %} - @@ -107,4 +105,4 @@ - \ No newline at end of file + From 513c6f73adb9329201e63c383d5e3e4d2f413238 Mon Sep 17 00:00:00 2001 From: Patrick Date: Wed, 23 Jun 2021 14:39:23 +0200 Subject: [PATCH 11/14] django 2.2 compatibility --- .gitignore | 8 +++++++- tastypie_swagger/mapping.py | 4 ++-- tastypie_swagger/views.py | 8 ++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 45fc375..c278559 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,10 @@ dist build *.egg-info .pypirc -docs/_build \ No newline at end of file +docs/_build +/.idea/django-tastypie-swagger.iml +/.idea/misc.xml +/.idea/modules.xml +/.idea/workspace.xml +/.project +/.pydevproject diff --git a/tastypie_swagger/mapping.py b/tastypie_swagger/mapping.py index 45f3a15..8cacb35 100644 --- a/tastypie_swagger/mapping.py +++ b/tastypie_swagger/mapping.py @@ -400,12 +400,12 @@ def build_extra_apis(self): identifier = self._detail_uri_name() for extra_action in self.resource._meta.extra_actions: extra_api = { - 'path': "%s{%s}/%s/" % (self.get_resource_base_uri(), identifier , extra_action.get('name')), + 'path': "%s/{%s}/%s/" % (self.get_resource_base_uri(), identifier , extra_action.get('name')), 'operations': [] } if extra_action.get("resource_type", "view") == "list": - extra_api['path'] = "%s%s/" % (self.get_resource_base_uri(), extra_action.get('name')) + extra_api['path'] = "%s/%s/" % (self.get_resource_base_uri(), extra_action.get('name')) operation = self.build_extra_operation(extra_action) extra_api['operations'].append(operation) diff --git a/tastypie_swagger/views.py b/tastypie_swagger/views.py index bc06e2f..422d24b 100644 --- a/tastypie_swagger/views.py +++ b/tastypie_swagger/views.py @@ -133,11 +133,11 @@ class Schema2View(TastypieApiMixin, SwaggerApiDataMixin, JSONView): """ Provide an individual resource schema for swagger - This JSON must conform to http://swagger.io/specification/ + This JSON must conform to http://swagger.io/specification/ at Version 2.0 For testing see example/demo.tests, which validates a default ModelResource - to conform to this specification + to conform to this specification """ def get_context_data(self, *args, **kwargs): @@ -168,7 +168,7 @@ class SwaggerSpecs2View(TastypieApiMixin, JSONView): This JSON must conform to https://github.com/wordnik/swagger-core/wiki/Resource-Listing Usage: - url(r'^api/doc/', include('tastypie_swagger.urls', + url(r'^api/doc/', include('tastypie_swagger.urls', namespace='demo_api_swagger'), kwargs={ "tastypie_api_module":"demo.apis.api", @@ -188,7 +188,7 @@ class SwaggerSpecs2View(TastypieApiMixin, JSONView): api.basePath - string of common base URI. if not provided defaults to the api's first Resource base path - In addition you may override any of the 'info' attributes in the + In addition you may override any of the 'info' attributes in the specification by adding an api.meta dict, e.g. api.info = { From d191b606c3dc6f5e0795f6f4f2f967d8c5d50aa5 Mon Sep 17 00:00:00 2001 From: miraculixx Date: Tue, 5 Apr 2022 16:45:54 +0200 Subject: [PATCH 12/14] remove six - six is a Python 2/3 migration helper, Python 2 is EOL --- tastypie_swagger/mapping2.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tastypie_swagger/mapping2.py b/tastypie_swagger/mapping2.py index 6fbcdd7..6e184ee 100644 --- a/tastypie_swagger/mapping2.py +++ b/tastypie_swagger/mapping2.py @@ -1,6 +1,5 @@ from tastypie_swagger.mapping import ResourceSwaggerMapping -from six import iteritems SWAGGER_V2_TYPE_MAP = { 'List': ('array', None), 'int': ('integer', 'int32'), @@ -41,13 +40,13 @@ class ResourceSwagger2Mapping(ResourceSwaggerMapping): tags - the "tags" dict of the specs Notes: - * Any tastypie common dataTypes referenced in the V1 specs are mapped + * Any tastypie common dataTypes referenced in the V1 specs are mapped as "$ref" and the data type is added to the defs dict. To ensure uniqueness among multiple resources, the ListView, Object and Meta datatypes are prefixed with the resource name * any parameter's "in" attribute for GET methods are set to "query", - all other methods GET/DELETE/PUT/PATCH get "body". + all other methods GET/DELETE/PUT/PATCH get "body". * if the parameter name appears as "{}" in the operation's path (e.g. parameter "id" in "/category/{id}"), the "in" attribute @@ -61,7 +60,7 @@ class ResourceSwagger2Mapping(ResourceSwaggerMapping): Developers: - IF YOU UPDATE THIS, BE SURE TO RUN validation tests in + IF YOU UPDATE THIS, BE SURE TO RUN validation tests in $ cd example $ manage.py test demo @@ -116,7 +115,7 @@ def build_apis(self): method, uri, op.get('parameters'), models) path[method]['parameters'] = op_params # build definitions - for name, model in iteritems(models): + for name, model in models.items(): model.pop('id') self.map_properties(model, models) # add actual definition object @@ -160,7 +159,7 @@ def map_properties(self, model, models): recursively map a model's properties to 'definitions' syntax This will create entries for 'definitions'. Types in their - own right are mapped using $ref references. + own right are mapped using $ref references. """ props = model.get('properties') def recurse(prop): @@ -174,7 +173,7 @@ def recurse(prop): ref = prop.get('$ref') if ref is not None and not ref.startswith('#'): prop['$ref'] = self.get_model_ref(ref) - for key, subprop in iteritems(prop): + for key, subprop in prop.items(): recurse(subprop) # if a type is referenced remove 'type' and 'descriptions' # to avoid warning 'other properties are defined at level ...' @@ -189,7 +188,7 @@ def get_model_ref_name(self, name): return unique ref name for definitions This is required because the Swagger V1 specs were on a per-resource - level, whereas the Swagger V2 specs are for multiple resources. + level, whereas the Swagger V2 specs are for multiple resources. """ if name in ['ListView', 'Objects', 'Meta', 'Object']: name = '%s_%s' % (self.resource_name.replace('/', '_'), From b5d97ab296067fbeab9b867ae6badfb27892bd7a Mon Sep 17 00:00:00 2001 From: miraculixx Date: Tue, 5 Apr 2022 17:02:43 +0200 Subject: [PATCH 13/14] added github worklfow --- .github/workflows/actions.yml | 29 +++++++++++++++++++++++++++++ example/demo/apis.py | 6 ++---- example/example/settings.py | 35 +++++++++++++++++------------------ example/example/urls.py | 10 +++++----- example/example/wsgi.py | 3 ++- setup.py | 9 +++++++++ 6 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/actions.yml diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml new file mode 100644 index 0000000..28da9c8 --- /dev/null +++ b/.github/workflows/actions.yml @@ -0,0 +1,29 @@ +name: Python package +on: [push] +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[dev] + - name: Lint with flake8 + run: | + pip install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + cd example + python manage.py test demo diff --git a/example/demo/apis.py b/example/demo/apis.py index 8ffc3d1..89c993b 100644 --- a/example/demo/apis.py +++ b/example/demo/apis.py @@ -1,20 +1,18 @@ +from demo.models import FooModel, BarModel from tastypie.api import Api from tastypie.resources import ModelResource -from demo.models import FooModel, BarModel - class FooResource(ModelResource): - class Meta: queryset = FooModel.objects.all() class BarResource(ModelResource): - class Meta: queryset = BarModel.objects.all() + api = Api('v1') api.title = 'demo API' api.description = 'lorem ipsum' diff --git a/example/example/settings.py b/example/example/settings.py index 0b2674d..90ca0ae 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -7,11 +7,10 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.7/ref/settings/ """ - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ @@ -26,22 +25,21 @@ ALLOWED_HOSTS = [] - # Application definition -TEMPLATES=[ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, ] INSTALLED_APPS = ( @@ -69,7 +67,6 @@ WSGI_APPLICATION = 'example.wsgi.application' - # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases @@ -93,8 +90,10 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ STATIC_URL = '/static/' + +# Django 3.2 +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' diff --git a/example/example/urls.py b/example/example/urls.py index f420912..ce24f43 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -6,10 +6,10 @@ url(r'^api/', include(api.urls)), url(r'^api/doc/', include(('tastypie_swagger.urls', 'tastypie_swagger'), namespace='demo_api_swagger'), - kwargs={ - "tastypie_api_module":"demo.apis.api", - "namespace":"demo_api_swagger", - "version": "0.1"} - ), + kwargs={ + "tastypie_api_module": "demo.apis.api", + "namespace": "demo_api_swagger", + "version": "0.1"} + ), url(r'^admin/', admin.site.urls), ] diff --git a/example/example/wsgi.py b/example/example/wsgi.py index f462885..5591384 100644 --- a/example/example/wsgi.py +++ b/example/example/wsgi.py @@ -8,7 +8,8 @@ """ import os +from django.core.wsgi import get_wsgi_application + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") -from django.core.wsgi import get_wsgi_application application = get_wsgi_application() diff --git a/setup.py b/setup.py index 2fd36da..dfaac50 100644 --- a/setup.py +++ b/setup.py @@ -29,4 +29,13 @@ packages=['tastypie_swagger'], include_package_data=True, zip_safe=False, + install_requires=[ + 'Django<3.3', + 'django-tastypie>=0.14.4', + ], + extras_require={ + 'dev': [ + 'swagger_spec_validator' # https://github.com/Yelp/swagger_spec_validator + ], + } ) From abcd5a8bc51ba7a6fe5521c9d51c798b9436b558 Mon Sep 17 00:00:00 2001 From: miraculixx Date: Tue, 5 Apr 2022 17:11:28 +0200 Subject: [PATCH 14/14] flake8 cleanup --- docs/conf.py | 110 ++++++++++++++++----------------- setup.py | 2 +- tastypie_swagger/mapping.py | 116 ++++++++++++++++++----------------- tastypie_swagger/mapping2.py | 3 +- tastypie_swagger/views.py | 30 ++++----- 5 files changed, 127 insertions(+), 134 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cfecd7e..94b265a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,18 +12,15 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys -import os - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -40,7 +37,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -60,13 +57,13 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -74,27 +71,27 @@ # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ---------------------------------------------- @@ -106,26 +103,26 @@ # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -135,93 +132,92 @@ # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'DjangoTastypieSwaggerdoc' - # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # Additional stuff for the LaTeX preamble. + # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'DjangoTastypieSwagger.tex', u'Django Tastypie Swagger Documentation', - u'Kyle Rimkus, Josh Bothun', 'manual'), + ('index', 'DjangoTastypieSwagger.tex', u'Django Tastypie Swagger Documentation', + u'Kyle Rimkus, Josh Bothun', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -234,7 +230,7 @@ ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -243,19 +239,19 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'DjangoTastypieSwagger', u'Django Tastypie Swagger Documentation', - u'Kyle Rimkus, Josh Bothun', 'DjangoTastypieSwagger', 'One line description of project.', - 'Miscellaneous'), + ('index', 'DjangoTastypieSwagger', u'Django Tastypie Swagger Documentation', + u'Kyle Rimkus, Josh Bothun', 'DjangoTastypieSwagger', 'One line description of project.', + 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/setup.py b/setup.py index dfaac50..c9a55f6 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ ], extras_require={ 'dev': [ - 'swagger_spec_validator' # https://github.com/Yelp/swagger_spec_validator + 'swagger_spec_validator' # https://github.com/Yelp/swagger_spec_validator ], } ) diff --git a/tastypie_swagger/mapping.py b/tastypie_swagger/mapping.py index 8cacb35..070c99d 100644 --- a/tastypie_swagger/mapping.py +++ b/tastypie_swagger/mapping.py @@ -1,28 +1,25 @@ import datetime import logging -QUERY_TERMS = { - 'exact', 'iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', 'lte', 'in', - 'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'year', - 'month', 'day', 'week_day', 'hour', 'minute', 'second', 'isnull', 'search', - 'regex', 'iregex', -} - try: from django.utils.encoding import force_text except ImportError: from django.utils.encoding import force_text as force_text - from tastypie import fields - from .utils import trailing_slash_or_none, urljoin_forced +QUERY_TERMS = { + 'exact', 'iexact', 'contains', 'icontains', 'gt', 'gte', 'lt', 'lte', 'in', + 'startswith', 'istartswith', 'endswith', 'iendswith', 'range', 'year', + 'month', 'day', 'week_day', 'hour', 'minute', 'second', 'isnull', 'search', + 'regex', 'iregex', +} + logger = logging.getLogger(__name__) # Ignored POST fields IGNORED_FIELDS = ['id', ] - # Enable all basic ORM filters but do not allow filtering across relationships. ALL = 1 # Enable all ORM filters, including across relationships @@ -38,7 +35,7 @@ class ResourceSwaggerMapping(object): http://django-tastypie.readthedocs.org/en/latest/resources.html https://github.com/wordnik/swagger-core/wiki/API-Declaration """ - WRITE_ACTION_IGNORED_FIELDS = ['id', 'resource_uri',] + WRITE_ACTION_IGNORED_FIELDS = ['id', 'resource_uri', ] # Default summary strings for operations OPERATION_SUMMARIES = { @@ -98,7 +95,7 @@ def get_operation_summary(self, detail=True, method='get'): Get a basic summary string for a single operation """ key = '%s-%s' % (method.lower(), detail and 'detail' or 'list') - plural = not detail and method is 'get' + plural = not detail and method == 'get' verbose_name = self.get_resource_verbose_name(plural=plural) summary = self.OPERATION_SUMMARIES.get(key, '') if summary: @@ -117,9 +114,11 @@ def get_resource_base_uri(self): elif hasattr(self.resource, 'get_resource_uri'): return self.resource.get_resource_uri() else: - raise AttributeError('Resource %(resource)s has neither get_resource_list_uri nor get_resource_uri' % {'resource': self.resource}) + raise AttributeError('Resource %(resource)s has neither get_resource_list_uri nor get_resource_uri' % { + 'resource': self.resource}) - def build_parameter(self, paramType='body', name='', dataType='', required=True, description='', allowed_values = None): + def build_parameter(self, paramType='body', name='', dataType='', required=True, description='', + allowed_values=None): parameter = { 'paramType': paramType, 'name': name, @@ -131,15 +130,15 @@ def build_parameter(self, paramType='body', name='', dataType='', required=True, # TODO make use of this to Implement the allowable_values of swagger # (https://github.com/wordnik/swagger-core/wiki/Datatypes) at the field level. # This could be added to the meta value of the resource to specify enum-like or range data on a field. -# if allowed_values: -# parameter.update({'allowableValues': allowed_values}) + # if allowed_values: + # parameter.update({'allowableValues': allowed_values}) return parameter def build_parameters_from_fields(self): parameters = [] for name, field in self.schema['fields'].items(): # Ignore readonly fields - if not field['readonly'] and not name in IGNORED_FIELDS: + if not field['readonly'] and name not in IGNORED_FIELDS: parameters.append(self.build_parameter( name=name, dataType=field['type'], @@ -158,23 +157,24 @@ def build_parameters_for_list(self, method='GET'): def build_parameters_from_ordering(self): values = [] - [values.extend([field,"-%s"%field]) for field in self.schema['ordering']] + [values.extend([field, "-%s" % field]) for field in self.schema['ordering']] return { 'paramType': "query", 'name': "order_by", 'dataType': "String", 'required': False, - 'description': unicode("Orders the result set based on the selection. " - "Ascending order by default, prepending the '-' " - "sign change the sorting order to descending"), + 'description': "Orders the result set based on the selection. " + "Ascending order by default, prepending the '-' " + "sign change the sorting order to descending", 'allowableValues': { - 'valueType' : "LIST", + 'valueType': "LIST", 'values': values } } - def build_parameters_from_filters(self, prefix="", method='GET'): + def build_parameters_from_filters(self, prefix="", method='GET'): # noqa + # FIXME this method is too complex (flake8) parameters = [] # Deal with the navigational filters. @@ -198,9 +198,9 @@ def build_parameters_from_filters(self, prefix="", method='GET'): if not prefix.find('{0}__'.format(name)) >= 0: # Integer value means this points to a related model if field in [ALL, ALL_WITH_RELATIONS]: - # For fields marked as ALL_WITH_RELATIONS, we must fetch information on their related resources as well. - # However, tastypie allows us to mark fields that do not have related resources as ALL_WITH_RELATIONS. - # This functions like a white list. + # For fields marked as ALL_WITH_RELATIONS, we must fetch information on their related + # resources as well. However, tastypie allows us to mark fields that do not have + # related resources as ALL_WITH_RELATIONS. This functions like a white list. # Therefore, we need to check whether a field actually has a related resource. if field == ALL: has_related_resource = False @@ -208,7 +208,7 @@ def build_parameters_from_filters(self, prefix="", method='GET'): has_related_resource = hasattr(self.resource.fields[name], 'get_related_resource') if not has_related_resource: - #This code has been mostly sucked from the tastypie lib + # This code has been mostly sucked from the tastypie lib if getattr(self.resource._meta, 'queryset', None) is not None: # Get the possible query terms from the current QuerySet. if hasattr(self.resource._meta.queryset.query.query_terms, 'keys'): @@ -225,18 +225,23 @@ def build_parameters_from_filters(self, prefix="", method='GET'): # Django 1.5+. field = QUERY_TERMS - else: # Show all params from related model + else: # Show all params from related model # Add a subset of filter only foreign-key compatible on the relation itself. # We assume foreign keys are only int based. - field = ['gt', 'in', 'gte', 'lt', 'lte', 'exact'] # TODO This could be extended by checking the actual type of the relational field, but afaik it's also an issue on tastypie. + # TODO This could be extended by checking the actual type of the relational field, + # but afaik it's also an issue on tastypie. + field = ['gt', 'in', 'gte', 'lt', 'lte', + 'exact'] related_resource = self.resource.fields[name].get_related_resource(None) related_mapping = ResourceSwaggerMapping(related_resource) - parameters.extend(related_mapping.build_parameters_from_filters(prefix="%s%s__" % (prefix, name))) + parameters.extend( + related_mapping.build_parameters_from_filters(prefix="%s%s__" % (prefix, name))) if isinstance(field, (list, tuple, set)): # Skip if this is an incorrect filter - if name not in self.schema['fields']: continue + if name not in self.schema['fields']: + continue schema_field = self.schema['fields'][name] @@ -254,7 +259,7 @@ def build_parameters_from_filters(self, prefix="", method='GET'): paramType="query", name="%s%s" % (prefix, name), dataType=dataType, - required = False, + required=False, description=description, )) else: @@ -262,7 +267,7 @@ def build_parameters_from_filters(self, prefix="", method='GET'): paramType="query", name="%s%s__%s" % (prefix, name, query), dataType=dataType, - required = False, + required=False, description=force_text(schema_field['help_text']), )) @@ -272,7 +277,7 @@ def build_parameter_for_object(self, method='get'): return self.build_parameter( name=self.resource_name, dataType="%s_%s" % (self.resource_name, method) if not method == "get" else self.resource_name, - required = True + required=True ) def _detail_uri_name(self): @@ -285,9 +290,9 @@ def build_parameters_from_extra_action(self, method, fields, resource_type): parameters = [] if resource_type == "view": parameters.append(self.build_parameter(paramType='path', - name=self._detail_uri_name(), - dataType=self.resource_pk_type, - description='Primary key of resource')) + name=self._detail_uri_name(), + dataType=self.resource_pk_type, + description='Primary key of resource')) for name, field in fields.items(): parameters.append(self.build_parameter( paramType=field.get("param_type", "query"), @@ -304,13 +309,12 @@ def build_parameters_from_extra_action(self, method, fields, resource_type): if hasattr(self.resource.Meta, 'custom_filtering'): for name, field in self.resource.Meta.custom_filtering.items(): parameters.append(self.build_parameter( - paramType='query', - name=name, - dataType=field['dataType'], - required=field['required'], - description=unicode(field['description']) - )) - + paramType='query', + name=name, + dataType=field['dataType'], + required=field['required'], + description=field['description'] + )) return parameters @@ -354,14 +358,15 @@ def build_extra_operation(self, extra_action): # is not set. fields=extra_action.get('fields', {}), resource_type=extra_action.get("resource_type", "view")), - 'responseClass': 'Object', #TODO this should be extended to allow the creation of a custom object. + 'responseClass': 'Object', # TODO this should be extended to allow the creation of a custom object. 'nickname': extra_action['name'], 'notes': extra_action.get('notes', ''), } def build_detail_api(self): detail_api = { - 'path': urljoin_forced(self.get_resource_base_uri(), '{%s}%s' % (self._detail_uri_name(), trailing_slash_or_none())), + 'path': urljoin_forced(self.get_resource_base_uri(), + '{%s}%s' % (self._detail_uri_name(), trailing_slash_or_none())), 'operations': [], } @@ -400,7 +405,7 @@ def build_extra_apis(self): identifier = self._detail_uri_name() for extra_action in self.resource._meta.extra_actions: extra_api = { - 'path': "%s/{%s}/%s/" % (self.get_resource_base_uri(), identifier , extra_action.get('name')), + 'path': "%s/{%s}/%s/" % (self.get_resource_base_uri(), identifier, extra_action.get('name')), 'operations': [] } @@ -437,7 +442,7 @@ def build_properties_from_fields(self, method='get'): if name in excludes: continue # Exclude fields from custom put / post object definition - if method in ['post','put']: + if method in ['post', 'put']: if name in self.WRITE_ACTION_IGNORED_FIELDS: continue if field.get('readonly'): @@ -449,12 +454,12 @@ def build_properties_from_fields(self, method='get'): field['default'] = field.get('default').isoformat() properties.update(self.build_property( - name, - field.get('type'), - # note: 'help_text' is a Django proxy which must be wrapped - # in unicode *specifically* to get the actual help text. - force_text(field.get('help_text', '')), - ) + name, + field.get('type'), + # note: 'help_text' is a Django proxy which must be wrapped + # in unicode *specifically* to get the actual help text. + force_text(field.get('help_text', '')), + ) ) return properties @@ -466,7 +471,6 @@ def build_model(self, resource_name, id, properties): } } - def build_list_models_and_properties(self): models = {} @@ -532,7 +536,7 @@ def build_list_models_and_properties(self): return models def build_models(self): - #TODO this should be extended to allow the creation of a custom objects for extra_actions. + # TODO this should be extended to allow the creation of a custom objects for extra_actions. models = {} # Take care of the list particular schema with meta and so on. diff --git a/tastypie_swagger/mapping2.py b/tastypie_swagger/mapping2.py index 6e184ee..734e765 100644 --- a/tastypie_swagger/mapping2.py +++ b/tastypie_swagger/mapping2.py @@ -12,7 +12,6 @@ class ResourceSwagger2Mapping(ResourceSwaggerMapping): - """ build the 'paths' and 'definitions' entries in a swagger V2 spec @@ -162,6 +161,7 @@ def map_properties(self, model, models): own right are mapped using $ref references. """ props = model.get('properties') + def recurse(prop): if isinstance(prop, dict): kind = prop.get('type') @@ -181,6 +181,7 @@ def recurse(prop): if '$ref' in prop and 'type' in prop: del prop['type'] del prop['description'] + recurse(props) def get_model_ref_name(self, name): diff --git a/tastypie_swagger/views.py b/tastypie_swagger/views.py index 422d24b..a454932 100644 --- a/tastypie_swagger/views.py +++ b/tastypie_swagger/views.py @@ -1,19 +1,16 @@ -from importlib import import_module import json -import sys -from tastypie_swagger.mapping import ResourceSwaggerMapping - +import tastypie from django.core.exceptions import ImproperlyConfigured -from django.urls import reverse from django.http import HttpResponse, Http404 +from django.urls import reverse from django.views.generic import TemplateView -import tastypie +from importlib import import_module +from tastypie_swagger.mapping import ResourceSwaggerMapping from tastypie_swagger.mapping2 import ResourceSwagger2Mapping class TastypieApiMixin(object): - """ Provides views with a 'tastypie_api' attr representing a tastypie.api.Api instance @@ -29,7 +26,8 @@ def tastypie_api(self): tastypie_api_module = self.kwargs.get('tastypie_api_module', None) if not tastypie_api_module: raise ImproperlyConfigured( - "tastypie_api_module must be defined as an extra parameters in urls.py with its value being a path to a tastypie.api.Api instance.") + "tastypie_api_module must be defined as an extra parameters " + "in urls.py with its value being a path to a tastypie.api.Api instance.") if isinstance(tastypie_api_module, tastypie.api.Api): tastypie_api = tastypie_api_module @@ -50,7 +48,6 @@ def tastypie_api(self): class SwaggerApiDataMixin(object): - """ Provides required API context data """ @@ -66,7 +63,6 @@ def get_context_data(self, *args, **kwargs): class JSONView(TemplateView): - """ Simple JSON rendering """ @@ -93,7 +89,6 @@ def render_to_response(self, context, **response_kwargs): class SwaggerView(TastypieApiMixin, TemplateView): - """ Display the swagger-ui page """ @@ -108,7 +103,6 @@ def get_context_data(self, **kwargs): class ResourcesView(TastypieApiMixin, SwaggerApiDataMixin, JSONView): - """ Provide a top-level resource listing for swagger @@ -122,14 +116,14 @@ def get_context_data(self, *args, **kwargs): apis = [{'path': '/%s' % name} for name in sorted(self.tastypie_api._registry.keys())] context.update({ - 'basePath': self.request.build_absolute_uri(reverse('%s:schema' % self.kwargs.get('namespace'))).rstrip('/'), + 'basePath': self.request.build_absolute_uri(reverse('%s:schema' % self.kwargs.get('namespace'))).rstrip( + '/'), 'apis': apis, }) return context class Schema2View(TastypieApiMixin, SwaggerApiDataMixin, JSONView): - """ Provide an individual resource schema for swagger @@ -143,7 +137,7 @@ class Schema2View(TastypieApiMixin, SwaggerApiDataMixin, JSONView): def get_context_data(self, *args, **kwargs): # Verify matching tastypie resource exists resource_name = kwargs.get('resource', None) - if not resource_name in self.tastypie_api._registry: + if resource_name not in self.tastypie_api._registry: raise Http404 # Generate mapping from tastypie.resources.Resource.build_schema @@ -161,7 +155,6 @@ def get_context_data(self, *args, **kwargs): class SwaggerSpecs2View(TastypieApiMixin, JSONView): - """ Provide a top-level resource listing for swagger @@ -206,7 +199,7 @@ class SwaggerSpecs2View(TastypieApiMixin, JSONView): def get_resource(self, context, resource_name): # Verify matching tastypie resource exists - if not resource_name in self.tastypie_api._registry: + if resource_name not in self.tastypie_api._registry: raise Http404 # Generate mapping from tastypie.resources.Resource.build_schema resource = self.tastypie_api._registry.get(resource_name) @@ -267,7 +260,6 @@ def get_context_data(self, *args, **kwargs): class SchemaView(TastypieApiMixin, SwaggerApiDataMixin, JSONView): - """ Provide an individual resource schema for swagger @@ -277,7 +269,7 @@ class SchemaView(TastypieApiMixin, SwaggerApiDataMixin, JSONView): def get_context_data(self, *args, **kwargs): # Verify matching tastypie resource exists resource_name = kwargs.get('resource', None) - if not resource_name in self.tastypie_api._registry: + if resource_name not in self.tastypie_api._registry: raise Http404 # Generate mapping from tastypie.resources.Resource.build_schema