├── tests ├── __init__.py ├── lib │ ├── __init__.py │ └── helpers.py ├── test_sanitize_doc.py ├── test_make_class.py ├── test_render_hompage.py ├── test_render_endpoint.py ├── test_model.py ├── test_nested_class.py ├── test_operation.py ├── test_nested_func.py ├── test_resource_lister.py ├── test_extract_swagger_path.py ├── test_swagger_registry.py ├── test_parse_docs.py ├── test_deduce_swagger_type_flat.py ├── test_swagger_endpoint_func.py ├── test_render_page.py ├── test_docs.py ├── test_extract_path_arguments.py ├── test_deduce_swagger_type.py ├── test_merge_parameter_list.py ├── test_get_current_registry.py ├── test_staticfiles.py ├── test_register_once.py ├── test_swagger_endpoint_class.py ├── fixtures_add_model.py └── test_add_model.py ├── pytest.ini ├── .bandit.rc ├── flask_restful_swagger ├── static │ ├── .gitignore │ ├── images │ │ ├── throbber.gif │ │ ├── logo_small.png │ │ ├── wordnik_api.png │ │ ├── explorer_icons.png │ │ └── pet_store_api.png │ ├── lib │ │ ├── jquery.slideto.min.js │ │ ├── jquery.wiggle.min.js │ │ ├── jquery.ba-bbq.min.js │ │ ├── highlight.7.3.pack.js │ │ ├── swagger-oauth.js │ │ ├── shred │ │ │ └── content.js │ │ └── underscore-min.js │ ├── o2c.html │ ├── css │ │ ├── highlight.default.css │ │ └── hightlight.default.css │ ├── endpoint.html │ └── index.html ├── __init__.py └── container_boot.sh ├── .dockerignore ├── MANIFEST.in ├── README ├── assets ├── requirements.txt └── requirements-dev.txt ├── development ├── bash │ ├── .bash_customize │ ├── README.md │ ├── .bash_profile │ ├── .bashrc │ └── .bash_git ├── Dockerfile └── DEVELOPMENT.md ├── static ├── images │ ├── throbber.gif │ ├── logo_small.png │ ├── wordnik_api.png │ └── pet_store_api.png ├── resources.json ├── js │ ├── jquery.slideto.min.js │ ├── jquery.wiggle.min.js │ ├── jquery.ba-bbq.min.js │ ├── highlight.7.3.pack.js │ ├── shred │ │ └── content.js │ └── underscore-min.js ├── docs.html └── css │ └── hightlight.default.css ├── development.env ├── container ├── MANIFEST ├── Pipfile ├── scripts ├── releases │ └── Makefile ├── dev ├── hooks │ └── pre-commit ├── common │ ├── wheel.sh │ ├── upload.sh │ └── common.sh └── commander.sh ├── .coveragerc ├── TODO ├── .isort.cfg ├── .flake8 ├── CONTRIBUTING.md ├── docker-compose.yml ├── setup.py ├── LICENSE ├── .github └── workflows │ ├── docker27.yml │ └── docker37.yml ├── examples ├── inheritance.py ├── basic.py └── blueprints.py ├── .gitignore └── CODE_OF_CONDUCT.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | -------------------------------------------------------------------------------- /tests/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bandit.rc: -------------------------------------------------------------------------------- 1 | skips: ['B101'] 2 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/.gitignore: -------------------------------------------------------------------------------- 1 | !lib 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .tox 2 | .pytest_cache 3 | tests/__pycache__ 4 | __pycache__ 5 | *.pyc 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README* 2 | recursive-include flask_restful_swagger/static * 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Please see documentation here: https://github.com/rantav/flask-restful-swagger -------------------------------------------------------------------------------- /flask_restful_swagger/__init__.py: -------------------------------------------------------------------------------- 1 | registry = {"models": {}} 2 | 3 | api_spec_static = "" 4 | -------------------------------------------------------------------------------- /assets/requirements.txt: -------------------------------------------------------------------------------- 1 | # Application Requirements 2 | Jinja2>=2.10.1,<3.0.0 3 | Flask-RESTful>=0.3.6 4 | -------------------------------------------------------------------------------- /development/bash/.bash_customize: -------------------------------------------------------------------------------- 1 | # Customize Your Path Here 2 | export PATH="/home/user/.local/bin:${PATH}" 3 | -------------------------------------------------------------------------------- /static/images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/flask-restful-swagger/HEAD/static/images/throbber.gif -------------------------------------------------------------------------------- /development.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=/app/flask_restful_swagger/ 2 | GIT_HOOKS=1 3 | GIT_HOOKS_PROTECTED_BRANCHES="^(master)" 4 | -------------------------------------------------------------------------------- /static/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/flask-restful-swagger/HEAD/static/images/logo_small.png -------------------------------------------------------------------------------- /static/images/wordnik_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/flask-restful-swagger/HEAD/static/images/wordnik_api.png -------------------------------------------------------------------------------- /static/images/pet_store_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/flask-restful-swagger/HEAD/static/images/pet_store_api.png -------------------------------------------------------------------------------- /container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Entering Development Container ..." 4 | docker-compose exec flask_restful_swagger bash 5 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | flask_restful_swagger/__init__.py 4 | flask_restful_swagger/swagger.py 5 | -------------------------------------------------------------------------------- /flask_restful_swagger/container_boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd "flask_restful_swagger" || exit 127 4 | while true; do sleep 1; done 5 | -------------------------------------------------------------------------------- /development/bash/README.md: -------------------------------------------------------------------------------- 1 | # Bash Environment 2 | 3 | Run the `dev setup` command to re-symlink `.bash_customize` into the container's BASH environment. 4 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/flask-restful-swagger/HEAD/flask_restful_swagger/static/images/throbber.gif -------------------------------------------------------------------------------- /flask_restful_swagger/static/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/flask-restful-swagger/HEAD/flask_restful_swagger/static/images/logo_small.png -------------------------------------------------------------------------------- /flask_restful_swagger/static/images/wordnik_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/flask-restful-swagger/HEAD/flask_restful_swagger/static/images/wordnik_api.png -------------------------------------------------------------------------------- /flask_restful_swagger/static/images/explorer_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/flask-restful-swagger/HEAD/flask_restful_swagger/static/images/explorer_icons.png -------------------------------------------------------------------------------- /flask_restful_swagger/static/images/pet_store_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rantav/flask-restful-swagger/HEAD/flask_restful_swagger/static/images/pet_store_api.png -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.6" 12 | -------------------------------------------------------------------------------- /scripts/releases/Makefile: -------------------------------------------------------------------------------- 1 | release: 2 | git tag -a `python setup.py --version` -m "Releasing to https://pypi.python.org/pypi/flask-restful-swagger/" 3 | git push --tags 4 | python setup.py sdist upload 5 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = tests/* 4 | setup.py 5 | source = . 6 | 7 | [report] 8 | precision = 1 9 | show_missing = True 10 | ignore_errors = True 11 | exclude_lines = 12 | no cover 13 | -------------------------------------------------------------------------------- /static/resources.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "0.1", 3 | "swaggerVersion": "1.2", 4 | "apis": [ 5 | { 6 | "path": "/../../api/spec.json", 7 | "description": "My TODO service" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /development/bash/.bash_profile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Do Not Modify This File, It's Intended To Be Updated From Time to TIme 4 | # INSTEAD: add additional functionality though the .bash_customize file. 5 | 6 | source "${HOME}/.bashrc" 7 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Syntactic sugar and smartness around dataType. Support the following: 2 | - 'Model' 3 | - Model 4 | - 'List[Model]' 5 | - ['Model'] 6 | - [Model] 7 | 8 | Support for HTML pages 9 | 10 | Create a pip package and publish it 11 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=80 3 | indent=' ' 4 | multi_line_output=3 5 | length_sort=0 6 | default_section=FIRSTPARTY 7 | no_lines_before=LOCALFOLDER 8 | sections=FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 9 | include_trailing_comma=true 10 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | ignore = E203, W503, W504, W605 4 | max-line-length = 79 5 | max-complexity = 12 6 | 7 | exclude = 8 | .git 9 | dist 10 | build 11 | flask_restful_swagger.egg-info 12 | htmlcov 13 | scripts 14 | static 15 | 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | [Code of Conduct](./CODE_OF_CONDUCT.md) 4 | 5 | [Contribution Guide](./development/DEVELOPMENT.md) 6 | 7 | # Contacts 8 | 9 | - @rantav 10 | - @niall-byrne 11 | 12 | __This project is part of the [Cloudify Cosmo project](https://github.com/CloudifySource/)__ 13 | -------------------------------------------------------------------------------- /assets/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Development Requirements 2 | bandit>=1.6.2,<1.7.0 3 | bs4>=0.0.1,<0.1.0 4 | commitizen>=0.9.11,<1.17.0 5 | isort>=4.3.21,<4.4.0 6 | flake8>=3.7.9,<3.8.0s 7 | mock>=3.0.5,<3.1.0 8 | pytest>=4.6.9,<5.3.0 9 | pytest-cov>=2.8.1,<2.9.0 10 | safety>=1.8.5,<1.9.0 11 | wheel>=0.34.1,<0.35.0 12 | yapf>=0.28.0,<0.29.0 13 | -------------------------------------------------------------------------------- /static/js/jquery.slideto.min.js: -------------------------------------------------------------------------------- 1 | (function(b){b.fn.slideto=function(a){a=b.extend({slide_duration:"slow",highlight_duration:3E3,highlight:true,highlight_color:"#FFFF99"},a);return this.each(function(){obj=b(this);b("body").animate({scrollTop:obj.offset().top},a.slide_duration,function(){a.highlight&&b.ui.version&&obj.effect("highlight",{color:a.highlight_color},a.highlight_duration)})})}})(jQuery); 2 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/lib/jquery.slideto.min.js: -------------------------------------------------------------------------------- 1 | (function(b){b.fn.slideto=function(a){a=b.extend({slide_duration:"slow",highlight_duration:3E3,highlight:true,highlight_color:"#FFFF99"},a);return this.each(function(){obj=b(this);b("body").animate({scrollTop:obj.offset().top},a.slide_duration,function(){a.highlight&&b.ui.version&&obj.effect("highlight",{color:a.highlight_color},a.highlight_duration)})})}})(jQuery); 2 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/o2c.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | flask_restful_swagger: 5 | build: 6 | context: . 7 | dockerfile: development/Dockerfile 8 | ports: 9 | - "127.0.0.1:5000:5000" 10 | env_file: 11 | - development.env 12 | volumes: 13 | - ${HOME}/.ssh:/home/user/.ssh 14 | - ${HOME}/.gitconfig:/home/user/.gitconfig 15 | - ${HOME}/.gitconfig_global:/home/user/.gitconfig_global 16 | - ./:/app 17 | command: > 18 | ./flask_restful_swagger/container_boot.sh 19 | -------------------------------------------------------------------------------- /tests/test_sanitize_doc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_restful_swagger import swagger 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "test_input,expected", 8 | [ 9 | ("Hey\n", "Hey
"), 10 | ("\n/n\n/n\n", "
/n
/n
"), 11 | ("No Change", "No Change"), 12 | ], 13 | ) 14 | def test_string_sanitize_doc(test_input, expected): 15 | assert swagger._sanitize_doc(test_input) == expected 16 | 17 | 18 | def test_none_sanitize_doc(): 19 | assert swagger._sanitize_doc(None) is None 20 | -------------------------------------------------------------------------------- /tests/test_make_class.py: -------------------------------------------------------------------------------- 1 | from flask_restful_swagger import swagger 2 | 3 | 4 | def test_make_class_with_input_class(): 5 | class TestClass: 6 | pass 7 | 8 | assert swagger.make_class(TestClass) == TestClass 9 | 10 | 11 | def test_make_class_with_input_instance(): 12 | class TestClass: 13 | pass 14 | 15 | test_class = TestClass() 16 | 17 | assert swagger.make_class(test_class) == TestClass 18 | 19 | 20 | def test_make_class_with_none(): 21 | assert isinstance(None, swagger.make_class(None)) 22 | -------------------------------------------------------------------------------- /tests/test_render_hompage.py: -------------------------------------------------------------------------------- 1 | from flask_restful_swagger import swagger 2 | 3 | try: 4 | from unittest.mock import patch 5 | except ImportError: 6 | from mock import patch 7 | 8 | 9 | @patch("flask_restful_swagger.swagger.render_page") 10 | @patch("flask.wrappers.Response") 11 | def test_render_hompage_func(response_obj, mock_render_page): 12 | resource_list_url = "resource_list_url" 13 | swagger.render_homepage(resource_list_url) 14 | mock_render_page.assert_called_once_with( 15 | "index.html", {"resource_list_url": resource_list_url} 16 | ) 17 | -------------------------------------------------------------------------------- /tests/test_render_endpoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from flask_restful_swagger import swagger 4 | 5 | try: 6 | from unittest.mock import patch 7 | except ImportError: 8 | from mock import patch 9 | 10 | 11 | class Endpoint: 12 | pass 13 | 14 | 15 | class TestRenderEndpoint(unittest.TestCase): 16 | def test_render_endpoint(self): 17 | endpoint = Endpoint() 18 | with patch( 19 | "flask_restful_swagger.swagger.render_page" 20 | ) as mock_render_page: 21 | swagger.render_endpoint(endpoint) 22 | mock_render_page.assert_called_with( 23 | "endpoint.html", endpoint.__dict__ 24 | ) 25 | -------------------------------------------------------------------------------- /scripts/dev: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | _script() 4 | { 5 | 6 | shopt -s expand_aliases 7 | _script_commands=$(dev shortlist) 8 | 9 | local cur 10 | COMPREPLY=() 11 | cur="${COMP_WORDS[COMP_CWORD]}" 12 | 13 | while IFS='' read -r line; do COMPREPLY+=("$line"); done < <(compgen -W "${_script_commands}" -- "${cur}") 14 | return 0 15 | 16 | } 17 | 18 | complete -o nospace -F _script dev 19 | 20 | dev_identifier() { 21 | Cyan='\033[36m' # Cyan 22 | BRed='\033[31m' # Red 23 | BGreen='\033[32m' # Green 24 | NC="\033[0m" # Color Reset 25 | echo -en "(${BGreen}flask_restful_swagger${NC})" 26 | } 27 | 28 | alias dev='$(git rev-parse --show-toplevel)/scripts/commander.sh' 29 | PROMPT_COMMAND="dev_identifier; $PROMPT_COMMAND" 30 | -------------------------------------------------------------------------------- /development/bash/.bashrc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Do Not Modify This File, It's Intended To Be Updated From Time to TIme 4 | # INSTEAD: add additional functionality though the .bash_customize file. 5 | 6 | PS1='${git_branch}\n${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' 7 | 8 | # Terminal Colors 9 | if [[ -x /usr/bin/dircolors ]]; then 10 | test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" 11 | alias ls='ls --color=auto' 12 | alias grep='grep --color=auto' 13 | alias fgrep='fgrep --color=auto' 14 | alias egrep='egrep --color=auto' 15 | fi 16 | 17 | set -e 18 | source /app/scripts/dev 19 | source /home/user/.bash_git 20 | source /home/user/.bash_customize 21 | set +e 22 | -------------------------------------------------------------------------------- /static/js/jquery.wiggle.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | jQuery Wiggle 3 | Author: WonderGroup, Jordan Thomas 4 | URL: http://labs.wondergroup.com/demos/mini-ui/index.html 5 | License: MIT (http://en.wikipedia.org/wiki/MIT_License) 6 | */ 7 | jQuery.fn.wiggle=function(o){var d={speed:50,wiggles:3,travel:5,callback:null};var o=jQuery.extend(d,o);return this.each(function(){var cache=this;var wrap=jQuery(this).wrap('
').css("position","relative");var calls=0;for(i=1;i<=o.wiggles;i++){jQuery(this).animate({left:"-="+o.travel},o.speed).animate({left:"+="+o.travel*2},o.speed*2).animate({left:"-="+o.travel},o.speed,function(){calls++;if(jQuery(cache).parent().hasClass('wiggle-wrap')){jQuery(cache).parent().replaceWith(cache);} 8 | if(calls==o.wiggles&&jQuery.isFunction(o.callback)){o.callback();}});}});}; -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import pytest 4 | 5 | from flask_restful_swagger import swagger 6 | 7 | try: 8 | from unittest.mock import patch 9 | except ImportError: 10 | from mock import patch 11 | 12 | 13 | class TestEmptyClass: 14 | pass 15 | 16 | 17 | @pytest.mark.parametrize( 18 | "test_input", 19 | [ 20 | TestEmptyClass, # Test a class 21 | "im a str", # Test a str 22 | 123, # Test int 23 | None, # Test None 24 | datetime.datetime.now(), # Test datetime 25 | ], 26 | ) 27 | def test_model_with_input(test_input): 28 | with patch("flask_restful_swagger.swagger.add_model") as mock_add_model: 29 | assert swagger.model(test_input) == test_input 30 | mock_add_model.assert_called_once_with(test_input) 31 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/lib/jquery.wiggle.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | jQuery Wiggle 3 | Author: WonderGroup, Jordan Thomas 4 | URL: http://labs.wondergroup.com/demos/mini-ui/index.html 5 | License: MIT (http://en.wikipedia.org/wiki/MIT_License) 6 | */ 7 | jQuery.fn.wiggle=function(o){var d={speed:50,wiggles:3,travel:5,callback:null};var o=jQuery.extend(d,o);return this.each(function(){var cache=this;var wrap=jQuery(this).wrap('
').css("position","relative");var calls=0;for(i=1;i<=o.wiggles;i++){jQuery(this).animate({left:"-="+o.travel},o.speed).animate({left:"+="+o.travel*2},o.speed*2).animate({left:"-="+o.travel},o.speed,function(){calls++;if(jQuery(cache).parent().hasClass('wiggle-wrap')){jQuery(cache).parent().replaceWith(cache);} 8 | if(calls==o.wiggles&&jQuery.isFunction(o.callback)){o.callback();}});}});}; -------------------------------------------------------------------------------- /tests/test_nested_class.py: -------------------------------------------------------------------------------- 1 | from flask_restful_swagger.swagger import _Nested 2 | 3 | 4 | class MockClass(object): 5 | pass 6 | 7 | 8 | def mock_function(*args, **kwargs): 9 | return args, kwargs 10 | 11 | 12 | def test_nested_class(): 13 | 14 | kwargs = {"arg1": 1, "arg2": "Helllllllo!"} 15 | 16 | instance = _Nested(MockClass, **kwargs) 17 | 18 | assert isinstance(instance, _Nested) 19 | assert instance._klass == MockClass 20 | assert instance._nested == kwargs 21 | assert instance.nested() == kwargs 22 | 23 | 24 | def test_nested_function(): 25 | 26 | kwargs = {"arg1": 1, "arg2": "Helllllllo!"} 27 | args = ("hello", "there") 28 | 29 | instance = _Nested(mock_function, **kwargs) 30 | value1, value2 = instance(*args, **kwargs) 31 | assert value1 == args 32 | assert value2 == kwargs 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | with open("README") as file: 7 | long_description = file.read() 8 | 9 | setup( 10 | name="flask-restful-swagger", 11 | version="0.20.2", 12 | url="https://github.com/rantav/flask-restful-swagger", 13 | zip_safe=False, 14 | packages=["flask_restful_swagger"], 15 | package_data={ 16 | "flask_restful_swagger": [ 17 | "static/*.*", 18 | "static/css/*.*", 19 | "static/images/*.*", 20 | "static/lib/*.*", 21 | "static/lib/shred/*.*", 22 | ] 23 | }, 24 | description="Extract swagger specs from your flask-restful project", 25 | author="Ran Tavory", 26 | license="MIT", 27 | long_description=long_description, 28 | install_requires=[ 29 | "Jinja2>=2.10.1,<3.0.0", 30 | "Flask-RESTful>=0.3.6", 31 | ], 32 | ) 33 | -------------------------------------------------------------------------------- /tests/test_operation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_restful_swagger import swagger 4 | 5 | 6 | def empty_func(): 7 | pass 8 | 9 | 10 | def func_with_many_args(arg1, arg2, arg3, kwarg1=None, kwarg2=None): 11 | allargs = (arg1, arg2, arg3, kwarg1, kwarg2) 12 | print("func_with_many_args: %s, %s, %s, %s, %s" % allargs) 13 | 14 | 15 | class EmptyClass: 16 | pass 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "plain_input,swagger_kwargs", 21 | [ 22 | (empty_func, {"arg1": None, "arg2": None}), 23 | (func_with_many_args, {"arg1": None, "arg2": None}), 24 | (EmptyClass, {"arg1": None}), 25 | (EmptyClass(), {"arg1": None}), 26 | ], 27 | ) 28 | def test_operation(plain_input, swagger_kwargs): 29 | _add_swagger_attr_wrapper = swagger.operation(**swagger_kwargs) 30 | swaggered_input = _add_swagger_attr_wrapper(plain_input) 31 | 32 | assert hasattr(swaggered_input, "__swagger_attr") 33 | assert swaggered_input.__swagger_attr == swagger_kwargs 34 | -------------------------------------------------------------------------------- /tests/test_nested_func.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | import pytest 4 | 5 | from flask_restful_swagger.swagger import _Nested, nested 6 | 7 | 8 | class MockClass(object): 9 | pass 10 | 11 | 12 | def test_nested_without_a_class(): 13 | ret = nested(None, kwargs={"arg1": 1, "arg2": "Helllllllo!"}) 14 | assert isinstance(ret, types.FunctionType) 15 | assert ret.__name__ == "wrapper" 16 | 17 | 18 | def test_wrapped_object_is_correct(): 19 | ret = nested(klass=None, kwargs={"arg1": 1, "arg2": "Helllllllo!"}) 20 | resolved = ret(MockClass) 21 | assert isinstance(resolved, _Nested) 22 | assert isinstance(resolved, _Nested) 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "testcase_klass, testcase_kwargs", 27 | [(MockClass, {}), (MockClass, {"arg1": 1, "arg2": "Helllllllo!"})], 28 | ) 29 | def test_nested_with_a_class(testcase_klass, testcase_kwargs): 30 | ret = nested(klass=testcase_klass, **testcase_kwargs) 31 | assert isinstance(ret, _Nested) 32 | assert ret._klass == MockClass 33 | -------------------------------------------------------------------------------- /scripts/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | bypass() { 6 | 7 | # Bypass Unless GitHooks Are Enabled 8 | [[ "${GIT_HOOKS}" == "1" ]] && exit 0 9 | 10 | local_branch="$(git rev-parse --abbrev-ref HEAD)" 11 | protected_branches="${GIT_HOOKS_PROTECTED_BRANCHES}" 12 | 13 | if [[ ! ${local_branch} =~ ${protected_branches} ]]; then 14 | exit 0 15 | fi 16 | 17 | } 18 | 19 | main() { 20 | 21 | bypass 22 | 23 | bash scripts/commander.sh lint-validate 24 | bash scripts/commander.sh sectest 25 | bash scripts/commander.sh test 26 | shellcheck -x scripts/*.sh 27 | shellcheck -x scripts/common/*.sh 28 | 29 | if [[ -n "$(git diff)" ]]; then 30 | 31 | git status 32 | 33 | exec < /dev/tty 34 | echo -e "\nWARNING: You have uncommitted changes!" 35 | read -r -p "Type 'yes' to confirm you wish to proceed with this commit: " confirm 36 | [[ ${confirm} != "yes" ]] && echo 'ABORTED' && exit 127 37 | 38 | exit 0 39 | 40 | fi 41 | 42 | } 43 | 44 | main 45 | -------------------------------------------------------------------------------- /scripts/common/wheel.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # shellcheck disable=SC1117 4 | build_wheel() { 5 | 6 | set -e 7 | 8 | BRed='\033[31m' # Red 9 | BGreen='\033[32m' # Green 10 | NC="\033[0m" # Color Reset 11 | 12 | OWNER="${OWNER}" 13 | REPO="${REPO}" 14 | TAG="${TAG}" 15 | 16 | source_enviroment 17 | pushd "${PROJECT_HOME}" > /dev/null 18 | 19 | if [[ -f .gittoken ]]; then 20 | GITHUB_TOKEN=$(cat .gittoken) 21 | export GITHUB_TOKEN 22 | fi 23 | 24 | rm -rf dist ./*.egg-info build 25 | python setup.py bdist_wheel 26 | mv dist/*.whl . 27 | rm -rf dist ./*.egg-info build 28 | echo -e "\\n${BGreen}Built:${NC} ${BRed}$(ls ./*.whl)${NC}" 29 | 30 | if [[ -n ${GITHUB_TOKEN} ]]; then 31 | ./scripts/common/upload.sh _GITHUB_API_TOKEN="${GITHUB_TOKEN}" _OWNER="${OWNER}" _REPO="${REPO}" _TAG="${TAG}" _FILENAME="$(ls ./*.whl)" 32 | rm ./*.whl 33 | else 34 | echo -e "Set the environment variable ${BRed}GITHUB_TOKEN${NC} to automate the upload to github.\\n" 35 | fi 36 | 37 | popd > /dev/null 38 | 39 | } -------------------------------------------------------------------------------- /tests/test_resource_lister.py: -------------------------------------------------------------------------------- 1 | from flask_restful_swagger.swagger import ResourceLister 2 | 3 | try: 4 | from unittest.mock import patch 5 | except ImportError: 6 | from mock import patch 7 | 8 | 9 | @patch("flask_restful_swagger.swagger.render_page") 10 | @patch("flask_restful_swagger.swagger._get_current_registry") 11 | def test_get_valid_content_renders(registry, render_page): 12 | 13 | expected_result = { 14 | "apiVersion": "mock_version", 15 | "swaggerVersion": "mock_swagger_version", 16 | "apis": [ 17 | { 18 | "path": "mock_pathmock_spec_endpoint_path", 19 | "description": "mock_description", 20 | } 21 | ], 22 | } 23 | 24 | registry.return_value = { 25 | "apiVersion": "mock_version", 26 | "swaggerVersion": "mock_swagger_version", 27 | "basePath": "mock_path", 28 | "spec_endpoint_path": "mock_spec_endpoint_path", 29 | "description": "mock_description", 30 | } 31 | resource_lister = ResourceLister() 32 | assert resource_lister.get() == expected_result 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Ran Tavory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /development/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-slim 2 | # Default Development Version 3 | 4 | MAINTAINER rantav@gmail.com 5 | LABEL PROJECT=flask_restful_swagger 6 | 7 | ENV PYTHONUNBUFFERED 1 8 | 9 | # Mark Container 10 | RUN echo "flask_restful_swagger" > /etc/container_release 11 | 12 | # Install Dependencies 13 | RUN apt-get update && \ 14 | apt-get upgrade -y && \ 15 | apt-get install -y \ 16 | bash \ 17 | build-essential \ 18 | curl \ 19 | jq \ 20 | openssh-client \ 21 | shellcheck \ 22 | sudo \ 23 | tig \ 24 | vim 25 | 26 | # Setup directories 27 | RUN mkdir -p /home/user /app 28 | WORKDIR /app 29 | 30 | # Copy the codebase 31 | COPY . /app 32 | 33 | # Create the runtime user, and change permissions 34 | RUN useradd user -d /home/user \ 35 | -s /bin/bash \ 36 | -M \ 37 | && chown -R user:user /home/user \ 38 | && chown -R user:user /app \ 39 | && echo "user ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers 40 | USER user 41 | 42 | # Setup The Dev CLI 43 | RUN scripts/commander.sh setup 44 | -------------------------------------------------------------------------------- /tests/lib/helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from types import CodeType, FunctionType 3 | from unittest import TestCase 4 | 5 | 6 | def freeVar(val): 7 | def nested(): 8 | return val 9 | 10 | return nested.__closure__[0] 11 | 12 | 13 | def find_nested_func(parent, child_name, **kwargs): 14 | """Returns a function nested inside another function. 15 | :param parent: The parent function to search inside. 16 | :type parent: func 17 | :param child_name: A string containing the name of the child function. 18 | :type child_name: string 19 | :returns: The nested function, or None 20 | """ 21 | if sys.version_info[0] < 3: 22 | consts = parent.func_code.co_consts 23 | else: 24 | consts = parent.__code__.co_consts 25 | for item in consts: 26 | if isinstance(item, CodeType): 27 | if item.co_name == child_name: 28 | return FunctionType( 29 | item, 30 | globals(), 31 | None, 32 | None, 33 | tuple(freeVar(name) for name in item.co_freevars), 34 | ) 35 | return None 36 | 37 | 38 | class TestCaseSupport(TestCase): 39 | def runTest(self): 40 | pass 41 | -------------------------------------------------------------------------------- /.github/workflows/docker27.yml: -------------------------------------------------------------------------------- 1 | name: flask_restful_swagger Python2.7 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Create Docker Mounted Content 12 | run: | 13 | echo | ssh-keygen 14 | touch ${HOME}/.gitconfig 15 | touch ${HOME}/.gitconfig_global 16 | - name: Modify Dockerfile's Python Version 17 | run: | 18 | PYTHON_CONTAINER="python:2.7-slim" 19 | DOCKER_CONTENT=$(tail -n +2 development/Dockerfile) 20 | echo "FROM ${PYTHON_CONTAINER}" > development/Dockerfile 21 | echo "${DOCKER_CONTENT}" >> development/Dockerfile 22 | - name: Ensure File System is Writable by the Container 23 | run: | 24 | sudo chmod -R 777 . 25 | - name: Build Container 26 | run: | 27 | docker-compose build 28 | docker-compose up -d 29 | - name: Run Linter 30 | run: | 31 | docker-compose exec -T flask_restful_swagger bash -l -c 'scripts/commander.sh lint-validate' 32 | - name: Run Sec Test 33 | run: | 34 | docker-compose exec -T flask_restful_swagger bash -l -c 'scripts/commander.sh sectest' 35 | - name: Run Unit Tests 36 | run: | 37 | docker-compose exec -T flask_restful_swagger bash -l -c 'scripts/commander.sh test coverage' 38 | -------------------------------------------------------------------------------- /.github/workflows/docker37.yml: -------------------------------------------------------------------------------- 1 | name: flask_restful_swagger Python3.7 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - name: Create Docker Mounted Content 12 | run: | 13 | echo | ssh-keygen 14 | touch ${HOME}/.gitconfig 15 | touch ${HOME}/.gitconfig_global 16 | - name: Modify Dockerfile's Python Version 17 | run: | 18 | PYTHON_CONTAINER="python:3.7-slim" 19 | DOCKER_CONTENT=$(tail -n +2 development/Dockerfile) 20 | echo "FROM ${PYTHON_CONTAINER}" > development/Dockerfile 21 | echo "${DOCKER_CONTENT}" >> development/Dockerfile 22 | - name: Ensure File System is Writable by the Container 23 | run: | 24 | sudo chmod -R 777 . 25 | - name: Build Container 26 | run: | 27 | docker-compose build 28 | docker-compose up -d 29 | - name: Run Linter 30 | run: | 31 | docker-compose exec -T flask_restful_swagger bash -l -c 'scripts/commander.sh lint-validate' 32 | - name: Run Sec Test 33 | run: | 34 | docker-compose exec -T flask_restful_swagger bash -l -c 'scripts/commander.sh sectest' 35 | - name: Run Unit Tests 36 | run: | 37 | docker-compose exec -T flask_restful_swagger bash -l -c 'scripts/commander.sh test coverage' 38 | -------------------------------------------------------------------------------- /tests/test_extract_swagger_path.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_restful_swagger import swagger 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "case_name, test_input, expected", 8 | [ 9 | ("empty_string", "", ""), 10 | ("simple", "/endpoint", "/endpoint"), 11 | ("single_parameter_no_type", "/path/", "/path/{parameter}"), 12 | ( 13 | "single_parameter_string", 14 | "/", 15 | "/{lang_code}", 16 | ), 17 | ( 18 | "multiple_parameters", 19 | "///", 20 | "/{lang_code}/{id}/{probability}", 21 | ), 22 | ( 23 | "multiple_parameters_varied_length_string", 24 | "///", 25 | "/{lang_code}/{id}/{probability}", 26 | ), 27 | ( 28 | "long_path_single_parameter", 29 | "path/subpath/other_path/", 30 | "path/subpath/other_path/{lang_code}", 31 | ), 32 | ], 33 | ) 34 | def test_extract_swagger_path(case_name, test_input, expected): 35 | assert swagger.extract_swagger_path(test_input) == expected 36 | 37 | 38 | def test_extract_swagger_path_returns_string(): 39 | assert isinstance(swagger.extract_swagger_path("/endpoint/123"), str) 40 | -------------------------------------------------------------------------------- /tests/test_swagger_registry.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from flask_restful_swagger.swagger import SwaggerRegistry 4 | 5 | try: 6 | from unittest.mock import patch 7 | except ImportError: 8 | from mock import patch 9 | 10 | 11 | @patch("flask_restful_swagger.swagger._get_current_registry") 12 | @patch("flask_restful_swagger.swagger.render_homepage") 13 | def test_get_swagger_registry(homepage, registry): 14 | 15 | mock_registry = { 16 | "apiVersion": "mock_version", 17 | "swaggerVersion": "mock_swagger_version", 18 | "basePath": "mock_path", 19 | "spec_endpoint_path": "mock_spec_endpoint_path", 20 | "description": "mock_description", 21 | } 22 | 23 | registry.return_value = mock_registry 24 | 25 | app = Flask(__name__) 26 | 27 | resource = SwaggerRegistry() 28 | bases = [base.__name__ for base in SwaggerRegistry.__mro__] 29 | 30 | assert sorted(bases) == [ 31 | "MethodView", 32 | "Resource", 33 | "SwaggerRegistry", 34 | "View", 35 | "object", 36 | ] 37 | 38 | with app.test_request_context(path="/some_path.html"): 39 | _ = resource.get() 40 | assert homepage.called 41 | homepage.assert_called_once_with( 42 | "mock_pathmock_spec_endpoint_path/_/resource_list.json" 43 | ) 44 | 45 | with app.test_request_context(path="/some_path"): 46 | homepage.reset_mock() 47 | response = resource.get() 48 | assert not homepage.called 49 | assert response == mock_registry 50 | -------------------------------------------------------------------------------- /examples/inheritance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Running: 3 | 4 | PYTHONPATH=. python examples/inheritance.py 5 | 6 | """ 7 | from flask import Flask 8 | from flask_restful import Api, Resource 9 | 10 | from flask_restful_swagger import swagger 11 | 12 | app = Flask(__name__, static_folder="../static") 13 | 14 | ################################### 15 | # This is important: 16 | api = swagger.docs( 17 | Api(app), 18 | apiVersion="0.1", 19 | basePath="http://localhost:5000", 20 | resourcePath="/", 21 | produces=["application/json", "text/html"], 22 | api_spec_url="/api/spec", 23 | description="Inheritance demonstration", 24 | ) 25 | ################################### 26 | 27 | 28 | class Base(Resource): 29 | def get(self): 30 | pass 31 | 32 | def post(self): 33 | pass 34 | 35 | def delete(self): 36 | pass 37 | 38 | 39 | class Inherited(Base): 40 | @swagger.operation( 41 | notes="just testing inheritance", 42 | nickname="get", 43 | parameters=[ 44 | { 45 | "name": "a_bool", 46 | "description": "The ID of the TODO item", 47 | "required": True, 48 | "allowMultiple": False, 49 | "dataType": "boolean", 50 | "paramType": "path", 51 | } 52 | ], 53 | ) 54 | def get(self): 55 | return "hello" 56 | 57 | def post(self): 58 | # wont be visible in the swagger docs 59 | return "world" 60 | 61 | 62 | # 63 | # Actually setup the Api resource routing here 64 | # 65 | api.add_resource(Inherited, "/inherited") 66 | 67 | if __name__ == "__main__": 68 | app.run(host='0.0.0.0', debug=True) 69 | -------------------------------------------------------------------------------- /tests/test_parse_docs.py: -------------------------------------------------------------------------------- 1 | from flask_restful_swagger import swagger 2 | 3 | 4 | class MockBasicObject: 5 | pass 6 | 7 | 8 | def test_parse_doc_no_object_is_none(): 9 | assert swagger._parse_doc(None) == (None, None) 10 | 11 | 12 | def test_parse_doc_no_docs_is_none(): 13 | assert swagger._parse_doc(MockBasicObject()) == (None, None) 14 | 15 | 16 | def test_parse_doc_one_line_doc(): 17 | test_one_line_doc = MockBasicObject() 18 | test_one_line_doc.__doc__ = "Some Text Goes Here" 19 | assert swagger._parse_doc(test_one_line_doc) == ( 20 | "Some Text Goes Here", 21 | None, 22 | ) 23 | 24 | 25 | def test_parse_doc_multi_line_doc(): 26 | test_multi_line_doc = MockBasicObject() 27 | test_multi_line_doc.__doc__ = ( 28 | "Some Text Goes Here \n this is the extra text\n" 29 | "and this is the third line." 30 | ) 31 | extracted = swagger._parse_doc(test_multi_line_doc) 32 | assert extracted[0] == "Some Text Goes Here " 33 | assert ( 34 | extracted[1] 35 | == " this is the extra text
and this is the third line." 36 | ) 37 | 38 | 39 | def test_parse_doc_weird_characters(): 40 | test_weird_characters = MockBasicObject() 41 | test_weird_characters.__doc__ = ( 42 | "Hi, 297agiu(*#&_$ ! \n Oh, the terrible 2908*&%)(#%#" 43 | ) 44 | extracted = swagger._parse_doc(test_weird_characters) 45 | assert extracted[0] == "Hi, 297agiu(*#&_$ ! " 46 | assert extracted[1] == "Oh, the terrible 2908*&%)(#%#" 47 | 48 | 49 | def test_parse_doc_ends_with_newline(): 50 | test_ends_newline = MockBasicObject() 51 | test_ends_newline.__doc__ = "Overview \n Some details \n" 52 | assert swagger._parse_doc(test_ends_newline)[1] == "Some details " 53 | -------------------------------------------------------------------------------- /scripts/commander.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | PROJECT_HOME="$(git rev-parse --show-toplevel)" 6 | PROJECT_NAME="flask_restful_swagger" 7 | export PROJECT_HOME 8 | export PROJECT_NAME 9 | 10 | # shellcheck source=scripts/common/common.sh 11 | source "$( dirname "${BASH_SOURCE[0]}" )/common/common.sh" 12 | 13 | # Optional For Libraries 14 | # shellcheck source=scripts/common/wheel.sh 15 | # source "$( dirname "${BASH_SOURCE[0]}" )/common/wheel.sh" 16 | 17 | # Add Additional Functionality Via Imports Here 18 | 19 | case $1 in 20 | 'lint') 21 | shift 22 | source_enviroment 23 | lint "$@" 24 | ;; 25 | 'lint-validate') 26 | shift 27 | source_enviroment 28 | lint_check "$@" 29 | ;; 30 | 'reinstall-requirements') 31 | shift 32 | source_enviroment 33 | reinstall_requirements "$@" 34 | ;; 35 | 'sectest') 36 | shift 37 | source_enviroment 38 | security "$@" 39 | ;; 40 | 'setup') 41 | shift 42 | setup_bash "$@" 43 | setup_python "$@" 44 | ;; 45 | 'shortlist') 46 | echo "lint lint-validate reinstall-requirements sectest setup test test-coverage" 47 | ;; 48 | 'test') 49 | shift 50 | source_enviroment 51 | unittests "$@" 52 | ;; 53 | 'test-coverage') 54 | shift 55 | source_enviroment 56 | unittests "coverage" "$@" 57 | ;; 58 | *) 59 | echo "Valid Commands:" 60 | echo ' - lint (Run the linter)' 61 | echo ' - lint-validate (Validate linting)' 62 | echo ' - reinstall-requirements (Reinstall Packages' 63 | echo ' - sectest (Run security tests)' 64 | echo ' - setup (Setup/Reset environment)' 65 | echo ' - test (Run pytest)' 66 | echo ' - test-coverage (Run pytest with coverage)' 67 | ;; 68 | 69 | esac 70 | -------------------------------------------------------------------------------- /tests/test_deduce_swagger_type_flat.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask_restful import fields 3 | 4 | from flask_restful_swagger import swagger 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "case_name, test_input, expected", 9 | [ 10 | ("Null", None, None), 11 | ("Simple False", False, "boolean"), 12 | ("Simple True", True, "boolean"), 13 | ("Integer", 1, "integer"), 14 | ("Very large integer", 9223372036854775807, "integer"), 15 | ("Float less than 1", 0.8092, "number"), 16 | ("Float greater than 1", 98763.09, "number"), 17 | ("String", "helloWorld!", "string"), 18 | ], 19 | ) 20 | def test_deduce_swagger_type_flat_instances(case_name, test_input, expected): 21 | assert swagger.deduce_swagger_type_flat(test_input) == expected 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "field_type, expected", 26 | [ 27 | ("Boolean", "boolean"), 28 | ("Integer", "integer"), 29 | ("Arbitrary", "number"), 30 | ("Fixed", "number"), 31 | ("DateTime", "date-time"), 32 | ], 33 | ) 34 | def test_deduce_swagger_type_flat_flask_field(field_type, expected): 35 | new_field = getattr(fields, field_type)() 36 | assert swagger.deduce_swagger_type_flat(new_field) == expected 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "case_name, object_type, expected", 41 | [ 42 | ("Class derived from string", str, "string"), 43 | ("Class derived from integer", int, "integer"), 44 | ("Class derived from float", float, "number"), 45 | ], 46 | ) 47 | def test_deduce_swagger_type_flat_create_new_class( 48 | case_name, object_type, expected 49 | ): 50 | class NewSubClass(object_type): 51 | pass 52 | 53 | new_instance = NewSubClass() 54 | assert swagger.deduce_swagger_type_flat(new_instance) == expected 55 | 56 | 57 | def test_deduce_swagger_type_flat_with_nested_object(): 58 | assert swagger.deduce_swagger_type_flat("anything", "cookies") == "cookies" 59 | 60 | 61 | def test_deduce_swagger_type_flat_with_class(): 62 | class NewSubClass(str): 63 | pass 64 | 65 | assert swagger.deduce_swagger_type_flat(NewSubClass) == "string" 66 | -------------------------------------------------------------------------------- /tests/test_swagger_endpoint_func.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from flask import Flask 3 | from flask_restful import Resource 4 | 5 | from flask_restful_swagger.swagger import swagger_endpoint 6 | 7 | try: 8 | from unittest.mock import patch 9 | except ImportError: 10 | from mock import patch 11 | 12 | 13 | @patch("flask_restful_swagger.swagger._get_current_registry") 14 | def test_get_swagger_endpoint(registry): 15 | registry.return_value = { 16 | "apiVersion": "mock_version", 17 | "swaggerVersion": "mock_swagger_version", 18 | "basePath": "mock_path", 19 | "spec_endpoint_path": "mock_spec_endpoint_path", 20 | "description": "mock_description", 21 | } 22 | 23 | class MockResource(Resource): 24 | def get(self): 25 | return "OK", 200, {"Access-Control-Allow-Origin": "*"} 26 | 27 | app = Flask(__name__) 28 | 29 | resource = swagger_endpoint("some_api", MockResource, "/some_path") 30 | bases = [base.__name__ for base in resource.__mro__] 31 | 32 | assert sorted(bases) == [ 33 | "MethodView", 34 | "Resource", 35 | "SwaggerResource", 36 | "View", 37 | "object", 38 | ] 39 | 40 | with app.test_request_context(path="/some_path.help.json"): 41 | resource_instance = resource() 42 | response = resource_instance.get() 43 | assert sorted(list(response.keys())) == [ 44 | "description", 45 | "notes", 46 | "operations", 47 | "path", 48 | ] 49 | assert response["path"] == "/some_path" 50 | assert response["operations"] == [] 51 | 52 | with app.test_request_context(path="/some_path.help.html"): 53 | resource_instance = resource() 54 | response = resource_instance.get() 55 | assert response.status_code == 200 56 | assert isinstance(response.data, bytes) 57 | assert BeautifulSoup( 58 | response.data.decode("utf-8"), "html.parser" 59 | ).find() 60 | 61 | with app.test_request_context(path="/some_path"): 62 | resource_instance = resource() 63 | assert resource_instance.get() is None 64 | -------------------------------------------------------------------------------- /tests/test_render_page.py: -------------------------------------------------------------------------------- 1 | from flask import Response 2 | 3 | from flask_restful_swagger import swagger 4 | 5 | try: 6 | from unittest.mock import patch, mock_open 7 | except ImportError: 8 | from mock import patch, mock_open 9 | 10 | 11 | @patch("flask_restful_swagger.swagger._get_current_registry") 12 | @patch("flask_restful_swagger.swagger.open", new_callable=mock_open) 13 | def test_render_page(mocked_open, test_reg): 14 | test_reg.return_value = { 15 | "apiVersion": "mock_version", 16 | "swaggerVersion": "mock_swagger_version", 17 | "basePath": "mock_path", 18 | "spec_endpoint_path": "mock_spec_endpoint_path", 19 | "description": "mock_description", 20 | } 21 | 22 | result = swagger.render_page("docs.html", None) 23 | assert isinstance(result, Response) 24 | 25 | 26 | @patch("flask_restful_swagger.swagger._get_current_registry") 27 | @patch("flask_restful_swagger.swagger.open", new_callable=mock_open) 28 | def test_render_page_with_slash(mocked_open, test_reg): 29 | test_reg.return_value = { 30 | "apiVersion": "mock_version", 31 | "swaggerVersion": "mock_swagger_version", 32 | "basePath": "mock_path/", 33 | "spec_endpoint_path": "mock_spec_endpoint_path", 34 | "description": "mock_description", 35 | } 36 | 37 | result_with_trailing_slash = swagger.render_page( 38 | "docs.html", {"some info": "info"} 39 | ) 40 | assert isinstance(result_with_trailing_slash, Response) 41 | 42 | 43 | @patch("flask_restful_swagger.swagger._get_current_registry") 44 | @patch("flask_restful_swagger.swagger.open", new_callable=mock_open) 45 | def test_render_page_in_js(mocked_open, test_reg): 46 | test_reg.return_value = { 47 | "apiVersion": "mock_version", 48 | "swaggerVersion": "mock_swagger_version", 49 | "basePath": "mock_path/", 50 | "spec_endpoint_path": "mock_spec_endpoint_path", 51 | "description": "mock_description", 52 | } 53 | 54 | result_with_js = swagger.render_page("docs.js", {"some info": "info"}) 55 | assert ( 56 | result_with_js.headers["Content-Type"] 57 | == "text/javascript; charset=utf-8" 58 | ) 59 | -------------------------------------------------------------------------------- /tests/test_docs.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | from flask import Blueprint, Flask 4 | from flask_restful import Api, Resource 5 | 6 | from flask_restful_swagger.swagger import docs 7 | 8 | try: 9 | from unittest.mock import patch 10 | except ImportError: 11 | from mock import patch 12 | 13 | docs_kwargs = { 14 | "apiVersion": "an api version", 15 | "basePath": "a basepath", 16 | "resourcePath": "a resource path", 17 | "produces": ["application/json", "text/html"], 18 | "api_spec_url": "an api spec url", 19 | "description": "an Api Description Description", 20 | } 21 | 22 | 23 | def test_docs_simple_instantiate(): 24 | 25 | app = Flask(__name__) 26 | app.config["basePath"] = "/abc/123" 27 | my_blueprint1 = Blueprint("my_blueprint1", __name__) 28 | 29 | api1 = docs(Api(my_blueprint1), **docs_kwargs) 30 | 31 | assert api1.add_resource.__name__ == "add_resource" 32 | assert isinstance(api1.add_resource, types.FunctionType) 33 | 34 | 35 | @patch("flask_restful_swagger.swagger.register_once") 36 | @patch("flask_restful_swagger.swagger.make_class") 37 | @patch("flask_restful_swagger.swagger.swagger_endpoint") 38 | @patch("flask_restful_swagger.swagger.extract_swagger_path") 39 | def test_docs_simple_instantiate_add_resources( 40 | path, endpoint, make_class, register 41 | ): 42 | 43 | my_blueprint1 = Blueprint("my_blueprint1", __name__) 44 | 45 | api1 = docs(Api(my_blueprint1), **docs_kwargs) 46 | 47 | class MockResource(Resource): 48 | def get(self): 49 | return "OK", 200, {"Access-Control-Allow-Origin": "*"} 50 | 51 | make_class.return_value = MockResource 52 | endpoint.return_value = MockResource 53 | path.return_value = "/some/swagger/path" 54 | 55 | api1.add_resource(MockResource, "/some/url") 56 | 57 | # Validate All Mock Calls 58 | 59 | assert register.call_args_list[0][0][0] == api1 60 | assert register.call_args_list[0][0][2:] == ( 61 | "an api version", 62 | "1.2", 63 | "a basepath", 64 | "a resource path", 65 | ["application/json", "text/html"], 66 | "an api spec url", 67 | "an Api Description Description", 68 | ) 69 | assert len(register.call_args_list[0][0]) == 9 70 | 71 | path.assert_called_once_with("/some/url") 72 | assert endpoint.call_args_list[0][0][0] == api1 73 | assert endpoint.call_args_list[0][0][2] == "/some/url" 74 | make_class.assert_called_once_with(MockResource) 75 | -------------------------------------------------------------------------------- /development/bash/.bash_git: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Do Not Modify This File, It's Intended To Be Updated From Time to TIme 4 | # INSTEAD: add additional functionality though the .bash_customize file. 5 | 6 | # ---------------------------------------------------------------- 7 | # Bash Git Support - Show Git Repo Information in Bash Prompt 8 | # ---------------------------------------------------------------- 9 | 10 | env_colors() { 11 | # Normal Colors 12 | Black='\033[30m' # Black 13 | Red='\033[31m' # Red 14 | Green='\033[32m' # Green 15 | Yellow='\033[33m' # Yellow 16 | Blue='\033[34m' # Blue 17 | Purple='\033[35m' # Purple 18 | Cyan='\033[36m' # Cyan 19 | White='\033[37m' # White 20 | 21 | # Bold 22 | BBlack='\033[30m' # Black 23 | BRed='\033[31m' # Red 24 | BGreen='\033[32m' # Green 25 | BYellow='\033[33m' # Yellow 26 | BBlue='\033[34m' # Blue 27 | BPurple='\033[35m' # Purple 28 | BCyan='\033[36m' # Cyan 29 | BWhite='\033[37m' # White 30 | 31 | # Background 32 | On_Black='\033[40m' # Black 33 | On_Red='\033[41m' # Red 34 | On_Green='\033[42m' # Green 35 | On_Yellow='\033[43m' # Yellow 36 | On_Blue='\033[44m' # Blue 37 | On_Purple='\033[45m' # Purple 38 | On_Cyan='\033[46m' # Cyan 39 | On_White='\033[47m' # White 40 | 41 | NC="\033[0m" # Color Reset 42 | } 43 | 44 | find_git_dirty() { 45 | local git_dirty 46 | local status 47 | env_colors 48 | 49 | status=$(git status --porcelain 2> /dev/null) 50 | if [[ "$status" != "" ]]; then 51 | git_dirty="${BRed}*${NC}" 52 | else 53 | git_dirty="" 54 | fi 55 | 56 | echo -en "${git_dirty}" 57 | 58 | } 59 | 60 | find_git_branch() { 61 | env_colors 62 | 63 | local branch 64 | local git_branch 65 | local repository_name 66 | 67 | if branch=$(git rev-parse --abbrev-ref HEAD 2> /dev/null); then 68 | if [[ "$branch" == "HEAD" ]]; then 69 | branch='detached*' 70 | fi 71 | 72 | repository_name=$(git rev-parse --show-toplevel) 73 | repository_name=$(basename "${repository_name}") 74 | 75 | git_branch="[r:${Yellow}${repository_name}${NC}/b:${Cyan}${branch}${NC}]\n" 76 | else 77 | git_branch="" 78 | fi 79 | echo -en "${git_branch}" 80 | } 81 | 82 | git_status() { 83 | find_git_dirty 84 | find_git_branch 85 | } 86 | 87 | PROMPT_COMMAND="git_status; $PROMPT_COMMAND" 88 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Overrides 132 | override.env 133 | 134 | # Pycharm 135 | .idea 136 | 137 | # Archives 138 | .archive 139 | 140 | -------------------------------------------------------------------------------- /development/DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # flask-restful-swagger 2 | 3 | A Swagger Spec Extractor for Flask-Restful 4 | 5 | ## Please Do's! 6 | - Please use commitizen to normalize your commit messages 7 | - Please lint your code before sending pull requests 8 | 9 | ## Development Dependencies 10 | 11 | You'll need to install: 12 | - [Docker](https://www.docker.com/) 13 | - [Docker Compose](https://docs.docker.com/compose/install/) 14 | 15 | ## Setup the Development Environment 16 | 17 | Build the development environment container (this takes a few minutes): 18 | - `docker-compose build` 19 | 20 | Start the environment container: 21 | - `docker-compose up -d` 22 | 23 | Spawn a shell inside the container: 24 | - `./container` 25 | 26 | ## But I Want to Develop in Python2 27 | 28 | Although Python2 is EOL, we still want to support the existing users out there for as long as we can: 29 | 30 | Modify the first line of `development/Dockerfile` to: 31 | - `FROM python:2.7-slim` 32 | 33 | You should now be able to rebuild your container, and restart your development environment. 34 | 35 | 36 | If you're switching back and forth between Python2 and 3, you'll need to wipe the compiled bytecode.
Run this command inside the container: 37 | - `find . -name *.pyc -delete` 38 | 39 | ## Install the Project Packages on your Host Machine: 40 | This is useful for making your IDE aware of what's installed in a venv. 41 | 42 | - `pip install pipenv` 43 | - `source scripts/dev` 44 | - `dev setup` (Installs the requirements.txt in the `assets` folder.) 45 | - `pipenv --venv` (To get the path of the virtual environment for your IDE.) 46 | 47 | ## Environment 48 | The [development.env](./development.env) file can be modified to inject environment variable content into the container. 49 | 50 | You can override the values set in this file by setting shell ENV variables prior to starting the container: 51 | - `export GIT_HOOKS_PROTECTED_BRANCHES='.*'` 52 | - `docker-compose kill` (Kill the current running container.) 53 | - `docker-compose rm` (Remove the stopped container.) 54 | - `docker-compose up -d` (Restart the dev environment, with a new container, containing the override.) 55 | - `./container` 56 | 57 | ## Git Hooks 58 | Git hooks are installed that will enforce linting and unit-testing on the specified branches. 59 | The following environment variables can be used to customize this behavior: 60 | 61 | - `GIT_HOOKS` (Set this value to 1 to enable the pre-commit hook) 62 | - `GIT_HOOKS_PROTECTED_BRANCHES` (Customize this regex to specify the branches that should enforce the Git Hook on commit.) 63 | 64 | ## CLI Reference 65 | The CLI is enabled by default inside the container, and is also available on the host machine.
66 | Run the CLI without arguments to see the complete list of available commands: `$ dev` 67 | -------------------------------------------------------------------------------- /static/docs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | API Spec 5 | 6 | 7 | 8 | 9 | 42 | 57 | 58 | 59 | 60 | 73 | 74 |
 
75 | 76 |
77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /static/css/hightlight.default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original style from softwaremaniacs.org (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | pre code { 8 | display: block; padding: 0.5em; 9 | background: #F0F0F0; 10 | } 11 | 12 | pre code, 13 | pre .subst, 14 | pre .tag .title, 15 | pre .lisp .title, 16 | pre .clojure .built_in, 17 | pre .nginx .title { 18 | color: black; 19 | } 20 | 21 | pre .string, 22 | pre .title, 23 | pre .constant, 24 | pre .parent, 25 | pre .tag .value, 26 | pre .rules .value, 27 | pre .rules .value .number, 28 | pre .preprocessor, 29 | pre .ruby .symbol, 30 | pre .ruby .symbol .string, 31 | pre .aggregate, 32 | pre .template_tag, 33 | pre .django .variable, 34 | pre .smalltalk .class, 35 | pre .addition, 36 | pre .flow, 37 | pre .stream, 38 | pre .bash .variable, 39 | pre .apache .tag, 40 | pre .apache .cbracket, 41 | pre .tex .command, 42 | pre .tex .special, 43 | pre .erlang_repl .function_or_atom, 44 | pre .markdown .header { 45 | color: #800; 46 | } 47 | 48 | pre .comment, 49 | pre .annotation, 50 | pre .template_comment, 51 | pre .diff .header, 52 | pre .chunk, 53 | pre .markdown .blockquote { 54 | color: #888; 55 | } 56 | 57 | pre .number, 58 | pre .date, 59 | pre .regexp, 60 | pre .literal, 61 | pre .smalltalk .symbol, 62 | pre .smalltalk .char, 63 | pre .go .constant, 64 | pre .change, 65 | pre .markdown .bullet, 66 | pre .markdown .link_url { 67 | color: #080; 68 | } 69 | 70 | pre .label, 71 | pre .javadoc, 72 | pre .ruby .string, 73 | pre .decorator, 74 | pre .filter .argument, 75 | pre .localvars, 76 | pre .array, 77 | pre .attr_selector, 78 | pre .important, 79 | pre .pseudo, 80 | pre .pi, 81 | pre .doctype, 82 | pre .deletion, 83 | pre .envvar, 84 | pre .shebang, 85 | pre .apache .sqbracket, 86 | pre .nginx .built_in, 87 | pre .tex .formula, 88 | pre .erlang_repl .reserved, 89 | pre .prompt, 90 | pre .markdown .link_label, 91 | pre .vhdl .attribute, 92 | pre .clojure .attribute, 93 | pre .coffeescript .property { 94 | color: #88F 95 | } 96 | 97 | pre .keyword, 98 | pre .id, 99 | pre .phpdoc, 100 | pre .title, 101 | pre .built_in, 102 | pre .aggregate, 103 | pre .css .tag, 104 | pre .javadoctag, 105 | pre .phpdoc, 106 | pre .yardoctag, 107 | pre .smalltalk .class, 108 | pre .winutils, 109 | pre .bash .variable, 110 | pre .apache .tag, 111 | pre .go .typename, 112 | pre .tex .command, 113 | pre .markdown .strong, 114 | pre .request, 115 | pre .status { 116 | font-weight: bold; 117 | } 118 | 119 | pre .markdown .emphasis { 120 | font-style: italic; 121 | } 122 | 123 | pre .nginx .built_in { 124 | font-weight: normal; 125 | } 126 | 127 | pre .coffeescript .javascript, 128 | pre .javascript .xml, 129 | pre .tex .formula, 130 | pre .xml .javascript, 131 | pre .xml .vbscript, 132 | pre .xml .css, 133 | pre .xml .cdata { 134 | opacity: 0.5; 135 | } 136 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/css/highlight.default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original style from softwaremaniacs.org (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | pre code { 8 | display: block; padding: 0.5em; 9 | background: #F0F0F0; 10 | } 11 | 12 | pre code, 13 | pre .subst, 14 | pre .tag .title, 15 | pre .lisp .title, 16 | pre .clojure .built_in, 17 | pre .nginx .title { 18 | color: black; 19 | } 20 | 21 | pre .string, 22 | pre .title, 23 | pre .constant, 24 | pre .parent, 25 | pre .tag .value, 26 | pre .rules .value, 27 | pre .rules .value .number, 28 | pre .preprocessor, 29 | pre .ruby .symbol, 30 | pre .ruby .symbol .string, 31 | pre .aggregate, 32 | pre .template_tag, 33 | pre .django .variable, 34 | pre .smalltalk .class, 35 | pre .addition, 36 | pre .flow, 37 | pre .stream, 38 | pre .bash .variable, 39 | pre .apache .tag, 40 | pre .apache .cbracket, 41 | pre .tex .command, 42 | pre .tex .special, 43 | pre .erlang_repl .function_or_atom, 44 | pre .markdown .header { 45 | color: #800; 46 | } 47 | 48 | pre .comment, 49 | pre .annotation, 50 | pre .template_comment, 51 | pre .diff .header, 52 | pre .chunk, 53 | pre .markdown .blockquote { 54 | color: #888; 55 | } 56 | 57 | pre .number, 58 | pre .date, 59 | pre .regexp, 60 | pre .literal, 61 | pre .smalltalk .symbol, 62 | pre .smalltalk .char, 63 | pre .go .constant, 64 | pre .change, 65 | pre .markdown .bullet, 66 | pre .markdown .link_url { 67 | color: #080; 68 | } 69 | 70 | pre .label, 71 | pre .javadoc, 72 | pre .ruby .string, 73 | pre .decorator, 74 | pre .filter .argument, 75 | pre .localvars, 76 | pre .array, 77 | pre .attr_selector, 78 | pre .important, 79 | pre .pseudo, 80 | pre .pi, 81 | pre .doctype, 82 | pre .deletion, 83 | pre .envvar, 84 | pre .shebang, 85 | pre .apache .sqbracket, 86 | pre .nginx .built_in, 87 | pre .tex .formula, 88 | pre .erlang_repl .reserved, 89 | pre .prompt, 90 | pre .markdown .link_label, 91 | pre .vhdl .attribute, 92 | pre .clojure .attribute, 93 | pre .coffeescript .property { 94 | color: #88F 95 | } 96 | 97 | pre .keyword, 98 | pre .id, 99 | pre .phpdoc, 100 | pre .title, 101 | pre .built_in, 102 | pre .aggregate, 103 | pre .css .tag, 104 | pre .javadoctag, 105 | pre .phpdoc, 106 | pre .yardoctag, 107 | pre .smalltalk .class, 108 | pre .winutils, 109 | pre .bash .variable, 110 | pre .apache .tag, 111 | pre .go .typename, 112 | pre .tex .command, 113 | pre .markdown .strong, 114 | pre .request, 115 | pre .status { 116 | font-weight: bold; 117 | } 118 | 119 | pre .markdown .emphasis { 120 | font-style: italic; 121 | } 122 | 123 | pre .nginx .built_in { 124 | font-weight: normal; 125 | } 126 | 127 | pre .coffeescript .javascript, 128 | pre .javascript .xml, 129 | pre .tex .formula, 130 | pre .xml .javascript, 131 | pre .xml .vbscript, 132 | pre .xml .css, 133 | pre .xml .cdata { 134 | opacity: 0.5; 135 | } 136 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/css/hightlight.default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original style from softwaremaniacs.org (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | pre code { 8 | display: block; padding: 0.5em; 9 | background: #F0F0F0; 10 | } 11 | 12 | pre code, 13 | pre .subst, 14 | pre .tag .title, 15 | pre .lisp .title, 16 | pre .clojure .built_in, 17 | pre .nginx .title { 18 | color: black; 19 | } 20 | 21 | pre .string, 22 | pre .title, 23 | pre .constant, 24 | pre .parent, 25 | pre .tag .value, 26 | pre .rules .value, 27 | pre .rules .value .number, 28 | pre .preprocessor, 29 | pre .ruby .symbol, 30 | pre .ruby .symbol .string, 31 | pre .aggregate, 32 | pre .template_tag, 33 | pre .django .variable, 34 | pre .smalltalk .class, 35 | pre .addition, 36 | pre .flow, 37 | pre .stream, 38 | pre .bash .variable, 39 | pre .apache .tag, 40 | pre .apache .cbracket, 41 | pre .tex .command, 42 | pre .tex .special, 43 | pre .erlang_repl .function_or_atom, 44 | pre .markdown .header { 45 | color: #800; 46 | } 47 | 48 | pre .comment, 49 | pre .annotation, 50 | pre .template_comment, 51 | pre .diff .header, 52 | pre .chunk, 53 | pre .markdown .blockquote { 54 | color: #888; 55 | } 56 | 57 | pre .number, 58 | pre .date, 59 | pre .regexp, 60 | pre .literal, 61 | pre .smalltalk .symbol, 62 | pre .smalltalk .char, 63 | pre .go .constant, 64 | pre .change, 65 | pre .markdown .bullet, 66 | pre .markdown .link_url { 67 | color: #080; 68 | } 69 | 70 | pre .label, 71 | pre .javadoc, 72 | pre .ruby .string, 73 | pre .decorator, 74 | pre .filter .argument, 75 | pre .localvars, 76 | pre .array, 77 | pre .attr_selector, 78 | pre .important, 79 | pre .pseudo, 80 | pre .pi, 81 | pre .doctype, 82 | pre .deletion, 83 | pre .envvar, 84 | pre .shebang, 85 | pre .apache .sqbracket, 86 | pre .nginx .built_in, 87 | pre .tex .formula, 88 | pre .erlang_repl .reserved, 89 | pre .prompt, 90 | pre .markdown .link_label, 91 | pre .vhdl .attribute, 92 | pre .clojure .attribute, 93 | pre .coffeescript .property { 94 | color: #88F 95 | } 96 | 97 | pre .keyword, 98 | pre .id, 99 | pre .phpdoc, 100 | pre .title, 101 | pre .built_in, 102 | pre .aggregate, 103 | pre .css .tag, 104 | pre .javadoctag, 105 | pre .phpdoc, 106 | pre .yardoctag, 107 | pre .smalltalk .class, 108 | pre .winutils, 109 | pre .bash .variable, 110 | pre .apache .tag, 111 | pre .go .typename, 112 | pre .tex .command, 113 | pre .markdown .strong, 114 | pre .request, 115 | pre .status { 116 | font-weight: bold; 117 | } 118 | 119 | pre .markdown .emphasis { 120 | font-style: italic; 121 | } 122 | 123 | pre .nginx .built_in { 124 | font-weight: normal; 125 | } 126 | 127 | pre .coffeescript .javascript, 128 | pre .javascript .xml, 129 | pre .tex .formula, 130 | pre .xml .javascript, 131 | pre .xml .vbscript, 132 | pre .xml .css, 133 | pre .xml .cdata { 134 | opacity: 0.5; 135 | } 136 | -------------------------------------------------------------------------------- /scripts/common/upload.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # shellcheck disable=SC1117 3 | 4 | # Check dependencies. 5 | set -e 6 | [[ -n "${TRACE}" ]] && set -x 7 | 8 | BRed='\033[31m' # Red 9 | BGreen='\033[32m' # Green 10 | NC="\033[0m" # Color Reset 11 | 12 | _OWNER="" 13 | _REPO="" 14 | _TAG="" 15 | _FILENAME="" 16 | _GITHUB_API_TOKEN="" 17 | _ID="" 18 | 19 | main() { 20 | 21 | # Define variables. 22 | GH_API="https://api.github.com" 23 | GH_REPO="$GH_API/repos/${_OWNER}/${_REPO}" 24 | AUTH="Authorization: token ${_GITHUB_API_TOKEN}" 25 | 26 | # Validate token. 27 | curl -o /dev/null -sH "$AUTH" "${GH_REPO}" || { echo "Error: Invalid repo, token or network issue!"; exit 1; } 28 | 29 | # Delete Existing Release 30 | GH_ASSET="https://api.github.com/repos/${_OWNER}/${_REPO}/releases" 31 | EXISTING_RELEASES="$(curl -s -H "Authorization: token ${_GITHUB_API_TOKEN}" "${GH_ASSET}")" 32 | 33 | if jq -e .[].id <<< "${EXISTING_RELEASES}" > /dev/null; then 34 | for release in $(jq .[].id <<< "${EXISTING_RELEASES}"); do 35 | curl -X DELETE -s -H "Authorization: token ${_GITHUB_API_TOKEN}" "${GH_ASSET}/${release}" > /dev/null 36 | echo -e "${BGreen}Deleted Release:${NC} ${BRed}${release}${NC}" 37 | done 38 | fi 39 | 40 | 41 | # Retag the master branch on latest commit local and remotes 42 | set +e 43 | if git push origin :refs/tags/${_TAG} 2>/dev/null; then 44 | git tag -d ${_TAG} 2>/dev/null 45 | git tag ${_TAG} 2>/dev/null 46 | git push origin --tags 2>/dev/null 47 | fi 48 | 49 | 50 | set -e 51 | 52 | # Create New Release, and Fetch it's ID 53 | _ID=$(curl -s -X POST -H "Authorization: token ${_GITHUB_API_TOKEN}" --data "{ \"tag_name\": \"${_TAG}\" }" "https://api.github.com/repos/${_OWNER}/${_REPO}/releases" | jq -r .id) 54 | echo -e "${BGreen}Created Release:${NC} ${BRed}${_ID}${NC}" 55 | 56 | # Look For Existing Assets and Delete As Necessary 57 | GH_ASSET="https://api.github.com/repos/${_OWNER}/${_REPO}/releases" 58 | EXISTING_ASSET="$(curl -s -H "Authorization: token ${_GITHUB_API_TOKEN}" "${GH_ASSET}")" 59 | 60 | if jq -e .[0].assets[0] <<< "${EXISTING_ASSET}" > /dev/null; then 61 | EXISTING_ASSET="$(jq .[0].assets[0].url -r <<< "${EXISTING_ASSET}")" 62 | curl -s -X DELETE -H "Authorization: token ${_GITHUB_API_TOKEN}" "${EXISTING_ASSET}" 63 | echo -e "${BGreen}Deleted Asset:${NC} ${BRed}${EXISTING_ASSET}${NC}" 64 | fi 65 | 66 | # Upload New Assets 67 | GH_ASSET="https://uploads.github.com/repos/${_OWNER}/${_REPO}/releases/${_ID}/assets?name=$(basename ${_FILENAME})" 68 | ASSET=$(curl -s --data-binary @"${_FILENAME}" -H "Authorization: token ${_GITHUB_API_TOKEN}" -H "Content-Type: application/octet-stream" "${GH_ASSET}" | jq .url -r) 69 | 70 | # Pretty Print Result 71 | echo -e "${BGreen}Uploaded:${NC} ${BRed}${ASSET}${NC}" 72 | } 73 | 74 | parse_args() { 75 | 76 | for LINE in "$@"; do 77 | eval "${LINE}" 78 | done 79 | 80 | } 81 | 82 | parse_args "$@" 83 | main 84 | -------------------------------------------------------------------------------- /tests/test_extract_path_arguments.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_restful_swagger.swagger import extract_path_arguments 4 | from .lib.helpers import find_nested_func 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "path,expected", 9 | [ 10 | ("/path/with/no/parameters", []), 11 | ( 12 | "/path/", 13 | [ 14 | { 15 | "name": "parameter", 16 | "dataType": "string", 17 | "paramType": "path", 18 | }, 19 | ], 20 | ), 21 | ( 22 | ( 23 | "///" 24 | "" 25 | ), 26 | [ 27 | { 28 | "name": "lang_code", 29 | "dataType": "string", 30 | "paramType": "path", 31 | }, 32 | { 33 | "name": "identifier", 34 | "dataType": "string", 35 | "paramType": "path", 36 | }, 37 | { 38 | "name": "probability", 39 | "dataType": "float", 40 | "paramType": "path", 41 | }, 42 | ], 43 | ), 44 | ( 45 | ( 46 | "///" 47 | "" 48 | ), 49 | [ 50 | { 51 | "name": "lang_code", 52 | "dataType": "string", 53 | "paramType": "path", 54 | }, 55 | { 56 | "name": "identifier", 57 | "dataType": "float", 58 | "paramType": "path", 59 | }, 60 | { 61 | "name": "ready_to_proceed", 62 | "dataType": "bool", 63 | "paramType": "path", 64 | }, 65 | ], 66 | ), 67 | ], 68 | ) 69 | def test_extract_path(path, expected): 70 | assert extract_path_arguments(path) == expected 71 | 72 | 73 | @pytest.mark.parametrize( 74 | "arg,expected", 75 | [ 76 | ( 77 | "not_declared", 78 | { 79 | "name": "not_declared", 80 | "dataType": "string", 81 | "paramType": "path", 82 | }, 83 | ), 84 | ( 85 | "int:identifier", 86 | {"name": "identifier", "dataType": "int", "paramType": "path"}, 87 | ), 88 | ( 89 | "float:amount", 90 | {"name": "amount", "dataType": "float", "paramType": "path"}, 91 | ), 92 | ], 93 | ) 94 | def test_nested_split_args(arg, expected): 95 | split_arg = find_nested_func(extract_path_arguments, "split_arg") 96 | assert split_arg(arg) == expected 97 | -------------------------------------------------------------------------------- /tests/test_deduce_swagger_type.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | from flask_restful import fields 5 | 6 | from flask_restful_swagger import swagger 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "case_name, test_input, expected", 11 | [ 12 | ("Null", None, {"type": "null"}), 13 | ("Simple False", False, {"type": "boolean"}), 14 | ("Simple True", True, {"type": "boolean"}), 15 | ("Integer", 1, {"type": "integer"}), 16 | ("Very large integer", sys.maxsize, {"type": "integer"}), 17 | ("Float less than 1", 0.8092, {"type": "number"}), 18 | ("Float greater than 1", 98763.09, {"type": "number"}), 19 | ("String", "helloWorld!", {"type": "string"}), 20 | ], 21 | ) 22 | def test_deduce_swagger_type_instances(case_name, test_input, expected): 23 | assert swagger.deduce_swagger_type(test_input) == expected 24 | 25 | 26 | @pytest.mark.parametrize( 27 | "field_type, expected", 28 | [ 29 | ("Boolean", {"type": "boolean"}), 30 | ("Integer", {"type": "integer"}), 31 | ("Arbitrary", {"type": "number"}), 32 | ("Fixed", {"type": "number"}), 33 | ("DateTime", {"type": "date-time"}), 34 | ], 35 | ) 36 | def test_deduce_swagger_type_flask_field(field_type, expected): 37 | new_field = getattr(fields, field_type)() 38 | assert swagger.deduce_swagger_type(new_field) == expected 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "case_name, object_type, expected", 43 | [ 44 | ("Class derived from string", str, {"type": "string"}), 45 | ("Class derived from integer", int, {"type": "integer"}), 46 | ("Class derived from float", float, {"type": "number"}), 47 | ], 48 | ) 49 | def test_deduce_swagger_type_create_new_class( 50 | case_name, object_type, expected): 51 | class NewSubClass(object_type): 52 | pass 53 | 54 | new_instance = NewSubClass() 55 | assert swagger.deduce_swagger_type(new_instance) == expected 56 | 57 | 58 | @pytest.mark.parametrize( 59 | "case_name, object_type, expected", 60 | [ 61 | ("Class derived from string", str, {"type": "string"}), 62 | ("Class derived from integer", int, {"type": "integer"}), 63 | ("Class derived from float", float, {"type": "number"}), 64 | ("Class derived from fields.List", fields.List, {"type": "array"}), 65 | ], 66 | ) 67 | def test_deduce_swagger_type_with_class(case_name, object_type, expected): 68 | class NewSubClass(object_type): 69 | pass 70 | 71 | assert swagger.deduce_swagger_type(NewSubClass) == expected 72 | 73 | 74 | def test_deduce_swagger_type_fields_formatted_string(): 75 | new_instance = fields.FormattedString("Hello {name}") 76 | 77 | assert swagger.deduce_swagger_type(new_instance) == {"type": "string"} 78 | 79 | 80 | def test_deduce_swagger_type_fields_list_instance(): 81 | new_instance = fields.List(fields.String) 82 | 83 | assert "items" in swagger.deduce_swagger_type(new_instance) 84 | 85 | 86 | def test_deduce_swagger_type_fields_nested_instance(): 87 | new_instance = fields.Nested({}) 88 | 89 | assert swagger.deduce_swagger_type(new_instance) == {"type": None} 90 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at niall@niallbyrne.ca. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /scripts/common/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Do Not Modify This File, It's Intended To Be Updated From Time to TIme 4 | # INSTEAD: add additional functionality by adding separate library files 5 | # Import your new libraries into the commander.sh script and add them to the CLI. 6 | 7 | lint() { 8 | 9 | set -e 10 | 11 | pushd "${PROJECT_HOME}" > /dev/null 12 | yapf -i --recursive --style=pep8 "${PROJECT_NAME}/" 13 | isort -y 14 | popd > /dev/null 15 | 16 | lint_check 17 | 18 | } 19 | 20 | lint_check() { 21 | 22 | set -e 23 | 24 | pushd "${PROJECT_HOME}" > /dev/null 25 | isort -c 26 | flake8 27 | shellcheck -x scripts/*.sh 28 | shellcheck -x scripts/common/*.sh 29 | popd > /dev/null 30 | 31 | } 32 | 33 | reinstall_requirements() { 34 | 35 | set -e 36 | 37 | pushd "${PROJECT_HOME}" > /dev/null 38 | pip install -r assets/requirements.txt --no-warn-script-location 39 | pip install -r assets/requirements-dev.txt --no-warn-script-location 40 | popd > /dev/null 41 | 42 | } 43 | 44 | 45 | security() { 46 | 47 | set -e 48 | 49 | pushd "${PROJECT_HOME}" > /dev/null 50 | bandit -r "${PROJECT_NAME}" -c .bandit.rc 51 | safety check 52 | popd > /dev/null 53 | 54 | } 55 | 56 | setup_bash() { 57 | 58 | [[ ! -f /etc/container_release ]] && return 59 | 60 | for filename in /app/development/bash/.bash*; do 61 | echo "Symlinking ${filename} ..." 62 | ln -sf "${filename}" "/home/user/$(basename "${filename}")" 63 | done 64 | 65 | } 66 | 67 | setup_python() { 68 | 69 | unvirtualize 70 | 71 | pushd "${PROJECT_HOME}" > /dev/null 72 | if [[ ! -f /etc/container_release ]]; then 73 | set +e 74 | pipenv --rm 75 | set -e 76 | pipenv --python 3.7 77 | fi 78 | source_enviroment 79 | reinstall_requirements 80 | unvirtualize 81 | popd > /dev/null 82 | 83 | } 84 | 85 | source_enviroment() { 86 | 87 | if [[ ! -f /etc/container_release ]]; then 88 | 89 | unvirtualize 90 | 91 | # shellcheck disable=SC1090 92 | source "$(pipenv --venv)/bin/activate" 93 | 94 | fi 95 | 96 | pushd "${PROJECT_HOME}" > /dev/null 97 | set +e 98 | cd .git/hooks 99 | ln -sf ../../scripts/hooks/pre-commit pre-commit 100 | set -e 101 | popd > /dev/null 102 | 103 | } 104 | 105 | unittests() { 106 | 107 | set -e 108 | 109 | pushd "${PROJECT_HOME}" > /dev/null 110 | if [[ $1 == "coverage" ]]; then 111 | shift 112 | set +e 113 | pytest --cov=. --cov-fail-under=100 "$@" 114 | exit_code="$?" 115 | coverage html 116 | set -e 117 | exit "${exit_code}" 118 | else 119 | pytest "$@" 120 | fi 121 | popd > /dev/null 122 | 123 | } 124 | 125 | unvirtualize() { 126 | 127 | if [[ ! -f /etc/container_release ]]; then 128 | 129 | toggle=1 130 | 131 | if [[ -n "${-//[^e]/}" ]]; then set +e; else toggle=0; fi 132 | if python -c 'import sys; sys.exit(0 if hasattr(sys, "real_prefix") else 1)'; then 133 | deactivate_present=$(LC_ALL=C type deactivate 2>/dev/null) 134 | if [[ -n ${deactivate_present} ]]; then 135 | deactivate 136 | else 137 | exit 138 | fi 139 | fi 140 | if [[ "${toggle}" == "1" ]]; then set -e; fi 141 | 142 | fi 143 | 144 | } 145 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/endpoint.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Api Docs for {{path}} 7 | 8 | 11 | 12 | 13 | 33 |
34 |
35 |
36 |

{{path}}

37 |

{{description if description != None}}

38 |
39 |
40 | {% for operation in operations %} 41 |
42 |
43 |

{{operation.method}}

44 |

{{operation.summary if operation.summary != None}}

45 |
46 |
47 | {% if operation.parameters %} 48 |

Parameters

49 |
50 | {% for parameter in operation.parameters %} 51 |
52 | {{parameter.name}} 53 | {% if parameter.description %} 54 | - {{parameter.description}} 55 | {% endif %} 56 |
57 |
Type: {{parameter.dataType}}
58 |
Allow Multiple: {{parameter.allowMultiple}}
59 |
Required: {{parameter.required}}
60 | {% endfor %} 61 |
62 | {% endif %} 63 | {% if operation.notes %} 64 |

Implementation notes: {{operation.notes}}

65 | {% endif %} 66 | {% if operation.responseClass %} 67 |

Response Class: {{operation.responseClass}}

68 | {% endif %} 69 |
70 |
71 | {% endfor %} 72 |
73 |
74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 62 | 63 | 64 | 65 | 81 | 82 |
 
83 |
84 | 85 | 86 | -------------------------------------------------------------------------------- /tests/test_merge_parameter_list.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_restful_swagger import swagger 4 | 5 | 6 | def test_merge_parameter_list_empty_lists(): 7 | assert swagger.merge_parameter_list([], []) == [] 8 | 9 | 10 | def test_merge_parameter_list_no_changes(): 11 | base = [ 12 | { 13 | "method": "ABC", 14 | "parameters": "None", 15 | "nickname": "ABC", 16 | "name": "ABC", 17 | } 18 | ] 19 | overrides = [] 20 | assert swagger.merge_parameter_list(base, overrides) == base 21 | 22 | 23 | @pytest.mark.parametrize( 24 | "overrides, expected", 25 | [ 26 | [ 27 | ({"parameters": "None", "name": "ABC"}), 28 | ({"parameters": "None", "name": "ABC"}), 29 | ], 30 | [ 31 | ( 32 | { 33 | "method": "GET", 34 | "parameters": "Parameters", 35 | "nickname": "ABC", 36 | "name": "ABC", 37 | } 38 | ), 39 | ( 40 | { 41 | "method": "GET", 42 | "parameters": "Parameters", 43 | "nickname": "ABC", 44 | "name": "ABC", 45 | } 46 | ), 47 | ], 48 | [ 49 | ({"extra_parameter": "something", "name": "ABC"}), 50 | ({"extra_parameter": "something", "name": "ABC"}), 51 | ], 52 | [ 53 | ({"extra_parameter": "something", "name": "ABC"}), 54 | ({"extra_parameter": "something", "name": "ABC"}), 55 | ], 56 | ], 57 | ) 58 | def test_merge_parameter_list_with_changes(overrides, expected): 59 | base = [ 60 | { 61 | "method": "ABC", 62 | "parameters": "Some", 63 | "nickname": "ABC", 64 | "name": "ABC", 65 | } 66 | ] 67 | assert swagger.merge_parameter_list(base, [overrides]) == [expected] 68 | 69 | 70 | @pytest.mark.parametrize( 71 | "overrides, expected", 72 | [ 73 | [ 74 | [ 75 | { 76 | "method": "ABCD", 77 | "parameters": "Some", 78 | "nickname": "ABCD", 79 | "name": "ABCD", 80 | }, 81 | { 82 | "method": "ABC", 83 | "parameters": "Some", 84 | "nickname": "ABC", 85 | "name": "ABC", 86 | }, 87 | ], 88 | [ 89 | { 90 | "method": "ABC", 91 | "parameters": "Some", 92 | "nickname": "ABC", 93 | "name": "ABC", 94 | }, 95 | { 96 | "method": "ABCDE", 97 | "parameters": "Some", 98 | "nickname": "ABCDE", 99 | "name": "ABCDE", 100 | }, 101 | { 102 | "method": "ABCD", 103 | "parameters": "Some", 104 | "nickname": "ABCD", 105 | "name": "ABCD", 106 | }, 107 | ], 108 | ], 109 | ], 110 | ) 111 | def test_merge_parameter_list_appended(overrides, expected): 112 | base = [ 113 | { 114 | "method": "ABC", 115 | "parameters": "Some", 116 | "nickname": "ABC", 117 | "name": "ABC", 118 | }, 119 | { 120 | "method": "ABCDE", 121 | "parameters": "Some", 122 | "nickname": "ABCDE", 123 | "name": "ABCDE", 124 | }, 125 | ] 126 | assert swagger.merge_parameter_list(base, overrides) == expected 127 | -------------------------------------------------------------------------------- /static/js/jquery.ba-bbq.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010 3 | * http://benalman.com/projects/jquery-bbq-plugin/ 4 | * 5 | * Copyright (c) 2010 "Cowboy" Ben Alman 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://benalman.com/about/license/ 8 | */ 9 | (function($,p){var i,m=Array.prototype.slice,r=decodeURIComponent,a=$.param,c,l,v,b=$.bbq=$.bbq||{},q,u,j,e=$.event.special,d="hashchange",A="querystring",D="fragment",y="elemUrlAttr",g="location",k="href",t="src",x=/^.*\?|#.*$/g,w=/^.*\#/,h,C={};function E(F){return typeof F==="string"}function B(G){var F=m.call(arguments,1);return function(){return G.apply(this,F.concat(m.call(arguments)))}}function n(F){return F.replace(/^[^#]*#?(.*)$/,"$1")}function o(F){return F.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(H,M,F,I,G){var O,L,K,N,J;if(I!==i){K=F.match(H?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);J=K[3]||"";if(G===2&&E(I)){L=I.replace(H?w:x,"")}else{N=l(K[2]);I=E(I)?l[H?D:A](I):I;L=G===2?I:G===1?$.extend({},I,N):$.extend({},N,I);L=a(L);if(H){L=L.replace(h,r)}}O=K[1]+(H?"#":L||!K[1]?"?":"")+L+J}else{O=M(F!==i?F:p[g][k])}return O}a[A]=B(f,0,o);a[D]=c=B(f,1,n);c.noEscape=function(G){G=G||"";var F=$.map(G.split(""),encodeURIComponent);h=new RegExp(F.join("|"),"g")};c.noEscape(",/");$.deparam=l=function(I,F){var H={},G={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(L,Q){var K=Q.split("="),P=r(K[0]),J,O=H,M=0,R=P.split("]["),N=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[N])){R[N]=R[N].replace(/\]$/,"");R=R.shift().split("[").concat(R);N=R.length-1}else{N=0}if(K.length===2){J=r(K[1]);if(F){J=J&&!isNaN(J)?+J:J==="undefined"?i:G[J]!==i?G[J]:J}if(N){for(;M<=N;M++){P=R[M]===""?O.length:R[M];O=O[P]=M').hide().insertAfter("body")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash="#"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,"")+"#"+u}}r=setTimeout(s,$[d+"Delay"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,this); -------------------------------------------------------------------------------- /flask_restful_swagger/static/lib/jquery.ba-bbq.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010 3 | * http://benalman.com/projects/jquery-bbq-plugin/ 4 | * 5 | * Copyright (c) 2010 "Cowboy" Ben Alman 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://benalman.com/about/license/ 8 | */ 9 | (function($,p){var i,m=Array.prototype.slice,r=decodeURIComponent,a=$.param,c,l,v,b=$.bbq=$.bbq||{},q,u,j,e=$.event.special,d="hashchange",A="querystring",D="fragment",y="elemUrlAttr",g="location",k="href",t="src",x=/^.*\?|#.*$/g,w=/^.*\#/,h,C={};function E(F){return typeof F==="string"}function B(G){var F=m.call(arguments,1);return function(){return G.apply(this,F.concat(m.call(arguments)))}}function n(F){return F.replace(/^[^#]*#?(.*)$/,"$1")}function o(F){return F.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(H,M,F,I,G){var O,L,K,N,J;if(I!==i){K=F.match(H?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);J=K[3]||"";if(G===2&&E(I)){L=I.replace(H?w:x,"")}else{N=l(K[2]);I=E(I)?l[H?D:A](I):I;L=G===2?I:G===1?$.extend({},I,N):$.extend({},N,I);L=a(L);if(H){L=L.replace(h,r)}}O=K[1]+(H?"#":L||!K[1]?"?":"")+L+J}else{O=M(F!==i?F:p[g][k])}return O}a[A]=B(f,0,o);a[D]=c=B(f,1,n);c.noEscape=function(G){G=G||"";var F=$.map(G.split(""),encodeURIComponent);h=new RegExp(F.join("|"),"g")};c.noEscape(",/");$.deparam=l=function(I,F){var H={},G={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(L,Q){var K=Q.split("="),P=r(K[0]),J,O=H,M=0,R=P.split("]["),N=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[N])){R[N]=R[N].replace(/\]$/,"");R=R.shift().split("[").concat(R);N=R.length-1}else{N=0}if(K.length===2){J=r(K[1]);if(F){J=J&&!isNaN(J)?+J:J==="undefined"?i:G[J]!==i?G[J]:J}if(N){for(;M<=N;M++){P=R[M]===""?O.length:R[M];O=O[P]=M').hide().insertAfter("body")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash="#"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,"")+"#"+u}}r=setTimeout(s,$[d+"Delay"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,this); -------------------------------------------------------------------------------- /tests/test_get_current_registry.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from flask import Blueprint, Flask 4 | from flask_restful import Api, Resource 5 | 6 | from flask_restful_swagger import swagger 7 | from flask_restful_swagger.swagger import _get_current_registry 8 | from .lib.helpers import TestCaseSupport 9 | 10 | tc = TestCaseSupport() 11 | tc.maxDiff = None 12 | 13 | 14 | def test_get_current_registry_simple(): 15 | app = Flask(__name__) 16 | 17 | with app.test_request_context(path="/some_path.html"): 18 | registry = _get_current_registry() 19 | 20 | assert registry == {"basePath": "http://localhost", "models": {}} 21 | 22 | 23 | def test_get_current_registry_request_features(): 24 | 25 | app = Flask(__name__) 26 | app.config["basePath"] = "/abc/123" 27 | my_blueprint1 = Blueprint("my_blueprint1", __name__) 28 | api1 = swagger.docs( 29 | Api(my_blueprint1), 30 | apiVersion="0.1", 31 | basePath="http://localhost:5000", 32 | resourcePath="/", 33 | produces=["application/json", "text/html"], 34 | api_spec_url="/api/spec", 35 | description="Blueprint1 Description", 36 | ) 37 | 38 | class MockResource(Resource): 39 | @swagger.operation() 40 | def get(self): 41 | return "OK", 200, {"Access-Control-Allow-Origin": "*"} 42 | 43 | app.register_blueprint(my_blueprint1, url_prefix="") 44 | api1.add_resource(MockResource, "/some/urls") 45 | 46 | with app.test_request_context(path="some_path.html"): 47 | registry = _get_current_registry(api=api1) 48 | 49 | description = ( 50 | "Represents an abstract RESTful resource. " "Concrete resources should" 51 | ) 52 | notes = ( 53 | "extend from this class and expose methods for each " 54 | "supported HTTP
method. If a resource is invoked " 55 | "with an unsupported HTTP method,
the API will " 56 | "return a response with status 405 Method Not Allowed." 57 | "
Otherwise the appropriate method is called and " 58 | "passed all arguments
from the url rule used when " 59 | "adding the resource to an Api instance. " 60 | "See
:meth:`~flask_restful.Api.add_resource` " 61 | "for details." 62 | ) 63 | 64 | if sys.version_info[0] < 3: 65 | notes = None 66 | description = None 67 | 68 | tc.assertDictEqual( 69 | registry, 70 | { 71 | "apiVersion": "0.1", 72 | "swaggerVersion": "1.2", 73 | "basePath": "http://localhost:5000", 74 | "spec_endpoint_path": "/api/spec", 75 | "resourcePath": "/", 76 | "produces": ["application/json", "text/html"], 77 | "x-api-prefix": "", 78 | "apis": [ 79 | { 80 | "path": "/some/urls", 81 | "description": description, 82 | "notes": notes, 83 | "operations": [ 84 | { 85 | "method": "get", 86 | "nickname": "nickname", 87 | "notes": None, 88 | "parameters": [], 89 | "summary": None, 90 | } 91 | ], 92 | } 93 | ], 94 | "description": "Blueprint1 Description", 95 | "models": {}, 96 | }, 97 | ) 98 | 99 | 100 | def test_get_current_registry_request_features_and_docs(): 101 | app = Flask(__name__) 102 | app.config["basePath"] = "/abc/123" 103 | my_blueprint1 = Blueprint("my_blueprint1", __name__) 104 | app.register_blueprint(my_blueprint1, url_prefix="") 105 | _ = swagger.docs( 106 | Api(my_blueprint1), 107 | apiVersion="0.1", 108 | basePath="http://localhost:5000", 109 | resourcePath="/", 110 | produces=["application/json", "text/html"], 111 | api_spec_url="/api/spec", 112 | description="Blueprint1 Description", 113 | ) 114 | 115 | with app.test_request_context(path="some_path.html"): 116 | registry = _get_current_registry() 117 | 118 | tc.assertDictEqual( 119 | registry, 120 | {"basePath": "http://localhost", "models": {}} 121 | ) 122 | -------------------------------------------------------------------------------- /tests/test_staticfiles.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | import flask_restful_swagger 6 | from flask_restful_swagger.swagger import StaticFiles 7 | 8 | try: 9 | from unittest.mock import patch 10 | except ImportError: 11 | from mock import patch 12 | 13 | 14 | test_fixtures_renders = [ 15 | ["index.html", None, None], 16 | ["o2c.html", None, None], 17 | ["swagger-ui.js", None, None], 18 | ["swagger-ui.min.js", None, None], 19 | ["lib/swagger-oauth.js", None, None], 20 | ] 21 | 22 | 23 | @patch("flask_restful_swagger.swagger.render_page") 24 | @patch("flask_restful_swagger.swagger._get_current_registry") 25 | @pytest.mark.parametrize("dir1,dir2,dir3", test_fixtures_renders) 26 | def test_get_valid_content_renders(registry, render_page, dir1, dir2, dir3): 27 | 28 | static_files = StaticFiles() 29 | registry.return_value = {"spec_endpoint_path": "dummy"} 30 | 31 | static_files.get(dir1, dir2, dir3) 32 | assert render_page.call_args[0] == (dir1, {"resource_list_url": "dummy"}) 33 | 34 | 35 | test_fixtures_none = [[None, None, None]] 36 | 37 | 38 | @patch("flask_restful_swagger.swagger.render_page") 39 | @patch("flask_restful_swagger.swagger._get_current_registry") 40 | @pytest.mark.parametrize("dir1,dir2,dir3", test_fixtures_none) 41 | def test_get_valid_content_renders_none( 42 | registry, render_page, dir1, dir2, dir3 43 | ): 44 | 45 | static_files = StaticFiles() 46 | registry.return_value = {"spec_endpoint_path": "dummy"} 47 | 48 | static_files.get(dir1, dir2, dir3) 49 | assert render_page.call_args[0] == ( 50 | "index.html", 51 | {"resource_list_url": "dummy"}, 52 | ) 53 | 54 | 55 | test_fixtures_mimes = [ 56 | ["index2.html", "text/plain"], 57 | ["image.gif", "image/gif"], 58 | ["image.png", "image/png"], 59 | ["javascript.js", "text/javascript"], 60 | ["style.css", "text/css"], 61 | ] 62 | 63 | 64 | @patch("flask_restful_swagger.swagger.Response", autospec=True) 65 | @patch("flask_restful_swagger.swagger.open") 66 | @patch("flask_restful_swagger.swagger.os.path.exists") 67 | @patch("flask_restful_swagger.swagger._get_current_registry") 68 | @pytest.mark.parametrize("dir1,mime", test_fixtures_mimes) 69 | def test_get_valid_content_mime( 70 | registry, mock_exists, mock_open, response, dir1, mime 71 | ): 72 | 73 | mock_open.return_value = "file_handle" 74 | mock_exists.return_value = True 75 | 76 | static_files = StaticFiles() 77 | static_files.get(dir1, None, None) 78 | assert mock_exists.called 79 | assert mock_open.called 80 | 81 | args, kwargs = response.call_args_list[0] 82 | assert args == ("file_handle",) 83 | assert kwargs == {"mimetype": mime} 84 | 85 | 86 | test_fixtures_mimes_does_not_exist = ["index2.html"] 87 | 88 | 89 | @patch("flask_restful_swagger.swagger.os.path.exists") 90 | @patch("flask_restful_swagger.swagger._get_current_registry") 91 | @patch("flask_restful_swagger.swagger.abort") 92 | @pytest.mark.parametrize("dir1", test_fixtures_mimes_does_not_exist) 93 | def test_get_valid_content_mime_file_does_not_exist( 94 | abort, registry, mock_exists, dir1 95 | ): 96 | 97 | mock_exists.return_value = False 98 | static_files = StaticFiles() 99 | static_files.get(dir1, None, None) 100 | assert mock_exists.called 101 | assert abort.called 102 | 103 | 104 | test_fixtures_paths = [ 105 | ["paths", "index2.html", None, "paths/index2.html"], 106 | ["paths", "more_paths", "index2.html", "paths/more_paths/index2.html"], 107 | ] 108 | 109 | 110 | @patch("flask_restful_swagger.swagger.Response", autospec=True) 111 | @patch("flask_restful_swagger.swagger.os.path.exists") 112 | @patch("flask_restful_swagger.swagger.open") 113 | @patch("flask_restful_swagger.swagger.render_page") 114 | @patch("flask_restful_swagger.swagger._get_current_registry") 115 | @pytest.mark.parametrize("dir1,dir2,dir3,expected", test_fixtures_paths) 116 | def test_get_valid_content_paths( 117 | registry, 118 | render_page, 119 | mock_open, 120 | mock_exists, 121 | response, 122 | dir1, 123 | dir2, 124 | dir3, 125 | expected, 126 | ): 127 | 128 | mock_open.return_value = "file_handle" 129 | mock_exists.return_value = True 130 | 131 | static_files = StaticFiles() 132 | registry.return_value = {"spec_endpoint_path": "dummy"} 133 | 134 | static_files.get(dir1, dir2, dir3) 135 | module_path = os.path.dirname(flask_restful_swagger.__file__) 136 | static_files = "static" 137 | full_path = os.path.join(module_path, static_files, expected) 138 | 139 | assert mock_exists.called 140 | assert mock_open.call_args_list[0][0][0] == full_path 141 | 142 | args, kwargs = response.call_args_list[0] 143 | assert args == ("file_handle",) 144 | assert kwargs == {"mimetype": "text/plain"} 145 | -------------------------------------------------------------------------------- /tests/test_register_once.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask_restful import Api 3 | 4 | import flask_restful_swagger 5 | from .lib.helpers import find_nested_func 6 | 7 | 8 | class MockRegistration: 9 | def __init__(self): 10 | self.args = [] 11 | self.kwargs = [] 12 | 13 | def reset(self): 14 | self.args = [] 15 | self.kwargs = [] 16 | 17 | def add(self, *args, **kwargs): 18 | self.args.append(args) 19 | self.kwargs.append(kwargs) 20 | 21 | 22 | class MockState(object): 23 | def __init__(self, blueprint=None, url_prefix=None): 24 | self.blueprint = blueprint 25 | self.url_prefix = url_prefix 26 | 27 | 28 | mock_registration = MockRegistration() 29 | 30 | 31 | register_once_kwargs = { 32 | "add_resource_func": mock_registration.add, 33 | "apiVersion": "0.1", 34 | "swaggerVersion": "1.2", 35 | "basePath": "https://localhost:5000", 36 | "resourcePath": "/api/spec", 37 | "produces": ["application/json"], 38 | "endpoint_path": "/endpoint", 39 | "description": "Mock API Registry Data", 40 | } 41 | 42 | registry_content_for_test = { 43 | "apiVersion": "0.1", 44 | "swaggerVersion": "1.2", 45 | "basePath": "https://localhost:5000", 46 | "spec_endpoint_path": "/endpoint", 47 | "resourcePath": "/api/spec", 48 | "produces": ["application/json"], 49 | "x-api-prefix": "", 50 | "apis": [], 51 | "description": "Mock API Registry Data", 52 | } 53 | 54 | 55 | def test_register_once_blueprint(): 56 | 57 | mock_registration.reset() 58 | 59 | if "app" in flask_restful_swagger.registry: 60 | del flask_restful_swagger.registry["app"] 61 | 62 | my_blueprint1 = Blueprint("test", __name__) 63 | api = Api(my_blueprint1) 64 | 65 | kwargs = dict(register_once_kwargs) 66 | kwargs["api"] = api 67 | 68 | flask_restful_swagger.swagger.register_once(**kwargs) 69 | assert flask_restful_swagger.registry["test"] == registry_content_for_test 70 | flask_restful_swagger.swagger.register_once(**kwargs) 71 | assert flask_restful_swagger.registry["test"] == registry_content_for_test 72 | 73 | assert mock_registration.args == [ 74 | ( 75 | flask_restful_swagger.swagger.SwaggerRegistry, 76 | "/endpoint", 77 | "/endpoint.json", 78 | "/endpoint.html", 79 | ), 80 | ( 81 | flask_restful_swagger.swagger.ResourceLister, 82 | "/endpoint/_/resource_list.json", 83 | ), 84 | ( 85 | flask_restful_swagger.swagger.StaticFiles, 86 | "/endpoint/_/static///", 87 | "/endpoint/_/static//", 88 | "/endpoint/_/static/", 89 | ), 90 | ( 91 | flask_restful_swagger.swagger.SwaggerRegistry, 92 | "/endpoint", 93 | "/endpoint.json", 94 | "/endpoint.html", 95 | ), 96 | ( 97 | flask_restful_swagger.swagger.ResourceLister, 98 | "/endpoint/_/resource_list.json", 99 | ), 100 | ( 101 | flask_restful_swagger.swagger.StaticFiles, 102 | "/endpoint/_/static///", 103 | "/endpoint/_/static//", 104 | "/endpoint/_/static/", 105 | ), 106 | ] 107 | assert mock_registration.kwargs == [ 108 | {}, 109 | {}, 110 | {}, 111 | {"endpoint": "app/registry"}, 112 | {"endpoint": "app/resourcelister"}, 113 | {"endpoint": "app/staticfiles"}, 114 | ] 115 | 116 | 117 | def test_register_once_without_blueprint(): 118 | 119 | mock_registration.reset() 120 | 121 | if "app" in flask_restful_swagger.registry: 122 | del flask_restful_swagger.registry["app"] 123 | 124 | api = Api() 125 | kwargs = dict(register_once_kwargs) 126 | kwargs["api"] = api 127 | 128 | flask_restful_swagger.swagger.register_once(**kwargs) 129 | assert flask_restful_swagger.registry["test"] == registry_content_for_test 130 | flask_restful_swagger.swagger.register_once(**kwargs) 131 | assert flask_restful_swagger.registry["test"] == registry_content_for_test 132 | 133 | assert mock_registration.args == [ 134 | ( 135 | flask_restful_swagger.swagger.SwaggerRegistry, 136 | "/endpoint", 137 | "/endpoint.json", 138 | "/endpoint.html", 139 | ), 140 | ( 141 | flask_restful_swagger.swagger.ResourceLister, 142 | "/endpoint/_/resource_list.json", 143 | ), 144 | ( 145 | flask_restful_swagger.swagger.StaticFiles, 146 | "/endpoint/_/static///", 147 | "/endpoint/_/static//", 148 | "/endpoint/_/static/", 149 | ), 150 | ] 151 | 152 | assert mock_registration.kwargs == [ 153 | {"endpoint": "app/registry"}, 154 | {"endpoint": "app/resourcelister"}, 155 | {"endpoint": "app/staticfiles"}, 156 | ] 157 | 158 | 159 | def test_register_test_deferred_setup(): 160 | 161 | if "app" in flask_restful_swagger.registry: 162 | del flask_restful_swagger.registry["app"] 163 | if "registered_blueprint" in flask_restful_swagger.registry: 164 | del flask_restful_swagger.registry["registered_blueprint"] 165 | 166 | blueprint = Blueprint("registered_blueprint", __name__) 167 | api = Api(blueprint) 168 | 169 | kwargs = dict(register_once_kwargs) 170 | kwargs["api"] = api 171 | 172 | flask_restful_swagger.swagger.register_once(**kwargs) 173 | 174 | assert ( 175 | flask_restful_swagger.registry["registered_blueprint"]["x-api-prefix"] 176 | == "" 177 | ) 178 | 179 | func = find_nested_func( 180 | flask_restful_swagger.swagger.register_once, "registering_blueprint" 181 | ) 182 | state = MockState(blueprint=blueprint, url_prefix="/none") 183 | 184 | func.__globals__["registry"] = flask_restful_swagger.registry 185 | func(state) 186 | 187 | assert ( 188 | flask_restful_swagger.registry["registered_blueprint"]["x-api-prefix"] 189 | == "/none" 190 | ) 191 | -------------------------------------------------------------------------------- /tests/test_swagger_endpoint_class.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask_restful import Resource 3 | 4 | from flask_restful_swagger.swagger import SwaggerEndpoint, operation 5 | from .lib.helpers import TestCaseSupport 6 | 7 | try: 8 | from unittest.mock import patch 9 | except ImportError: 10 | from mock import patch 11 | 12 | 13 | class MockDataType(object): 14 | pass 15 | 16 | 17 | tc = TestCaseSupport() 18 | tc.maxDiff = None 19 | 20 | 21 | @patch("flask_restful_swagger.swagger.extract_swagger_path") 22 | @patch("flask_restful_swagger.swagger.extract_path_arguments") 23 | @patch("flask_restful_swagger.swagger._parse_doc") 24 | @patch("flask_restful_swagger.swagger.SwaggerEndpoint.extract_operations") 25 | def test_swagger_endpoint(operations, docs, args, path): 26 | 27 | path.return_value = "Sometime Soon" 28 | args.return_value = "I Will Return" 29 | docs.return_value = ("A Description Will Follow", "As to Where to Meet") 30 | operations.return_value = ["knee surgery", "back surgery"] 31 | 32 | endpoint = SwaggerEndpoint("Fake Resource", "/some/path") 33 | 34 | assert path.called 35 | assert args.called 36 | assert docs.called 37 | assert operations.called 38 | 39 | assert endpoint.path == "Sometime Soon" 40 | assert endpoint.description == "A Description Will Follow" 41 | assert endpoint.notes == "As to Where to Meet" 42 | assert endpoint.operations == ["knee surgery", "back surgery"] 43 | 44 | operations.assert_called_once_with("Fake Resource", "I Will Return") 45 | 46 | 47 | def test_swagger_endpoint_extract_operations_empty(): 48 | class MockResource(Resource): 49 | def get(self): 50 | return "OK", 200, {"Access-Control-Allow-Origin": "*"} 51 | 52 | assert SwaggerEndpoint.extract_operations(MockResource, []) == [] 53 | 54 | 55 | @pytest.mark.parametrize( 56 | "mock_properties, update_with", 57 | [ 58 | ( 59 | { 60 | "name": "one", 61 | "method": "get", 62 | "other": MockDataType, 63 | "parameters": [ 64 | { 65 | "name": "identifier", 66 | "description": "identifier", 67 | "required": True, 68 | "allowMultiple": False, 69 | "dataType": "string", 70 | "paramType": "path", 71 | }, 72 | { 73 | "name": "identifier2", 74 | "description": "identifier2", 75 | "required": True, 76 | "allowMultiple": False, 77 | "dataType": "float", 78 | "paramType": "path", 79 | }, 80 | ], 81 | }, 82 | { 83 | "method": "get
get", 84 | "nickname": "nickname", 85 | "summary": None, 86 | "notes": None, 87 | "other": "MockDataType", 88 | }, 89 | ), 90 | ], 91 | ) 92 | @patch("flask_restful_swagger.swagger._get_current_registry") 93 | def test_get_swagger_endpoint_not_subclassed_basic_example( 94 | registry, mock_properties, update_with 95 | ): 96 | 97 | registry.return_value = { 98 | "apiVersion": "mock_version", 99 | "swaggerVersion": "mock_swagger_version", 100 | "basePath": "mock_path", 101 | "spec_endpoint_path": "mock_spec_endpoint_path", 102 | "description": "mock_description", 103 | } 104 | 105 | class MockResource(Resource): 106 | @operation(**mock_properties) 107 | def get(self): 108 | return "OK", 200, {"Access-Control-Allow-Origin": "*"} 109 | 110 | return_value = SwaggerEndpoint.extract_operations( 111 | MockResource, 112 | [ 113 | {"name": "identifier", "dataType": "string", "paramType": "path"}, 114 | {"name": "identifier2", "dataType": "float", "paramType": "path"}, 115 | ], 116 | ) 117 | mock_properties.update(update_with) 118 | tc.assertDictEqual(return_value[0], mock_properties) 119 | 120 | 121 | @pytest.mark.parametrize( 122 | "mock_properties, update_with", 123 | [ 124 | ( 125 | { 126 | "name": "one", 127 | "method": "get", 128 | "other": MockDataType, 129 | "parameters": [ 130 | { 131 | "name": "identifier", 132 | "description": "identifier", 133 | "required": True, 134 | "allowMultiple": False, 135 | "dataType": "string", 136 | "paramType": "path", 137 | }, 138 | { 139 | "name": "identifier2", 140 | "description": "identifier2", 141 | "required": True, 142 | "allowMultiple": False, 143 | "dataType": "float", 144 | "paramType": "path", 145 | }, 146 | ], 147 | }, 148 | { 149 | "method": "get
get", 150 | "nickname": "nickname", 151 | "summary": None, 152 | "notes": None, 153 | "other": "MockDataType", 154 | }, 155 | ), 156 | ], 157 | ) 158 | @patch("flask_restful_swagger.swagger._get_current_registry") 159 | def test_get_swagger_endpoint_subclassed_basic_example( 160 | registry, mock_properties, update_with 161 | ): 162 | 163 | registry.return_value = { 164 | "apiVersion": "mock_version", 165 | "swaggerVersion": "mock_swagger_version", 166 | "basePath": "mock_path", 167 | "spec_endpoint_path": "mock_spec_endpoint_path", 168 | "description": "mock_description", 169 | } 170 | 171 | class MockResource(Resource): 172 | @operation(**mock_properties) 173 | def get(self): 174 | return "OK", 200, {"Access-Control-Allow-Origin": "*"} 175 | 176 | class MockSubClass(MockResource): 177 | pass 178 | 179 | return_value = SwaggerEndpoint.extract_operations( 180 | MockSubClass, 181 | [ 182 | {"name": "identifier", "dataType": "string", "paramType": "path"}, 183 | {"name": "identifier2", "dataType": "float", "paramType": "path"}, 184 | ], 185 | ) 186 | mock_properties.update(update_with) 187 | tc.assertDictEqual(return_value[0], mock_properties) 188 | -------------------------------------------------------------------------------- /static/js/highlight.7.3.pack.js: -------------------------------------------------------------------------------- 1 | var hljs=new function(){function l(o){return o.replace(/&/gm,"&").replace(//gm,">")}function b(p){for(var o=p.firstChild;o;o=o.nextSibling){if(o.nodeName=="CODE"){return o}if(!(o.nodeType==3&&o.nodeValue.match(/\s+/))){break}}}function h(p,o){return Array.prototype.map.call(p.childNodes,function(q){if(q.nodeType==3){return o?q.nodeValue.replace(/\n/g,""):q.nodeValue}if(q.nodeName=="BR"){return"\n"}return h(q,o)}).join("")}function a(q){var p=(q.className+" "+q.parentNode.className).split(/\s+/);p=p.map(function(r){return r.replace(/^language-/,"")});for(var o=0;o"}while(x.length||v.length){var u=t().splice(0,1)[0];y+=l(w.substr(p,u.offset-p));p=u.offset;if(u.event=="start"){y+=s(u.node);r.push(u.node)}else{if(u.event=="stop"){var o,q=r.length;do{q--;o=r[q];y+=("")}while(o!=u.node);r.splice(q,1);while(q'+L[0]+""}else{r+=L[0]}N=A.lR.lastIndex;L=A.lR.exec(K)}return r+K.substr(N)}function z(){if(A.sL&&!e[A.sL]){return l(w)}var r=A.sL?d(A.sL,w):g(w);if(A.r>0){v+=r.keyword_count;B+=r.r}return''+r.value+""}function J(){return A.sL!==undefined?z():G()}function I(L,r){var K=L.cN?'':"";if(L.rB){x+=K;w=""}else{if(L.eB){x+=l(r)+K;w=""}else{x+=K;w=r}}A=Object.create(L,{parent:{value:A}});B+=L.r}function C(K,r){w+=K;if(r===undefined){x+=J();return 0}var L=o(r,A);if(L){x+=J();I(L,r);return L.rB?0:r.length}var M=s(A,r);if(M){if(!(M.rE||M.eE)){w+=r}x+=J();do{if(A.cN){x+=""}A=A.parent}while(A!=M.parent);if(M.eE){x+=l(r)}w="";if(M.starts){I(M.starts,"")}return M.rE?0:r.length}if(t(r,A)){throw"Illegal"}w+=r;return r.length||1}var F=e[D];f(F);var A=F;var w="";var B=0;var v=0;var x="";try{var u,q,p=0;while(true){A.t.lastIndex=p;u=A.t.exec(E);if(!u){break}q=C(E.substr(p,u.index-p),u[0]);p=u.index+q}C(E.substr(p));return{r:B,keyword_count:v,value:x,language:D}}catch(H){if(H=="Illegal"){return{r:0,keyword_count:0,value:l(E)}}else{throw H}}}function g(s){var o={keyword_count:0,r:0,value:l(s)};var q=o;for(var p in e){if(!e.hasOwnProperty(p)){continue}var r=d(p,s);r.language=p;if(r.keyword_count+r.r>q.keyword_count+q.r){q=r}if(r.keyword_count+r.r>o.keyword_count+o.r){q=o;o=r}}if(q.language){o.second_best=q}return o}function i(q,p,o){if(p){q=q.replace(/^((<[^>]+>|\t)+)/gm,function(r,v,u,t){return v.replace(/\t/g,p)})}if(o){q=q.replace(/\n/g,"
")}return q}function m(r,u,p){var v=h(r,p);var t=a(r);if(t=="no-highlight"){return}var w=t?d(t,v):g(v);t=w.language;var o=c(r);if(o.length){var q=document.createElement("pre");q.innerHTML=w.value;w.value=j(o,c(q),v)}w.value=i(w.value,u,p);var s=r.className;if(!s.match("(\\s|^)(language-)?"+t+"(\\s|$)")){s=s?(s+" "+t):t}r.innerHTML=w.value;r.className=s;r.result={language:t,kw:w.keyword_count,re:w.r};if(w.second_best){r.second_best={language:w.second_best.language,kw:w.second_best.keyword_count,re:w.second_best.r}}}function n(){if(n.called){return}n.called=true;Array.prototype.map.call(document.getElementsByTagName("pre"),b).filter(Boolean).forEach(function(o){m(o,hljs.tabReplace)})}function k(){window.addEventListener("DOMContentLoaded",n,false);window.addEventListener("load",n,false)}var e={};this.LANGUAGES=e;this.highlight=d;this.highlightAuto=g;this.fixMarkup=i;this.highlightBlock=m;this.initHighlighting=n;this.initHighlightingOnLoad=k;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|\\.|-|-=|/|/=|:|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE],r:0};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE],r:0};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.inherit=function(q,r){var o={};for(var p in q){o[p]=q[p]}if(r){for(var p in r){o[p]=r[p]}}return o}}();hljs.LANGUAGES.xml=function(a){var c="[A-Za-z0-9\\._:-]+";var b={eW:true,c:[{cN:"attribute",b:c,r:0},{b:'="',rB:true,e:'"',c:[{cN:"value",b:'"',eW:true}]},{b:"='",rB:true,e:"'",c:[{cN:"value",b:"'",eW:true}]},{b:"=",c:[{cN:"value",b:"[^\\s/>]+"}]}]};return{cI:true,c:[{cN:"pi",b:"<\\?",e:"\\?>",r:10},{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{title:"style"},c:[b],starts:{e:"",rE:true,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:"script"},c:[b],starts:{e:"<\/script>",rE:true,sL:"javascript"}},{b:"<%",e:"%>",sL:"vbscript"},{cN:"tag",b:"",c:[{cN:"title",b:"[^ />]+"},b]}]}}(hljs);hljs.LANGUAGES.json=function(a){var e={literal:"true false null"};var d=[a.QSM,a.CNM];var c={cN:"value",e:",",eW:true,eE:true,c:d,k:e};var b={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:true,eE:true,c:[a.BE],i:"\\n",starts:c}],i:"\\S"};var f={b:"\\[",e:"\\]",c:[a.inherit(c,{cN:null})],i:"\\S"};d.splice(d.length,0,b,f);return{c:d,k:e,i:"\\S"}}(hljs); -------------------------------------------------------------------------------- /flask_restful_swagger/static/lib/highlight.7.3.pack.js: -------------------------------------------------------------------------------- 1 | var hljs=new function(){function l(o){return o.replace(/&/gm,"&").replace(//gm,">")}function b(p){for(var o=p.firstChild;o;o=o.nextSibling){if(o.nodeName=="CODE"){return o}if(!(o.nodeType==3&&o.nodeValue.match(/\s+/))){break}}}function h(p,o){return Array.prototype.map.call(p.childNodes,function(q){if(q.nodeType==3){return o?q.nodeValue.replace(/\n/g,""):q.nodeValue}if(q.nodeName=="BR"){return"\n"}return h(q,o)}).join("")}function a(q){var p=(q.className+" "+q.parentNode.className).split(/\s+/);p=p.map(function(r){return r.replace(/^language-/,"")});for(var o=0;o"}while(x.length||v.length){var u=t().splice(0,1)[0];y+=l(w.substr(p,u.offset-p));p=u.offset;if(u.event=="start"){y+=s(u.node);r.push(u.node)}else{if(u.event=="stop"){var o,q=r.length;do{q--;o=r[q];y+=("")}while(o!=u.node);r.splice(q,1);while(q'+L[0]+""}else{r+=L[0]}N=A.lR.lastIndex;L=A.lR.exec(K)}return r+K.substr(N)}function z(){if(A.sL&&!e[A.sL]){return l(w)}var r=A.sL?d(A.sL,w):g(w);if(A.r>0){v+=r.keyword_count;B+=r.r}return''+r.value+""}function J(){return A.sL!==undefined?z():G()}function I(L,r){var K=L.cN?'':"";if(L.rB){x+=K;w=""}else{if(L.eB){x+=l(r)+K;w=""}else{x+=K;w=r}}A=Object.create(L,{parent:{value:A}});B+=L.r}function C(K,r){w+=K;if(r===undefined){x+=J();return 0}var L=o(r,A);if(L){x+=J();I(L,r);return L.rB?0:r.length}var M=s(A,r);if(M){if(!(M.rE||M.eE)){w+=r}x+=J();do{if(A.cN){x+=""}A=A.parent}while(A!=M.parent);if(M.eE){x+=l(r)}w="";if(M.starts){I(M.starts,"")}return M.rE?0:r.length}if(t(r,A)){throw"Illegal"}w+=r;return r.length||1}var F=e[D];f(F);var A=F;var w="";var B=0;var v=0;var x="";try{var u,q,p=0;while(true){A.t.lastIndex=p;u=A.t.exec(E);if(!u){break}q=C(E.substr(p,u.index-p),u[0]);p=u.index+q}C(E.substr(p));return{r:B,keyword_count:v,value:x,language:D}}catch(H){if(H=="Illegal"){return{r:0,keyword_count:0,value:l(E)}}else{throw H}}}function g(s){var o={keyword_count:0,r:0,value:l(s)};var q=o;for(var p in e){if(!e.hasOwnProperty(p)){continue}var r=d(p,s);r.language=p;if(r.keyword_count+r.r>q.keyword_count+q.r){q=r}if(r.keyword_count+r.r>o.keyword_count+o.r){q=o;o=r}}if(q.language){o.second_best=q}return o}function i(q,p,o){if(p){q=q.replace(/^((<[^>]+>|\t)+)/gm,function(r,v,u,t){return v.replace(/\t/g,p)})}if(o){q=q.replace(/\n/g,"
")}return q}function m(r,u,p){var v=h(r,p);var t=a(r);if(t=="no-highlight"){return}var w=t?d(t,v):g(v);t=w.language;var o=c(r);if(o.length){var q=document.createElement("pre");q.innerHTML=w.value;w.value=j(o,c(q),v)}w.value=i(w.value,u,p);var s=r.className;if(!s.match("(\\s|^)(language-)?"+t+"(\\s|$)")){s=s?(s+" "+t):t}r.innerHTML=w.value;r.className=s;r.result={language:t,kw:w.keyword_count,re:w.r};if(w.second_best){r.second_best={language:w.second_best.language,kw:w.second_best.keyword_count,re:w.second_best.r}}}function n(){if(n.called){return}n.called=true;Array.prototype.map.call(document.getElementsByTagName("pre"),b).filter(Boolean).forEach(function(o){m(o,hljs.tabReplace)})}function k(){window.addEventListener("DOMContentLoaded",n,false);window.addEventListener("load",n,false)}var e={};this.LANGUAGES=e;this.highlight=d;this.highlightAuto=g;this.fixMarkup=i;this.highlightBlock=m;this.initHighlighting=n;this.initHighlightingOnLoad=k;this.IR="[a-zA-Z][a-zA-Z0-9_]*";this.UIR="[a-zA-Z_][a-zA-Z0-9_]*";this.NR="\\b\\d+(\\.\\d+)?";this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";this.BNR="\\b(0b[01]+)";this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|\\.|-|-=|/|/=|:|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";this.BE={b:"\\\\[\\s\\S]",r:0};this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE],r:0};this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE],r:0};this.CLCM={cN:"comment",b:"//",e:"$"};this.CBLCLM={cN:"comment",b:"/\\*",e:"\\*/"};this.HCM={cN:"comment",b:"#",e:"$"};this.NM={cN:"number",b:this.NR,r:0};this.CNM={cN:"number",b:this.CNR,r:0};this.BNM={cN:"number",b:this.BNR,r:0};this.inherit=function(q,r){var o={};for(var p in q){o[p]=q[p]}if(r){for(var p in r){o[p]=r[p]}}return o}}();hljs.LANGUAGES.xml=function(a){var c="[A-Za-z0-9\\._:-]+";var b={eW:true,c:[{cN:"attribute",b:c,r:0},{b:'="',rB:true,e:'"',c:[{cN:"value",b:'"',eW:true}]},{b:"='",rB:true,e:"'",c:[{cN:"value",b:"'",eW:true}]},{b:"=",c:[{cN:"value",b:"[^\\s/>]+"}]}]};return{cI:true,c:[{cN:"pi",b:"<\\?",e:"\\?>",r:10},{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{title:"style"},c:[b],starts:{e:"",rE:true,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:"script"},c:[b],starts:{e:"<\/script>",rE:true,sL:"javascript"}},{b:"<%",e:"%>",sL:"vbscript"},{cN:"tag",b:"",c:[{cN:"title",b:"[^ />]+"},b]}]}}(hljs);hljs.LANGUAGES.json=function(a){var e={literal:"true false null"};var d=[a.QSM,a.CNM];var c={cN:"value",e:",",eW:true,eE:true,c:d,k:e};var b={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:true,eE:true,c:[a.BE],i:"\\n",starts:c}],i:"\\S"};var f={b:"\\[",e:"\\]",c:[a.inherit(c,{cN:null})],i:"\\S"};d.splice(d.length,0,b,f);return{c:d,k:e,i:"\\S"}}(hljs); -------------------------------------------------------------------------------- /flask_restful_swagger/static/lib/swagger-oauth.js: -------------------------------------------------------------------------------- 1 | var appName; 2 | var popupMask; 3 | var popupDialog; 4 | var clientId; 5 | var realm; 6 | 7 | function handleLogin() { 8 | var scopes = []; 9 | 10 | if(window.swaggerUi.api.authSchemes 11 | && window.swaggerUi.api.authSchemes.oauth2 12 | && window.swaggerUi.api.authSchemes.oauth2.scopes) { 13 | scopes = window.swaggerUi.api.authSchemes.oauth2.scopes; 14 | } 15 | 16 | if(window.swaggerUi.api 17 | && window.swaggerUi.api.info) { 18 | appName = window.swaggerUi.api.info.title; 19 | } 20 | 21 | if(popupDialog.length > 0) 22 | popupDialog = popupDialog.last(); 23 | else { 24 | popupDialog = $( 25 | [ 26 | '
', 27 | '
Select OAuth2.0 Scopes
', 28 | '
', 29 | '

Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.', 30 | 'Learn how to use', 31 | '

', 32 | '

' + appName + ' API requires the following scopes. Select which ones you want to grant to Swagger UI.

', 33 | '
    ', 34 | '
', 35 | '

', 36 | '
', 37 | '
', 38 | '
'].join('')); 39 | $(document.body).append(popupDialog); 40 | 41 | popup = popupDialog.find('ul.api-popup-scopes').empty(); 42 | for (i = 0; i < scopes.length; i ++) { 43 | scope = scopes[i]; 44 | str = '
  • ' + '
  • '; 49 | popup.append(str); 50 | } 51 | } 52 | 53 | var $win = $(window), 54 | dw = $win.width(), 55 | dh = $win.height(), 56 | st = $win.scrollTop(), 57 | dlgWd = popupDialog.outerWidth(), 58 | dlgHt = popupDialog.outerHeight(), 59 | top = (dh -dlgHt)/2 + st, 60 | left = (dw - dlgWd)/2; 61 | 62 | popupDialog.css({ 63 | top: (top < 0? 0 : top) + 'px', 64 | left: (left < 0? 0 : left) + 'px' 65 | }); 66 | 67 | popupDialog.find('button.api-popup-cancel').click(function() { 68 | popupMask.hide(); 69 | popupDialog.hide(); 70 | }); 71 | popupDialog.find('button.api-popup-authbtn').click(function() { 72 | popupMask.hide(); 73 | popupDialog.hide(); 74 | 75 | var authSchemes = window.swaggerUi.api.authSchemes; 76 | var host = window.location; 77 | var redirectUrl = "{{full_base_url}}o2c.html"; 78 | var url = null; 79 | 80 | var p = window.swaggerUi.api.authSchemes; 81 | for (var key in p) { 82 | if (p.hasOwnProperty(key)) { 83 | var o = p[key].grantTypes; 84 | for(var t in o) { 85 | if(o.hasOwnProperty(t) && t === 'implicit') { 86 | var dets = o[t]; 87 | url = dets.loginEndpoint.url + "?response_type=token"; 88 | window.swaggerUi.tokenName = dets.tokenName; 89 | } 90 | } 91 | } 92 | } 93 | var scopes = [] 94 | var o = $('.api-popup-scopes').find('input:checked'); 95 | 96 | for(k =0; k < o.length; k++) { 97 | scopes.push($(o[k]).attr("scope")); 98 | } 99 | 100 | window.enabledScopes=scopes; 101 | 102 | url += '&redirect_uri=' + encodeURIComponent(redirectUrl); 103 | url += '&realm=' + encodeURIComponent(realm); 104 | url += '&client_id=' + encodeURIComponent(clientId); 105 | url += '&scope=' + encodeURIComponent(scopes); 106 | 107 | window.open(url); 108 | }); 109 | 110 | popupMask.show(); 111 | popupDialog.show(); 112 | return; 113 | } 114 | 115 | 116 | function handleLogout() { 117 | for(key in window.authorizations.authz){ 118 | window.authorizations.remove(key) 119 | } 120 | window.enabledScopes = null; 121 | $('.api-ic.ic-on').addClass('ic-off'); 122 | $('.api-ic.ic-on').removeClass('ic-on'); 123 | 124 | // set the info box 125 | $('.api-ic.ic-warning').addClass('ic-error'); 126 | $('.api-ic.ic-warning').removeClass('ic-warning'); 127 | } 128 | 129 | function initOAuth(opts) { 130 | var o = (opts||{}); 131 | var errors = []; 132 | 133 | appName = (o.appName||errors.push("missing appName")); 134 | popupMask = (o.popupMask||$('#api-common-mask')); 135 | popupDialog = (o.popupDialog||$('.api-popup-dialog')); 136 | clientId = (o.clientId||errors.push("missing client id")); 137 | realm = (o.realm||errors.push("missing realm")); 138 | 139 | if(errors.length > 0){ 140 | log("auth unable initialize oauth: " + errors); 141 | return; 142 | } 143 | 144 | $('pre code').each(function(i, e) {hljs.highlightBlock(e)}); 145 | $('.api-ic').click(function(s) { 146 | if($(s.target).hasClass('ic-off')) 147 | handleLogin(); 148 | else { 149 | handleLogout(); 150 | } 151 | false; 152 | }); 153 | } 154 | 155 | function onOAuthComplete(token) { 156 | if(token) { 157 | if(token.error) { 158 | var checkbox = $('input[type=checkbox],.secured') 159 | checkbox.each(function(pos){ 160 | checkbox[pos].checked = false; 161 | }); 162 | alert(token.error); 163 | } 164 | else { 165 | var b = token[window.swaggerUi.tokenName]; 166 | if(b){ 167 | // if all roles are satisfied 168 | var o = null; 169 | $.each($('.auth #api_information_panel'), function(k, v) { 170 | var children = v; 171 | if(children && children.childNodes) { 172 | var requiredScopes = []; 173 | $.each((children.childNodes), function (k1, v1){ 174 | var inner = v1.innerHTML; 175 | if(inner) 176 | requiredScopes.push(inner); 177 | }); 178 | var diff = []; 179 | for(var i=0; i < requiredScopes.length; i++) { 180 | var s = requiredScopes[i]; 181 | if(window.enabledScopes && window.enabledScopes.indexOf(s) == -1) { 182 | diff.push(s); 183 | } 184 | } 185 | if(diff.length > 0){ 186 | o = v.parentNode; 187 | $(o.parentNode).find('.api-ic.ic-on').addClass('ic-off'); 188 | $(o.parentNode).find('.api-ic.ic-on').removeClass('ic-on'); 189 | 190 | // sorry, not all scopes are satisfied 191 | $(o).find('.api-ic').addClass('ic-warning'); 192 | $(o).find('.api-ic').removeClass('ic-error'); 193 | } 194 | else { 195 | o = v.parentNode; 196 | $(o.parentNode).find('.api-ic.ic-off').addClass('ic-on'); 197 | $(o.parentNode).find('.api-ic.ic-off').removeClass('ic-off'); 198 | 199 | // all scopes are satisfied 200 | $(o).find('.api-ic').addClass('ic-info'); 201 | $(o).find('.api-ic').removeClass('ic-warning'); 202 | $(o).find('.api-ic').removeClass('ic-error'); 203 | } 204 | } 205 | }); 206 | 207 | window.authorizations.add("key", new ApiKeyAuthorization("Authorization", "Bearer " + b, "header")); 208 | } 209 | } 210 | } 211 | } -------------------------------------------------------------------------------- /static/js/shred/content.js: -------------------------------------------------------------------------------- 1 | 2 | // The purpose of the `Content` object is to abstract away the data conversions 3 | // to and from raw content entities as strings. For example, you want to be able 4 | // to pass in a Javascript object and have it be automatically converted into a 5 | // JSON string if the `content-type` is set to a JSON-based media type. 6 | // Conversely, you want to be able to transparently get back a Javascript object 7 | // in the response if the `content-type` is a JSON-based media-type. 8 | 9 | // One limitation of the current implementation is that it [assumes the `charset` is UTF-8](https://github.com/spire-io/shred/issues/5). 10 | 11 | // The `Content` constructor takes an options object, which *must* have either a 12 | // `body` or `data` property and *may* have a `type` property indicating the 13 | // media type. If there is no `type` attribute, a default will be inferred. 14 | var Content = function(options) { 15 | this.body = options.body; 16 | this.data = options.data; 17 | this.type = options.type; 18 | }; 19 | 20 | Content.prototype = { 21 | // Treat `toString()` as asking for the `content.body`. That is, the raw content entity. 22 | // 23 | // toString: function() { return this.body; } 24 | // 25 | // Commented out, but I've forgotten why. :/ 26 | }; 27 | 28 | 29 | // `Content` objects have the following attributes: 30 | Object.defineProperties(Content.prototype,{ 31 | 32 | // - **type**. Typically accessed as `content.type`, reflects the `content-type` 33 | // header associated with the request or response. If not passed as an options 34 | // to the constructor or set explicitly, it will infer the type the `data` 35 | // attribute, if possible, and, failing that, will default to `text/plain`. 36 | type: { 37 | get: function() { 38 | if (this._type) { 39 | return this._type; 40 | } else { 41 | if (this._data) { 42 | switch(typeof this._data) { 43 | case "string": return "text/plain"; 44 | case "object": return "application/json"; 45 | } 46 | } 47 | } 48 | return "text/plain"; 49 | }, 50 | set: function(value) { 51 | this._type = value; 52 | return this; 53 | }, 54 | enumerable: true 55 | }, 56 | 57 | // - **data**. Typically accessed as `content.data`, reflects the content entity 58 | // converted into Javascript data. This can be a string, if the `type` is, say, 59 | // `text/plain`, but can also be a Javascript object. The conversion applied is 60 | // based on the `processor` attribute. The `data` attribute can also be set 61 | // directly, in which case the conversion will be done the other way, to infer 62 | // the `body` attribute. 63 | data: { 64 | get: function() { 65 | if (this._body) { 66 | return this.processor.parser(this._body); 67 | } else { 68 | return this._data; 69 | } 70 | }, 71 | set: function(data) { 72 | if (this._body&&data) Errors.setDataWithBody(this); 73 | this._data = data; 74 | return this; 75 | }, 76 | enumerable: true 77 | }, 78 | 79 | // - **body**. Typically accessed as `content.body`, reflects the content entity 80 | // as a UTF-8 string. It is the mirror of the `data` attribute. If you set the 81 | // `data` attribute, the `body` attribute will be inferred and vice-versa. If 82 | // you attempt to set both, an exception is raised. 83 | body: { 84 | get: function() { 85 | if (this._data) { 86 | return this.processor.stringify(this._data); 87 | } else { 88 | return this._body.toString(); 89 | } 90 | }, 91 | set: function(body) { 92 | if (this._data&&body) Errors.setBodyWithData(this); 93 | this._body = body; 94 | return this; 95 | }, 96 | enumerable: true 97 | }, 98 | 99 | // - **processor**. The functions that will be used to convert to/from `data` and 100 | // `body` attributes. You can add processors. The two that are built-in are for 101 | // `text/plain`, which is basically an identity transformation and 102 | // `application/json` and other JSON-based media types (including custom media 103 | // types with `+json`). You can add your own processors. See below. 104 | processor: { 105 | get: function() { 106 | var processor = Content.processors[this.type]; 107 | if (processor) { 108 | return processor; 109 | } else { 110 | // Return the first processor that matches any part of the 111 | // content type. ex: application/vnd.foobar.baz+json will match json. 112 | var main = this.type.split(";")[0]; 113 | var parts = main.split(/\+|\//); 114 | for (var i=0, l=parts.length; i < l; i++) { 115 | processor = Content.processors[parts[i]] 116 | } 117 | return processor || {parser:identity,stringify:toString}; 118 | } 119 | }, 120 | enumerable: true 121 | }, 122 | 123 | // - **length**. Typically accessed as `content.length`, returns the length in 124 | // bytes of the raw content entity. 125 | length: { 126 | get: function() { 127 | if (typeof Buffer !== 'undefined') { 128 | return Buffer.byteLength(this.body); 129 | } 130 | return this.body.length; 131 | } 132 | } 133 | }); 134 | 135 | Content.processors = {}; 136 | 137 | // The `registerProcessor` function allows you to add your own processors to 138 | // convert content entities. Each processor consists of a Javascript object with 139 | // two properties: 140 | // - **parser**. The function used to parse a raw content entity and convert it 141 | // into a Javascript data type. 142 | // - **stringify**. The function used to convert a Javascript data type into a 143 | // raw content entity. 144 | Content.registerProcessor = function(types,processor) { 145 | 146 | // You can pass an array of types that will trigger this processor, or just one. 147 | // We determine the array via duck-typing here. 148 | if (types.forEach) { 149 | types.forEach(function(type) { 150 | Content.processors[type] = processor; 151 | }); 152 | } else { 153 | // If you didn't pass an array, we just use what you pass in. 154 | Content.processors[types] = processor; 155 | } 156 | }; 157 | 158 | // Register the identity processor, which is used for text-based media types. 159 | var identity = function(x) { return x; } 160 | , toString = function(x) { return x.toString(); } 161 | Content.registerProcessor( 162 | ["text/html","text/plain","text"], 163 | { parser: identity, stringify: toString }); 164 | 165 | // Register the JSON processor, which is used for JSON-based media types. 166 | Content.registerProcessor( 167 | ["application/json; charset=utf-8","application/json","json"], 168 | { 169 | parser: function(string) { 170 | return JSON.parse(string); 171 | }, 172 | stringify: function(data) { 173 | return JSON.stringify(data); }}); 174 | 175 | var qs = require('querystring'); 176 | // Register the post processor, which is used for JSON-based media types. 177 | Content.registerProcessor( 178 | ["application/x-www-form-urlencoded"], 179 | { parser : qs.parse, stringify : qs.stringify }); 180 | 181 | // Error functions are defined separately here in an attempt to make the code 182 | // easier to read. 183 | var Errors = { 184 | setDataWithBody: function(object) { 185 | throw new Error("Attempt to set data attribute of a content object " + 186 | "when the body attributes was already set."); 187 | }, 188 | setBodyWithData: function(object) { 189 | throw new Error("Attempt to set body attribute of a content object " + 190 | "when the data attributes was already set."); 191 | } 192 | } 193 | module.exports = Content; -------------------------------------------------------------------------------- /flask_restful_swagger/static/lib/shred/content.js: -------------------------------------------------------------------------------- 1 | 2 | // The purpose of the `Content` object is to abstract away the data conversions 3 | // to and from raw content entities as strings. For example, you want to be able 4 | // to pass in a Javascript object and have it be automatically converted into a 5 | // JSON string if the `content-type` is set to a JSON-based media type. 6 | // Conversely, you want to be able to transparently get back a Javascript object 7 | // in the response if the `content-type` is a JSON-based media-type. 8 | 9 | // One limitation of the current implementation is that it [assumes the `charset` is UTF-8](https://github.com/spire-io/shred/issues/5). 10 | 11 | // The `Content` constructor takes an options object, which *must* have either a 12 | // `body` or `data` property and *may* have a `type` property indicating the 13 | // media type. If there is no `type` attribute, a default will be inferred. 14 | var Content = function(options) { 15 | this.body = options.body; 16 | this.data = options.data; 17 | this.type = options.type; 18 | }; 19 | 20 | Content.prototype = { 21 | // Treat `toString()` as asking for the `content.body`. That is, the raw content entity. 22 | // 23 | // toString: function() { return this.body; } 24 | // 25 | // Commented out, but I've forgotten why. :/ 26 | }; 27 | 28 | 29 | // `Content` objects have the following attributes: 30 | Object.defineProperties(Content.prototype,{ 31 | 32 | // - **type**. Typically accessed as `content.type`, reflects the `content-type` 33 | // header associated with the request or response. If not passed as an options 34 | // to the constructor or set explicitly, it will infer the type the `data` 35 | // attribute, if possible, and, failing that, will default to `text/plain`. 36 | type: { 37 | get: function() { 38 | if (this._type) { 39 | return this._type; 40 | } else { 41 | if (this._data) { 42 | switch(typeof this._data) { 43 | case "string": return "text/plain"; 44 | case "object": return "application/json"; 45 | } 46 | } 47 | } 48 | return "text/plain"; 49 | }, 50 | set: function(value) { 51 | this._type = value; 52 | return this; 53 | }, 54 | enumerable: true 55 | }, 56 | 57 | // - **data**. Typically accessed as `content.data`, reflects the content entity 58 | // converted into Javascript data. This can be a string, if the `type` is, say, 59 | // `text/plain`, but can also be a Javascript object. The conversion applied is 60 | // based on the `processor` attribute. The `data` attribute can also be set 61 | // directly, in which case the conversion will be done the other way, to infer 62 | // the `body` attribute. 63 | data: { 64 | get: function() { 65 | if (this._body) { 66 | return this.processor.parser(this._body); 67 | } else { 68 | return this._data; 69 | } 70 | }, 71 | set: function(data) { 72 | if (this._body&&data) Errors.setDataWithBody(this); 73 | this._data = data; 74 | return this; 75 | }, 76 | enumerable: true 77 | }, 78 | 79 | // - **body**. Typically accessed as `content.body`, reflects the content entity 80 | // as a UTF-8 string. It is the mirror of the `data` attribute. If you set the 81 | // `data` attribute, the `body` attribute will be inferred and vice-versa. If 82 | // you attempt to set both, an exception is raised. 83 | body: { 84 | get: function() { 85 | if (this._data) { 86 | return this.processor.stringify(this._data); 87 | } else { 88 | return this._body.toString(); 89 | } 90 | }, 91 | set: function(body) { 92 | if (this._data&&body) Errors.setBodyWithData(this); 93 | this._body = body; 94 | return this; 95 | }, 96 | enumerable: true 97 | }, 98 | 99 | // - **processor**. The functions that will be used to convert to/from `data` and 100 | // `body` attributes. You can add processors. The two that are built-in are for 101 | // `text/plain`, which is basically an identity transformation and 102 | // `application/json` and other JSON-based media types (including custom media 103 | // types with `+json`). You can add your own processors. See below. 104 | processor: { 105 | get: function() { 106 | var processor = Content.processors[this.type]; 107 | if (processor) { 108 | return processor; 109 | } else { 110 | // Return the first processor that matches any part of the 111 | // content type. ex: application/vnd.foobar.baz+json will match json. 112 | var main = this.type.split(";")[0]; 113 | var parts = main.split(/\+|\//); 114 | for (var i=0, l=parts.length; i < l; i++) { 115 | processor = Content.processors[parts[i]] 116 | } 117 | return processor || {parser:identity,stringify:toString}; 118 | } 119 | }, 120 | enumerable: true 121 | }, 122 | 123 | // - **length**. Typically accessed as `content.length`, returns the length in 124 | // bytes of the raw content entity. 125 | length: { 126 | get: function() { 127 | if (typeof Buffer !== 'undefined') { 128 | return Buffer.byteLength(this.body); 129 | } 130 | return this.body.length; 131 | } 132 | } 133 | }); 134 | 135 | Content.processors = {}; 136 | 137 | // The `registerProcessor` function allows you to add your own processors to 138 | // convert content entities. Each processor consists of a Javascript object with 139 | // two properties: 140 | // - **parser**. The function used to parse a raw content entity and convert it 141 | // into a Javascript data type. 142 | // - **stringify**. The function used to convert a Javascript data type into a 143 | // raw content entity. 144 | Content.registerProcessor = function(types,processor) { 145 | 146 | // You can pass an array of types that will trigger this processor, or just one. 147 | // We determine the array via duck-typing here. 148 | if (types.forEach) { 149 | types.forEach(function(type) { 150 | Content.processors[type] = processor; 151 | }); 152 | } else { 153 | // If you didn't pass an array, we just use what you pass in. 154 | Content.processors[types] = processor; 155 | } 156 | }; 157 | 158 | // Register the identity processor, which is used for text-based media types. 159 | var identity = function(x) { return x; } 160 | , toString = function(x) { return x.toString(); } 161 | Content.registerProcessor( 162 | ["text/html","text/plain","text"], 163 | { parser: identity, stringify: toString }); 164 | 165 | // Register the JSON processor, which is used for JSON-based media types. 166 | Content.registerProcessor( 167 | ["application/json; charset=utf-8","application/json","json"], 168 | { 169 | parser: function(string) { 170 | return JSON.parse(string); 171 | }, 172 | stringify: function(data) { 173 | return JSON.stringify(data); }}); 174 | 175 | var qs = require('querystring'); 176 | // Register the post processor, which is used for JSON-based media types. 177 | Content.registerProcessor( 178 | ["application/x-www-form-urlencoded"], 179 | { parser : qs.parse, stringify : qs.stringify }); 180 | 181 | // Error functions are defined separately here in an attempt to make the code 182 | // easier to read. 183 | var Errors = { 184 | setDataWithBody: function(object) { 185 | throw new Error("Attempt to set data attribute of a content object " + 186 | "when the body attributes was already set."); 187 | }, 188 | setBodyWithData: function(object) { 189 | throw new Error("Attempt to set body attribute of a content object " + 190 | "when the data attributes was already set."); 191 | } 192 | } 193 | module.exports = Content; -------------------------------------------------------------------------------- /tests/fixtures_add_model.py: -------------------------------------------------------------------------------- 1 | try: 2 | from unittest.mock import patch 3 | from unittest.mock import Mock 4 | except ImportError: 5 | from mock import patch 6 | from mock import Mock 7 | from contextlib import contextmanager 8 | 9 | from flask_restful import fields 10 | 11 | from flask_restful_swagger import swagger 12 | 13 | 14 | @contextmanager 15 | def patch_registry(): 16 | with patch("flask_restful_swagger.swagger.registry") as mock_registry: 17 | _temp_dict = {"models": {}} 18 | mock_registry.__getitem__.side_effect = _temp_dict.__getitem__ 19 | mock_registry.__setitem__.side_effect = _temp_dict.__setitem__ 20 | yield _temp_dict 21 | 22 | 23 | @contextmanager 24 | def patch_parse_doc(): 25 | with patch("flask_restful_swagger.swagger._parse_doc") as mock_parse_doc: 26 | mock_parse_doc.return_value = (None, None) 27 | yield mock_parse_doc 28 | 29 | 30 | @contextmanager 31 | def patch_deduce_swagger_type(): 32 | with patch( 33 | "flask_restful_swagger.swagger.deduce_swagger_type" 34 | ) as mock_deduce_swagger_type: 35 | mock_deduce_swagger_type.return_value = "dummy_swagger_type" 36 | yield mock_deduce_swagger_type 37 | 38 | 39 | @contextmanager 40 | def patch_isinstance(patchbool): 41 | with patch("flask_restful_swagger.swagger.isinstance") as mock_isinstance: 42 | mock_isinstance.return_value = patchbool 43 | yield mock_isinstance 44 | 45 | 46 | @contextmanager 47 | def patch_hasattr(): 48 | with patch("flask_restful_swagger.swagger.hasattr") as mock_hasattr: 49 | mock_hasattr.return_value = True 50 | yield mock_hasattr 51 | 52 | 53 | @contextmanager 54 | def patch_dir(patchreturn): 55 | with patch("flask_restful_swagger.swagger.dir") as mock_dir: 56 | mock_dir.return_value = patchreturn 57 | yield mock_dir 58 | 59 | 60 | @contextmanager 61 | def patch_getargspec(): 62 | with patch( 63 | "flask_restful_swagger.swagger.inspect.getargspec" 64 | ) as mock_getargspec: 65 | mock_argspec = Mock() 66 | mock_argspec.args = ["self", "arg1", "arg2", "arg3"] 67 | mock_argspec.defaults = ("123",) 68 | mock_getargspec.return_value = mock_argspec 69 | yield mock_getargspec 70 | 71 | 72 | ############################################################################### 73 | # Copy setup objects from examples/basic.py 74 | ############################################################################### 75 | 76 | MockBasicObjectNoInit = Mock() 77 | MockBasicObjectNoInit.__name__ = MockBasicObjectNoInit 78 | 79 | 80 | class MockBasicObject: 81 | def __init__(self, arg1): 82 | pass 83 | 84 | 85 | class MockBasicWithSwaggerMetadata1: 86 | def __init__(self, arg1): 87 | pass 88 | 89 | swagger_metadata = {"an_enum": {"enum": ["one", "two", "three"]}} 90 | 91 | 92 | class MockBasicWithSwaggerMetadata2: 93 | def __init__(self, arg1, an_enum): 94 | pass 95 | 96 | swagger_metadata = {"an_enum": {"enum": ["one", "two", "three"]}} 97 | 98 | 99 | class MockTodoItem: 100 | """This is an example of a model class that has parameters in its constructor 101 | and the fields in the swagger spec are derived from the parameters 102 | to __init__. 103 | In this case we would have args, arg2 as required parameters and arg3 as 104 | optional parameter. 105 | """ 106 | 107 | def __init__(self, arg1, arg2, arg3="123"): 108 | pass 109 | 110 | 111 | class MockModelWithResourceFieldsNoRequired: 112 | resource_fields = {"a_string": fields.String()} 113 | 114 | 115 | class MockModelWithResourceFieldsWithRequired: 116 | resource_fields = {"a_string": fields.String()} 117 | 118 | required = ["a_string"] 119 | 120 | 121 | @swagger.nested( 122 | a_nested_attribute=MockModelWithResourceFieldsNoRequired.__name__, 123 | a_list_of_nested_types=MockModelWithResourceFieldsNoRequired.__name__, 124 | ) 125 | class MockModelWithResourceFieldsWithRequiredWithSwaggerMetadata: 126 | resource_fields = { 127 | "a_string": fields.String(), 128 | "an_enum": fields.String, 129 | } 130 | required = ["a_string"] 131 | swagger_metadata = {"an_enum": {"enum": ["one", "two", "three"]}} 132 | 133 | 134 | @swagger.nested( 135 | a_nested_attribute=MockModelWithResourceFieldsNoRequired.__name__, 136 | a_list_of_nested_types=MockModelWithResourceFieldsNoRequired.__name__, 137 | ) 138 | class MockTodoItemWithResourceFields: 139 | """This is an example of how Output Fields work 140 | (http://flask-restful.readthedocs.org/en/latest/fields.html). 141 | Output Fields lets you add resource_fields to your model in which you 142 | specify the output of the model when it gets sent as an HTTP response. 143 | flask-restful-swagger takes advantage of this to specify the fields in 144 | the model 145 | """ 146 | 147 | resource_fields = { 148 | "a_string": fields.String(attribute="a_string_field_name"), 149 | "a_formatted_string": fields.FormattedString, 150 | "an_enum": fields.String, 151 | "an_int": fields.Integer, 152 | "a_bool": fields.Boolean, 153 | "a_url": fields.Url, 154 | "a_float": fields.Float, 155 | "an_float_with_arbitrary_precision": fields.Arbitrary, 156 | "a_fixed_point_decimal": fields.Fixed, 157 | "a_datetime": fields.DateTime, 158 | "a_list_of_strings": fields.List(fields.String), 159 | "a_nested_attribute": fields.Nested( 160 | MockModelWithResourceFieldsNoRequired.resource_fields 161 | ), 162 | "a_list_of_nested_types": fields.List( 163 | fields.Nested( 164 | MockModelWithResourceFieldsNoRequired.resource_fields 165 | ) 166 | ), 167 | } 168 | 169 | # Specify which of the resource fields are required 170 | required = ["a_string"] 171 | 172 | 173 | ############################################################################### 174 | # Tests Fixtures 175 | ############################################################################### 176 | 177 | fixtures_integration_test_add_model = [ 178 | (MockBasicObject, [], [], []), 179 | (MockTodoItem, ["arg1", "arg2", "arg3"], ["arg1", "arg2"], ["arg3"]), 180 | (MockModelWithResourceFieldsNoRequired, ["a_string"], [], []), 181 | ( 182 | MockTodoItemWithResourceFields, 183 | [ 184 | "a_string", 185 | "a_formatted_string", 186 | "an_enum", 187 | "an_int", 188 | "a_bool", 189 | "a_url", 190 | "a_float", 191 | "an_float_with_arbitrary_precision", 192 | "a_fixed_point_decimal", 193 | "a_datetime", 194 | "a_list_of_strings", 195 | "a_nested_attribute", 196 | "a_list_of_nested_types", 197 | ], 198 | ["a_string"], 199 | [], 200 | ), 201 | (MockBasicWithSwaggerMetadata1, [], [], []), 202 | (MockBasicWithSwaggerMetadata2, [], [], []), 203 | ] 204 | 205 | fixtures_add_model_get_docs = [ 206 | MockBasicObject, 207 | MockTodoItem, 208 | MockModelWithResourceFieldsNoRequired, 209 | MockTodoItemWithResourceFields, 210 | ] 211 | 212 | fixtures_add_model_with_resource_fields_without_swagger_metadata = [ 213 | MockModelWithResourceFieldsWithRequired, 214 | ] 215 | 216 | fixtures_add_model_with_resource_fields_with_nested = [ 217 | MockTodoItemWithResourceFields, 218 | ] 219 | 220 | fixtures_add_model_with_resource_fields_nested_swagger_metadata = [ 221 | MockModelWithResourceFieldsWithRequiredWithSwaggerMetadata, 222 | ] 223 | 224 | 225 | fixtures_add_model_no_properties = [ 226 | MockBasicObjectNoInit, 227 | ] 228 | 229 | fixtures_add_model_init = [ 230 | MockBasicObject, 231 | MockTodoItem, 232 | ] 233 | 234 | fixtures_add_model_init_parsing_args = [ 235 | [MockTodoItem, ["arg1", "arg2"], [("arg3", "123")]] 236 | ] 237 | -------------------------------------------------------------------------------- /examples/basic.py: -------------------------------------------------------------------------------- 1 | """ 2 | Running: 3 | 4 | PYTHONPATH=. python examples/basic.py 5 | 6 | """ 7 | 8 | 9 | from flask import Flask, redirect 10 | from flask_restful import Api, Resource, abort, fields, marshal_with, reqparse 11 | 12 | from flask_restful_swagger import swagger 13 | 14 | app = Flask(__name__, static_folder="../static") 15 | 16 | ################################### 17 | # This is important: 18 | api = swagger.docs( 19 | Api(app), 20 | apiVersion="0.1", 21 | basePath="http://localhost:5000", 22 | resourcePath="/", 23 | produces=["application/json", "text/html"], 24 | api_spec_url="/api/spec", 25 | description="A Basic API", 26 | ) 27 | ################################### 28 | 29 | TODOS = { 30 | "todo1": {"task": "build an API"}, 31 | "todo2": {"task": "?????"}, 32 | "todo3": {"task": "profit!"}, 33 | } 34 | 35 | 36 | def abort_if_todo_doesnt_exist(todo_id): 37 | if todo_id not in TODOS: 38 | abort(404, message="Todo {} doesn't exist".format(todo_id)) 39 | 40 | 41 | parser = reqparse.RequestParser() 42 | parser.add_argument("task", type=str) 43 | 44 | 45 | @swagger.model 46 | class TodoItem: 47 | """This is an example of a model class that has parameters in its constructor 48 | and the fields in the swagger spec are derived from the parameters 49 | to __init__. 50 | In this case we would have args, arg2 as required parameters and arg3 as 51 | optional parameter.""" 52 | 53 | def __init__(self, arg1, arg2, arg3="123"): 54 | pass 55 | 56 | 57 | class Todo(Resource): 58 | "My TODO API" 59 | 60 | @swagger.operation( 61 | notes="get a todo item by ID", 62 | nickname="get", 63 | # Parameters can be automatically extracted from URLs. 64 | # For Example: 65 | # but you could also override them here, or add other parameters. 66 | parameters=[ 67 | { 68 | "name": "todo_id_x", 69 | "description": "The ID of the TODO item", 70 | "required": True, 71 | "allowMultiple": False, 72 | "dataType": "string", 73 | "paramType": "path", 74 | }, 75 | { 76 | "name": "a_bool", 77 | "description": "The ID of the TODO item", 78 | "required": True, 79 | "allowMultiple": False, 80 | "dataType": "boolean", 81 | "paramType": "path", 82 | }, 83 | ], 84 | ) 85 | def get(self, todo_id): 86 | # This goes into the summary 87 | """Get a todo task 88 | 89 | This will be added to the Implementation Notes. 90 | It lets you put very long text in your api. 91 | 92 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 93 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 94 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 95 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 96 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat 97 | cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id 98 | est laborum. 99 | """ 100 | abort_if_todo_doesnt_exist(todo_id) 101 | return TODOS[todo_id], 200, {"Access-Control-Allow-Origin": "*"} 102 | 103 | @swagger.operation(notes="delete a todo item by ID",) 104 | def delete(self, todo_id): 105 | abort_if_todo_doesnt_exist(todo_id) 106 | del TODOS[todo_id] 107 | return "", 204, {"Access-Control-Allow-Origin": "*"} 108 | 109 | @swagger.operation(notes="edit a todo item by ID",) 110 | def put(self, todo_id): 111 | args = parser.parse_args() 112 | task = {"task": args["task"]} 113 | TODOS[todo_id] = task 114 | return task, 201, {"Access-Control-Allow-Origin": "*"} 115 | 116 | def options(self, **args): 117 | # since this method is not decorated with @swagger.operation it does 118 | # not get added to the swagger docs 119 | return ( 120 | {"Allow": "GET,PUT,POST,DELETE"}, 121 | 200, 122 | { 123 | "Access-Control-Allow-Origin": "*", 124 | "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE", 125 | "Access-Control-Allow-Headers": "Content-Type", 126 | }, 127 | ) 128 | 129 | 130 | # TodoList 131 | # shows a list of all todos, and lets you POST to add new tasks 132 | class TodoList(Resource): 133 | def get(self): 134 | return TODOS, 200, {"Access-Control-Allow-Origin": "*"} 135 | 136 | @swagger.operation( 137 | notes="Creates a new TODO item", 138 | responseClass=TodoItem.__name__, 139 | nickname="create", 140 | parameters=[ 141 | { 142 | "name": "body", 143 | "description": "A TODO item", 144 | "required": True, 145 | "allowMultiple": False, 146 | "dataType": TodoItem.__name__, 147 | "paramType": "body", 148 | } 149 | ], 150 | responseMessages=[ 151 | { 152 | "code": 201, 153 | "message": "Created. The URL of the created blueprint should " 154 | + "be in the Location header", 155 | }, 156 | {"code": 405, "message": "Invalid input"}, 157 | ], 158 | ) 159 | def post(self): 160 | args = parser.parse_args() 161 | todo_id = "todo%d" % (len(TODOS) + 1) 162 | TODOS[todo_id] = {"task": args["task"]} 163 | return TODOS[todo_id], 201, {"Access-Control-Allow-Origin": "*"} 164 | 165 | 166 | @swagger.model 167 | class ModelWithResourceFields: 168 | resource_fields = {"a_string": fields.String()} 169 | 170 | 171 | @swagger.model 172 | @swagger.nested( 173 | a_nested_attribute=ModelWithResourceFields.__name__, 174 | a_list_of_nested_types=ModelWithResourceFields.__name__, 175 | ) 176 | class TodoItemWithResourceFields: 177 | """This is an example of how Output Fields work 178 | (http://flask-restful.readthedocs.org/en/latest/fields.html). 179 | Output Fields lets you add resource_fields to your model in which you specify 180 | the output of the model when it gets sent as an HTTP response. 181 | flask-restful-swagger takes advantage of this to specify the fields in 182 | the model""" 183 | 184 | resource_fields = { 185 | "a_string": fields.String(attribute="a_string_field_name"), 186 | "a_formatted_string": fields.FormattedString, 187 | "an_enum": fields.String, 188 | "an_int": fields.Integer, 189 | "a_bool": fields.Boolean, 190 | "a_url": fields.Url, 191 | "a_float": fields.Float, 192 | "an_float_with_arbitrary_precision": fields.Arbitrary, 193 | "a_fixed_point_decimal": fields.Fixed, 194 | "a_datetime": fields.DateTime, 195 | "a_list_of_strings": fields.List(fields.String), 196 | "a_nested_attribute": fields.Nested( 197 | ModelWithResourceFields.resource_fields 198 | ), 199 | "a_list_of_nested_types": fields.List( 200 | fields.Nested(ModelWithResourceFields.resource_fields) 201 | ), 202 | } 203 | 204 | # Specify which of the resource fields are required 205 | required = ["a_string"] 206 | 207 | swagger_metadata = {"an_enum": {"enum": ["one", "two", "three"]}} 208 | 209 | 210 | class MarshalWithExample(Resource): 211 | @swagger.operation( 212 | notes="get something", 213 | responseClass=TodoItemWithResourceFields, 214 | nickname="get", 215 | ) 216 | @marshal_with(TodoItemWithResourceFields.resource_fields) 217 | def get(self, **kwargs): 218 | return {}, 200, {"Access-Control-Allow-Origin": "*"} 219 | 220 | 221 | # 222 | # Actually setup the Api resource routing here 223 | # 224 | api.add_resource(TodoList, "/todos") 225 | api.add_resource(Todo, "/todos/") 226 | api.add_resource(MarshalWithExample, "/marshal_with") 227 | 228 | 229 | @app.route("/docs") 230 | def docs(): 231 | return redirect("/static/docs.html") 232 | 233 | 234 | if __name__ == "__main__": 235 | TodoItemWithResourceFields() 236 | TodoItem(1, 2, "3") 237 | app.run(host='0.0.0.0', debug=True) 238 | -------------------------------------------------------------------------------- /examples/blueprints.py: -------------------------------------------------------------------------------- 1 | """ 2 | Running: 3 | 4 | PYTHONPATH=. python examples/basic.py 5 | 6 | Goto: http://127.0.0.1:5000/api2/api/spec.html 7 | 8 | """ 9 | 10 | 11 | from flask import Blueprint, Flask, redirect 12 | from flask_restful import Api, Resource, abort, fields, marshal_with, reqparse 13 | 14 | from flask_restful_swagger import swagger 15 | 16 | app = Flask(__name__, static_folder="../static") 17 | my_blueprint1 = Blueprint("my_blueprint1", __name__) 18 | my_blueprint2 = Blueprint("my_blueprint2", __name__) 19 | 20 | ################################### 21 | # This is important: 22 | api1 = swagger.docs( 23 | Api(my_blueprint1), 24 | apiVersion="0.1", 25 | basePath="http://localhost:5000", 26 | resourcePath="/", 27 | produces=["application/json", "text/html"], 28 | api_spec_url="/api/spec", 29 | description="Blueprint1 Description", 30 | ) 31 | api2 = swagger.docs( 32 | Api(my_blueprint2), 33 | apiVersion="0.1", 34 | basePath="http://localhost:5000", 35 | resourcePath="/", 36 | produces=["application/json", "text/html"], 37 | api_spec_url="/api/spec", 38 | description="Blueprint2 Description", 39 | ) 40 | ################################### 41 | 42 | TODOS = { 43 | "todo1": {"task": "build an API"}, 44 | "todo2": {"task": "?????"}, 45 | "todo3": {"task": "profit!"}, 46 | } 47 | 48 | 49 | def abort_if_todo_doesnt_exist(todo_id): 50 | if todo_id not in TODOS: 51 | abort(404, message="Todo {} doesn't exist".format(todo_id)) 52 | 53 | 54 | parser = reqparse.RequestParser() 55 | parser.add_argument("task", type=str) 56 | 57 | 58 | @swagger.model 59 | class TodoItem: 60 | """This is an example of a model class that has parameters in its constructor 61 | and the fields in the swagger spec are derived from the parameters 62 | to __init__. 63 | In this case we would have args, arg2 as required parameters and arg3 as 64 | optional parameter.""" 65 | 66 | def __init__(self, arg1, arg2, arg3="123"): 67 | pass 68 | 69 | 70 | class Todo(Resource): 71 | "My TODO API" 72 | 73 | @swagger.operation( 74 | notes="get a todo item by ID", 75 | nickname="get", 76 | # Parameters can be automatically extracted from URLs. 77 | # For Example: 78 | # but you could also override them here, or add other parameters. 79 | parameters=[ 80 | { 81 | "name": "todo_id_x", 82 | "description": "The ID of the TODO item", 83 | "required": True, 84 | "allowMultiple": False, 85 | "dataType": "string", 86 | "paramType": "path", 87 | }, 88 | { 89 | "name": "a_bool", 90 | "description": "The ID of the TODO item", 91 | "required": True, 92 | "allowMultiple": False, 93 | "dataType": "boolean", 94 | "paramType": "path", 95 | }, 96 | ], 97 | ) 98 | def get(self, todo_id): 99 | # This goes into the summary 100 | """Get a todo task 101 | 102 | This will be added to the Implementation Notes. 103 | It let's you put very long text in your api. 104 | 105 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 106 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim 107 | veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea 108 | commodo consequat. Duis aute irure dolor in reprehenderit in voluptate 109 | velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat 110 | cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id 111 | est laborum. 112 | """ 113 | abort_if_todo_doesnt_exist(todo_id) 114 | return TODOS[todo_id], 200, {"Access-Control-Allow-Origin": "*"} 115 | 116 | @swagger.operation(notes="delete a todo item by ID",) 117 | def delete(self, todo_id): 118 | abort_if_todo_doesnt_exist(todo_id) 119 | del TODOS[todo_id] 120 | return "", 204, {"Access-Control-Allow-Origin": "*"} 121 | 122 | @swagger.operation(notes="edit a todo item by ID",) 123 | def put(self, todo_id): 124 | args = parser.parse_args() 125 | task = {"task": args["task"]} 126 | TODOS[todo_id] = task 127 | return task, 201, {"Access-Control-Allow-Origin": "*"} 128 | 129 | def options(self, **args): 130 | # since this method is not decorated with @swagger.operation it does 131 | # not get added to the swagger docs 132 | return ( 133 | {"Allow": "GET,PUT,POST,DELETE"}, 134 | 200, 135 | { 136 | "Access-Control-Allow-Origin": "*", 137 | "Access-Control-Allow-Methods": "GET,PUT,POST,DELETE", 138 | "Access-Control-Allow-Headers": "Content-Type", 139 | }, 140 | ) 141 | 142 | 143 | # TodoList 144 | # shows a list of all todos, and lets you POST to add new tasks 145 | class TodoList(Resource): 146 | def get(self): 147 | return TODOS, 200, {"Access-Control-Allow-Origin": "*"} 148 | 149 | @swagger.operation( 150 | notes="Creates a new TODO item", 151 | responseClass=TodoItem.__name__, 152 | nickname="create", 153 | parameters=[ 154 | { 155 | "name": "body", 156 | "description": "A TODO item", 157 | "required": True, 158 | "allowMultiple": False, 159 | "dataType": TodoItem.__name__, 160 | "paramType": "body", 161 | } 162 | ], 163 | responseMessages=[ 164 | { 165 | "code": 201, 166 | "message": "Created. The URL of the created blueprint should " 167 | + "be in the Location header", 168 | }, 169 | {"code": 405, "message": "Invalid input"}, 170 | ], 171 | ) 172 | def post(self): 173 | args = parser.parse_args() 174 | todo_id = "todo%d" % (len(TODOS) + 1) 175 | TODOS[todo_id] = {"task": args["task"]} 176 | return TODOS[todo_id], 201, {"Access-Control-Allow-Origin": "*"} 177 | 178 | 179 | @swagger.model 180 | class ModelWithResourceFields: 181 | resource_fields = {"a_string": fields.String()} 182 | 183 | 184 | @swagger.model 185 | @swagger.nested( 186 | a_nested_attribute=ModelWithResourceFields.__name__, 187 | a_list_of_nested_types=ModelWithResourceFields.__name__, 188 | ) 189 | class TodoItemWithResourceFields: 190 | """This is an example of how Output Fields work 191 | (http://flask-restful.readthedocs.org/en/latest/fields.html). 192 | Output Fields lets you add resource_fields to your model in which you specify 193 | the output of the model when it gets sent as an HTTP response. 194 | flask-restful-swagger takes advantage of this to specify the fields in 195 | the model""" 196 | 197 | resource_fields = { 198 | "a_string": fields.String(attribute="a_string_field_name"), 199 | "a_formatted_string": fields.FormattedString, 200 | "an_int": fields.Integer, 201 | "a_bool": fields.Boolean, 202 | "a_url": fields.Url, 203 | "a_float": fields.Float, 204 | "an_float_with_arbitrary_precision": fields.Arbitrary, 205 | "a_fixed_point_decimal": fields.Fixed, 206 | "a_datetime": fields.DateTime, 207 | "a_list_of_strings": fields.List(fields.String), 208 | "a_nested_attribute": fields.Nested( 209 | ModelWithResourceFields.resource_fields 210 | ), 211 | "a_list_of_nested_types": fields.List( 212 | fields.Nested(ModelWithResourceFields.resource_fields) 213 | ), 214 | } 215 | 216 | # Specify which of the resource fields are required 217 | required = ["a_string"] 218 | 219 | 220 | class MarshalWithExample(Resource): 221 | @swagger.operation( 222 | notes="get something", 223 | responseClass=TodoItemWithResourceFields, 224 | nickname="get", 225 | ) 226 | @marshal_with(TodoItemWithResourceFields.resource_fields) 227 | def get(self, **kwargs): 228 | return {}, 200, {"Access-Control-Allow-Origin": "*"} 229 | 230 | 231 | # 232 | # Actually setup the Api resource routing here 233 | # 234 | api1.add_resource(TodoList, "/todos1") 235 | api1.add_resource(Todo, "/todos1/") 236 | api1.add_resource(MarshalWithExample, "/marshal_with1") 237 | api2.add_resource(TodoList, "/todos2") 238 | api2.add_resource(Todo, "/todos2/") 239 | api2.add_resource(MarshalWithExample, "/marshal_with2") 240 | 241 | 242 | @app.route("/docs") 243 | def docs(): 244 | return redirect("/static/docs.html") 245 | 246 | 247 | if __name__ == "__main__": 248 | TodoItemWithResourceFields() 249 | TodoItem(1, 2, "3") 250 | app.register_blueprint(my_blueprint1, url_prefix="/api1") 251 | app.register_blueprint(my_blueprint2, url_prefix="/api2") 252 | app.run(host='0.0.0.0', debug=True) 253 | -------------------------------------------------------------------------------- /tests/test_add_model.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_restful_swagger import swagger 4 | from tests.fixtures_add_model import ( 5 | fixtures_add_model_get_docs, 6 | fixtures_add_model_init, 7 | fixtures_add_model_init_parsing_args, 8 | fixtures_add_model_no_properties, 9 | fixtures_add_model_with_resource_fields_nested_swagger_metadata, 10 | fixtures_add_model_with_resource_fields_with_nested, 11 | fixtures_add_model_with_resource_fields_without_swagger_metadata, 12 | fixtures_integration_test_add_model, 13 | patch_deduce_swagger_type, 14 | patch_dir, 15 | patch_getargspec, 16 | patch_hasattr, 17 | patch_isinstance, 18 | patch_parse_doc, 19 | patch_registry, 20 | ) 21 | 22 | try: 23 | from unittest.mock import patch 24 | except ImportError: 25 | from mock import patch 26 | 27 | 28 | @pytest.mark.parametrize( 29 | "test_input,properties,required,defaults", 30 | fixtures_integration_test_add_model, 31 | ) 32 | def test_integration_test_add_model( 33 | test_input, properties, required, defaults 34 | ): 35 | """Integration test for `add_model(...)` method. 36 | 37 | Ensures models are added to `registry["models"]` with expected structure. 38 | Example each model should have 'description', 'id','notes', 'properties', 39 | etc. 40 | Example `registry["models"]`: 41 | # print(registry["models"]) 42 | { 'models': { ..... 43 | 'MockTodoItem': { 'description': 'This is an example of a ' 44 | 'model class that has ' 45 | 'parameters in its ' 46 | 'constructor', 47 | 'id': 'MockTodoItem', 48 | 'notes': 'and the fields in the swagger spec ' 49 | 'are derived from the ' 50 | 'parameters
    to __init__.
    In ' 51 | 'this case we would have args, arg2 ' 52 | 'as required parameters and arg3 ' 53 | 'as
    optional parameter.', 54 | 'properties': { 'arg1': {'type': 'string'}, 55 | 'arg2': {'type': 'string'}, 56 | 'arg3': { 'default': '123', 57 | 'type': 'string'}}, 58 | 'required': ['arg1', 'arg2']}, 59 | .......... 60 | """ 61 | with patch_registry() as registry: 62 | swagger.add_model(test_input) 63 | 64 | assert test_input.__name__ in registry["models"] 65 | assert "description" in registry["models"][test_input.__name__] 66 | assert "notes" in registry["models"][test_input.__name__] 67 | 68 | if "resource_fields" not in dir(test_input) and "__init__" not in dir( 69 | test_input 70 | ): 71 | # in py2, classes without __init__ or resource_fields defined 72 | # will cause issues. 73 | # note, no issue in PY3. 74 | pytest.fail( 75 | "do not call without resource_fields or __init__ defined." 76 | ) 77 | 78 | if "resource_fields" in dir(test_input): 79 | if hasattr(test_input, "required"): 80 | assert "required" in registry["models"][test_input.__name__] 81 | elif "__init__" in dir(test_input): 82 | assert "required" in registry["models"][test_input.__name__] 83 | 84 | assert "properties" in registry["models"][test_input.__name__] 85 | 86 | 87 | @pytest.mark.parametrize("input_model", fixtures_add_model_get_docs) 88 | def test_add_model_get_docs(input_model): 89 | """Ensure `_parse_doc(...)` is called without issues""" 90 | with patch_registry(), patch_parse_doc() as mock_parse_doc: 91 | swagger.add_model(input_model) 92 | mock_parse_doc.assert_called_once_with(input_model) 93 | 94 | 95 | @patch("flask_restful_swagger.swagger._Nested", spec=swagger._Nested) 96 | @pytest.mark.parametrize( 97 | "mock_model_class", 98 | fixtures_add_model_with_resource_fields_without_swagger_metadata, 99 | ) 100 | def test_add_model_with_resource_fields_without_swagger_metadata( 101 | mock_nested, mock_model_class, 102 | ): 103 | """Test adding model with resource fields, no init, without swagger metadata. 104 | """ 105 | pdst = patch_deduce_swagger_type 106 | pr = patch_registry 107 | ppd = patch_parse_doc 108 | pha = patch_hasattr 109 | 110 | with pr(), ppd(), patch_isinstance(False) as mock_isinstance: 111 | with pha() as mock_hasattr, patch_dir(["resource_fields"]) as mock_dir: 112 | with pdst() as mock_deduce_swagger_type: 113 | 114 | swagger.add_model(mock_model_class) 115 | mock_dir.assert_called_with(mock_model_class) 116 | assert mock_dir.call_count == 2 117 | mock_hasattr.assert_called_once_with( 118 | mock_model_class, "required") 119 | mock_isinstance.assert_called_with( 120 | mock_model_class, mock_nested) 121 | assert mock_deduce_swagger_type.call_count == len( 122 | mock_model_class.resource_fields.items() 123 | ) 124 | 125 | 126 | @pytest.mark.parametrize( 127 | "model_class", fixtures_add_model_with_resource_fields_with_nested 128 | ) 129 | def test_add_model_with_resource_fields_with_nested(model_class,): 130 | """Test for model with resource fields, nested subclass 131 | 132 | * resource_fields: YES 133 | * nested subclass: YES 134 | * __init__: NO 135 | * swagger_metadata:NO 136 | 137 | """ 138 | pdst = patch_deduce_swagger_type 139 | pr = patch_registry 140 | ppd = patch_parse_doc 141 | pha = patch_hasattr 142 | 143 | with pr(), ppd(), patch_isinstance(True) as mock_isinstance: 144 | with pha() as mock_hasattr, patch_dir(["resource_fields"]) as mock_dir: 145 | with pdst() as mock_deduce_swagger_type: 146 | 147 | swagger.add_model(model_class) 148 | mock_dir.assert_called_with(model_class) 149 | assert mock_dir.call_count == 2 150 | mock_hasattr.assert_called_once_with(model_class, "required") 151 | mock_isinstance.assert_called_with( 152 | model_class, swagger._Nested) 153 | assert mock_deduce_swagger_type.call_count == len( 154 | model_class.resource_fields.items() 155 | ) 156 | 157 | 158 | @pytest.mark.parametrize( 159 | "model_class", 160 | fixtures_add_model_with_resource_fields_nested_swagger_metadata, 161 | ) 162 | def test_add_model_with_resource_fields_nested_swagger_metadata(model_class,): 163 | """Test for model with resource fields, nested subclass, swagger metadata 164 | 165 | * resource_fields: YES 166 | * nested subclass: YES 167 | * __init__: NO 168 | * swagger_metadata:YES 169 | """ 170 | pdst = patch_deduce_swagger_type 171 | pr = patch_registry 172 | ppd = patch_parse_doc 173 | pha = patch_hasattr 174 | 175 | with pr(), ppd(), patch_isinstance(True) as mock_isinstance: 176 | with pha() as mock_hasattr: 177 | with patch_dir(["resource_fields"]) as mock_dir: 178 | with pdst() as mock_deduce_swagger_type: 179 | swagger.add_model(model_class) 180 | 181 | mock_dir.assert_called_with(model_class) 182 | assert mock_dir.call_count == 2 183 | mock_hasattr.assert_called_once_with( 184 | model_class, "required") 185 | mock_isinstance.assert_called_with( 186 | model_class, swagger._Nested) 187 | assert mock_deduce_swagger_type.call_count == len( 188 | model_class.resource_fields.items() 189 | ) 190 | 191 | 192 | @pytest.mark.parametrize("model_class", fixtures_add_model_init) 193 | def test_add_model_init(model_class): 194 | """Test for model with only init 195 | 196 | * resource_fields: NO 197 | * nested subclass: NO 198 | * __init__: YES 199 | * swagger_metadata: NO 200 | """ 201 | pdst = patch_deduce_swagger_type 202 | pr = patch_registry 203 | ppd = patch_parse_doc 204 | pgas = patch_getargspec 205 | pha = patch_hasattr 206 | 207 | with pdst() as mock_deduce_swagger_type: 208 | with patch_dir(["__init__"]), pr(), ppd(), pgas() as mock_getargspec: 209 | with pha() as mock_hasattr: 210 | swagger.add_model(model_class) 211 | mock_getargspec.assert_called_once_with(model_class.__init__) 212 | mock_hasattr.assert_not_called() 213 | mock_deduce_swagger_type.assert_not_called() 214 | 215 | 216 | @pytest.mark.parametrize("model_class", fixtures_add_model_no_properties) 217 | def test_add_model_no_init(model_class): 218 | """Test for model with only init 219 | 220 | * resource_fields: NO 221 | * nested subclass: NO 222 | * __init__: NO 223 | * swagger_metadata: NO 224 | """ 225 | pdst = patch_deduce_swagger_type 226 | pr = patch_registry 227 | ppd = patch_parse_doc 228 | pgas = patch_getargspec 229 | pha = patch_hasattr 230 | 231 | with pdst() as mock_deduce_swagger_type: 232 | with pr(), ppd(), pgas() as mock_getargspec: 233 | with pha() as mock_hasattr: 234 | swagger.add_model(model_class) 235 | mock_getargspec.assert_not_called() 236 | mock_hasattr.assert_not_called() 237 | mock_deduce_swagger_type.assert_not_called() 238 | 239 | 240 | @pytest.mark.parametrize( 241 | "model_class,required,defaults", fixtures_add_model_init_parsing_args 242 | ) 243 | def test_add_model_init_parsing_args(model_class, required, defaults): 244 | """Test to verify args parsed correctly 245 | """ 246 | with patch_registry() as registry, patch_parse_doc(), patch_dir( 247 | ["__init__"] 248 | ): 249 | swagger.add_model(model_class) 250 | 251 | assert model_class.__name__ in registry["models"] 252 | assert registry["models"][model_class.__name__]["required"] == required 253 | for key, default_value in defaults: 254 | _name = model_class.__name__ 255 | assert key in registry["models"][_name]["properties"] 256 | assert ( 257 | default_value 258 | == registry["models"][_name]["properties"][key]["default"] 259 | ) 260 | -------------------------------------------------------------------------------- /static/js/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.3 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | (function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source== 9 | c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break; 10 | g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a, 11 | c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e2;a==null&&(a=[]);if(A&& 12 | a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a, 13 | c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b, 14 | a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= 15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]}; 17 | j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a= 20 | i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&& 25 | c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty= 26 | function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"}; 27 | b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a, 28 | b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId= 29 | function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape|| 30 | u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c}; 31 | b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d, 32 | this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); 33 | -------------------------------------------------------------------------------- /flask_restful_swagger/static/lib/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.3.3 2 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | (function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source== 9 | c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break; 10 | g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a, 11 | c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e2;a==null&&(a=[]);if(A&& 12 | a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a, 13 | c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b, 14 | a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck= 15 | function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]}; 17 | j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e>1;d(a[g])=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a= 20 | i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&& 25 | c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty= 26 | function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"}; 27 | b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a, 28 | b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId= 29 | function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape|| 30 | u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c}; 31 | b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d, 32 | this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this); 33 | --------------------------------------------------------------------------------