Flask-API-1.1+dfsg/0000755000175000017500000000000013407030450014123 5ustar jonathanjonathanFlask-API-1.1+dfsg/setup.py0000755000175000017500000000473313407025124015651 0ustar jonathanjonathan#!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function from setuptools import setup import re import os import sys name = 'Flask-API' package = 'flask_api' description = 'Browsable web APIs for Flask.' url = 'http://www.flaskapi.org' author = 'Tom Christie' author_email = 'tom@tomchristie.com' license = 'BSD' install_requires = [ 'Flask >= 0.12.3', ] long_description = """Browsable web APIs for Flask.""" def get_version(package): """ Return package version as listed in `__version__` in `init.py`. """ init_py = open(os.path.join(package, '__init__.py')).read() return re.search("^__version__ = ['\"]([^'\"]+)['\"]", init_py, re.MULTILINE).group(1) def get_packages(package): """ Return root package and all sub-packages. """ return [dirpath for dirpath, dirnames, filenames in os.walk(package) if os.path.exists(os.path.join(dirpath, '__init__.py'))] def get_package_data(package): """ Return all files under the root package, that are not in a package themselves. """ walk = [(dirpath.replace(package + os.sep, '', 1), filenames) for dirpath, dirnames, filenames in os.walk(package) if not os.path.exists(os.path.join(dirpath, '__init__.py'))] filepaths = [] for base, filenames in walk: filepaths.extend([os.path.join(base, filename) for filename in filenames]) return {package: filepaths} setup( name=name, version=get_version(package), url=url, license=license, description=description, long_description=long_description, author=author, author_email=author_email, packages=get_packages(package), package_data=get_package_data(package), install_requires=install_requires, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Flask', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', ] ) Flask-API-1.1+dfsg/Flask_API.egg-info/0000755000175000017500000000000013435271004017351 5ustar jonathanjonathanFlask-API-1.1+dfsg/Flask_API.egg-info/top_level.txt0000644000175000017500000000003213435271004022076 0ustar jonathanjonathanflask_api flask_api/tests Flask-API-1.1+dfsg/Flask_API.egg-info/dependency_links.txt0000644000175000017500000000000113435271004023417 0ustar jonathanjonathan Flask-API-1.1+dfsg/Flask_API.egg-info/SOURCES.txt0000644000175000017500000000203713435271004021237 0ustar jonathanjonathansetup.cfg setup.py Flask_API.egg-info/PKG-INFO Flask_API.egg-info/SOURCES.txt Flask_API.egg-info/dependency_links.txt Flask_API.egg-info/requires.txt Flask_API.egg-info/top_level.txt flask_api/__init__.py flask_api/app.py flask_api/compat.py flask_api/decorators.py flask_api/exceptions.py flask_api/mediatypes.py flask_api/negotiation.py flask_api/parsers.py flask_api/renderers.py flask_api/request.py flask_api/response.py flask_api/settings.py flask_api/status.py flask_api/static/css/bootstrap-tweaks.css flask_api/static/css/default.css flask_api/static/img/glyphicons-halflings-white.png flask_api/static/img/glyphicons-halflings.png flask_api/static/img/grid.png flask_api/static/js/default.js flask_api/templates/base.html flask_api/tests/__init__.py flask_api/tests/test_app.py flask_api/tests/test_exceptions.py flask_api/tests/test_mediatypes.py flask_api/tests/test_negotiation.py flask_api/tests/test_parsers.py flask_api/tests/test_renderers.py flask_api/tests/test_request.py flask_api/tests/test_settings.py flask_api/tests/test_status.pyFlask-API-1.1+dfsg/Flask_API.egg-info/requires.txt0000644000175000017500000000001613435271004021746 0ustar jonathanjonathanFlask>=0.12.3 Flask-API-1.1+dfsg/Flask_API.egg-info/PKG-INFO0000644000175000017500000000166313435271004020454 0ustar jonathanjonathanMetadata-Version: 1.1 Name: Flask-API Version: 1.1 Summary: Browsable web APIs for Flask. Home-page: http://www.flaskapi.org Author: Tom Christie Author-email: tom@tomchristie.com License: BSD Description: Browsable web APIs for Flask. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Framework :: Flask Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Internet :: WWW/HTTP Flask-API-1.1+dfsg/setup.cfg0000644000175000017500000000004613407030450015744 0ustar jonathanjonathan[egg_info] tag_build = tag_date = 0 Flask-API-1.1+dfsg/flask_api/0000755000175000017500000000000013407030450016054 5ustar jonathanjonathanFlask-API-1.1+dfsg/flask_api/renderers.py0000644000175000017500000001074613165536712020444 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask import request, render_template, current_app from flask.globals import _request_ctx_stack from flask_api.mediatypes import MediaType from flask_api.compat import apply_markdown import json import pydoc import re def dedent(content): """ Remove leading indent from a block of text. Used when generating descriptions from docstrings. Note that python's `textwrap.dedent` doesn't quite cut it, as it fails to dedent multiline docstrings that include unindented text on the initial line. """ whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in content.splitlines()[1:] if line.lstrip()] # unindent the content if needed if whitespace_counts: whitespace_pattern = '^' + (' ' * min(whitespace_counts)) content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) return content.strip() def convert_to_title(name): for char in ['-', '_', '.']: name = name.replace(char, ' ') return name.capitalize() class BaseRenderer(object): media_type = None charset = 'utf-8' handles_empty_responses = False def render(self, data, media_type, **options): msg = '`render()` method must be implemented for class "%s"' raise NotImplementedError(msg % self.__class__.__name__) class JSONRenderer(BaseRenderer): media_type = 'application/json' charset = None def render(self, data, media_type, **options): # Requested indentation may be set in the Accept header. try: indent = max(min(int(media_type.params['indent']), 8), 0) except (KeyError, ValueError, TypeError): indent = None # Indent may be set explicitly, eg when rendered by the browsable API. indent = options.get('indent', indent) return json.dumps(data, cls=current_app.json_encoder, ensure_ascii=False, indent=indent) class HTMLRenderer(object): media_type = 'text/html' charset = 'utf-8' def render(self, data, media_type, **options): return data.encode(self.charset) class BrowsableAPIRenderer(BaseRenderer): media_type = 'text/html' handles_empty_responses = True template = 'base.html' def render(self, data, media_type, **options): # Render the content as it would have been if the client # had requested 'Accept: */*'. available_renderers = [ renderer for renderer in request.renderer_classes if not issubclass(renderer, BrowsableAPIRenderer) ] assert available_renderers, 'BrowsableAPIRenderer cannot be the only renderer' mock_renderer = available_renderers[0]() mock_media_type = MediaType(mock_renderer.media_type) if data == '' and not mock_renderer.handles_empty_responses: mock_content = None else: text = mock_renderer.render(data, mock_media_type, indent=4) mock_content = self._html_escape(text) # Determine the allowed methods on this view. adapter = _request_ctx_stack.top.url_adapter allowed_methods = adapter.allowed_methods() endpoint = request.url_rule.endpoint view_name = str(endpoint) view_description = current_app.view_functions[endpoint].__doc__ if view_description: if apply_markdown: view_description = dedent(view_description) view_description = apply_markdown(view_description) else: # pragma: no cover - markdown installed for tests view_description = dedent(view_description) view_description = pydoc.html.preformat(view_description) status = options['status'] headers = options['headers'] headers['Content-Type'] = str(mock_media_type) from flask_api import __version__ context = { 'status': status, 'headers': headers, 'content': mock_content, 'allowed_methods': allowed_methods, 'view_name': convert_to_title(view_name), 'view_description': view_description, 'version': __version__ } return render_template(self.template, **context) @staticmethod def _html_escape(text): escape_table = [ ("&", "&"), ("<", "<"), (">", ">") ] for char, replacement in escape_table: text = text.replace(char, replacement) return text Flask-API-1.1+dfsg/flask_api/__init__.py0000644000175000017500000000007013407026574020176 0ustar jonathanjonathanfrom flask_api.app import FlaskAPI __version__ = '1.1' Flask-API-1.1+dfsg/flask_api/mediatypes.py0000644000175000017500000000716213165536712020615 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals class MediaType(object): def __init__(self, media_type): self.main_type, self.sub_type, self.params = self._parse(media_type) @property def full_type(self): return self.main_type + '/' + self.sub_type @property def precedence(self): """ Precedence is determined by how specific a media type is: 3. 'type/subtype; param=val' 2. 'type/subtype' 1. 'type/*' 0. '*/*' """ if self.main_type == '*': return 0 elif self.sub_type == '*': return 1 elif not self.params or list(self.params.keys()) == ['q']: return 2 return 3 def satisfies(self, other): """ Returns `True` if this media type is a superset of `other`. Some examples of cases where this holds true: 'application/json; version=1.0' >= 'application/json; version=1.0' 'application/json' >= 'application/json; indent=4' 'text/*' >= 'text/plain' '*/*' >= 'text/plain' """ for key in self.params.keys(): if key != 'q' and other.params.get(key, None) != self.params.get(key, None): return False if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type: return False if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type: return False return True def _parse(self, media_type): """ Parse a media type string, like "application/json; indent=4" into a three-tuple, like: ('application', 'json', {'indent': 4}) """ full_type, sep, param_string = media_type.partition(';') params = {} for token in param_string.strip().split(','): key, sep, value = [s.strip() for s in token.partition('=')] if value.startswith('"') and value.endswith('"'): value = value[1:-1] if key: params[key] = value main_type, sep, sub_type = [s.strip() for s in full_type.partition('/')] return (main_type, sub_type, params) def __repr__(self): return "<%s '%s'>" % (self.__class__.__name__, str(self)) def __str__(self): """ Return a canonical string representing the media type. Note that this ensures the params are sorted. """ if self.params: params_str = ', '.join([ '%s="%s"' % (key, val) for key, val in sorted(self.params.items()) ]) return self.full_type + '; ' + params_str return self.full_type def __hash__(self): return hash(str(self)) def __eq__(self, other): # Compare two MediaType instances, ignoring parameter ordering. return ( self.full_type == other.full_type and self.params == other.params ) def parse_accept_header(accept): """ Parses the value of a clients accept header, and returns a list of sets of media types it included, ordered by precedence. For example, 'application/json, application/xml, */*' would return: [ set([, ]), set([]) ] """ ret = [set(), set(), set(), set()] for token in accept.split(','): media_type = MediaType(token.strip()) ret[3 - media_type.precedence].add(media_type) return [media_types for media_types in ret if media_types] Flask-API-1.1+dfsg/flask_api/exceptions.py0000644000175000017500000000401213165536712020621 0ustar jonathanjonathanfrom __future__ import unicode_literals from flask_api import status class APIException(Exception): status_code = status.HTTP_500_INTERNAL_SERVER_ERROR detail = '' def __init__(self, detail=None): if detail is not None: self.detail = detail def __str__(self): return self.detail class ParseError(APIException): status_code = status.HTTP_400_BAD_REQUEST detail = 'Malformed request.' class AuthenticationFailed(APIException): status_code = status.HTTP_401_UNAUTHORIZED detail = 'Incorrect authentication credentials.' class NotAuthenticated(APIException): status_code = status.HTTP_401_UNAUTHORIZED detail = 'Authentication credentials were not provided.' class PermissionDenied(APIException): status_code = status.HTTP_403_FORBIDDEN detail = 'You do not have permission to perform this action.' class NotFound(APIException): status_code = status.HTTP_404_NOT_FOUND detail = 'This resource does not exist.' # class MethodNotAllowed(APIException): # status_code = status.HTTP_405_METHOD_NOT_ALLOWED # detail = 'Request method "%s" not allowed.' # def __init__(self, method, detail=None): # self.detail = (detail or self.detail) % method class NotAcceptable(APIException): status_code = status.HTTP_406_NOT_ACCEPTABLE detail = 'Could not satisfy the request Accept header.' class UnsupportedMediaType(APIException): status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE detail = 'Unsupported media type in the request Content-Type header.' class Throttled(APIException): status_code = status.HTTP_429_TOO_MANY_REQUESTS detail = 'Request was throttled.' # def __init__(self, wait=None, detail=None): # if wait is None: # self.detail = detail or self.detail # self.wait = None # else: # format = (detail or self.detail) + ' ' + self.extra_detail # self.detail = format % (wait, wait != 1 and 's' or '') # self.wait = math.ceil(wait) Flask-API-1.1+dfsg/flask_api/status.py0000644000175000017500000000421013165536712017763 0ustar jonathanjonathan# coding: utf8 """ Descriptive HTTP status codes, for code readability. See RFC 2616 and RFC 6585. RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html RFC 6585: http://tools.ietf.org/html/rfc6585 """ from __future__ import unicode_literals def is_informational(code): return code >= 100 and code <= 199 def is_success(code): return code >= 200 and code <= 299 def is_redirect(code): return code >= 300 and code <= 399 def is_client_error(code): return code >= 400 and code <= 499 def is_server_error(code): return code >= 500 and code <= 599 HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 HTTP_200_OK = 200 HTTP_201_CREATED = 201 HTTP_202_ACCEPTED = 202 HTTP_203_NON_AUTHORITATIVE_INFORMATION = 203 HTTP_204_NO_CONTENT = 204 HTTP_205_RESET_CONTENT = 205 HTTP_206_PARTIAL_CONTENT = 206 HTTP_207_MULTI_STATUS = 207 HTTP_300_MULTIPLE_CHOICES = 300 HTTP_301_MOVED_PERMANENTLY = 301 HTTP_302_FOUND = 302 HTTP_303_SEE_OTHER = 303 HTTP_304_NOT_MODIFIED = 304 HTTP_305_USE_PROXY = 305 HTTP_306_RESERVED = 306 HTTP_307_TEMPORARY_REDIRECT = 307 HTTP_308_PERMANENT_REDIRECT = 308 HTTP_400_BAD_REQUEST = 400 HTTP_401_UNAUTHORIZED = 401 HTTP_402_PAYMENT_REQUIRED = 402 HTTP_403_FORBIDDEN = 403 HTTP_404_NOT_FOUND = 404 HTTP_405_METHOD_NOT_ALLOWED = 405 HTTP_406_NOT_ACCEPTABLE = 406 HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407 HTTP_408_REQUEST_TIMEOUT = 408 HTTP_409_CONFLICT = 409 HTTP_410_GONE = 410 HTTP_411_LENGTH_REQUIRED = 411 HTTP_412_PRECONDITION_FAILED = 412 HTTP_413_REQUEST_ENTITY_TOO_LARGE = 413 HTTP_414_REQUEST_URI_TOO_LONG = 414 HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 HTTP_417_EXPECTATION_FAILED = 417 HTTP_428_PRECONDITION_REQUIRED = 428 HTTP_429_TOO_MANY_REQUESTS = 429 HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE = 431 HTTP_444_CONNECTION_CLOSED_WITHOUT_RESPONSE = 444 HTTP_500_INTERNAL_SERVER_ERROR = 500 HTTP_501_NOT_IMPLEMENTED = 501 HTTP_502_BAD_GATEWAY = 502 HTTP_503_SERVICE_UNAVAILABLE = 503 HTTP_504_GATEWAY_TIMEOUT = 504 HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 HTTP_508_LOOP_DETECTED = 508 HTTP_510_NOT_EXTENDED = 510 HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511 Flask-API-1.1+dfsg/flask_api/static/0000755000175000017500000000000013407030450017343 5ustar jonathanjonathanFlask-API-1.1+dfsg/flask_api/static/css/0000755000175000017500000000000013435271004020136 5ustar jonathanjonathanFlask-API-1.1+dfsg/flask_api/static/css/bootstrap-tweaks.css0000644000175000017500000000531413165536712024176 0ustar jonathanjonathan/* This CSS file contains some tweaks specific to the included Bootstrap theme. It's separate from `style.css` so that it can be easily overridden by replacing a single block in the template. */ .form-actions { background: transparent; border-top-color: transparent; padding-top: 0; } .navbar-inverse .brand a { color: #999; } .navbar-inverse .brand:hover a { color: white; text-decoration: none; } /* custom navigation styles */ .wrapper .navbar{ width: 100%; position: absolute; left: 0; top: 0; } .navbar .navbar-inner{ background: #2C2C2C; color: white; border: none; border-top: 5px solid #A30000; border-radius: 0px; } .navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{ color: white; } .nav-list > .active > a, .nav-list > .active > a:hover { background: #2c2c2c; } .navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{ color: #A30000; } .navbar .navbar-inner .dropdown-menu li a:hover{ background: #eeeeee; color: #c20000; } /*=== dabapps bootstrap styles ====*/ html{ width:100%; background: none; } body, .navbar .navbar-inner .container-fluid { max-width: 1150px; margin: 0 auto; } body{ background: url("../img/grid.png") repeat-x; background-attachment: fixed; } #content{ margin: 0; } /* sticky footer and footer */ html, body { height: 100%; } .wrapper { min-height: 100%; height: auto !important; height: 100%; margin: 0 auto -60px; } .form-switcher { margin-bottom: 0; } .well { -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } .well .form-actions { padding-bottom: 0; margin-bottom: 0; } .well form { margin-bottom: 0; } .well form .help-block { color: #999; } .nav-tabs { border: 0; } .nav-tabs > li { float: right; } .nav-tabs li a { margin-right: 0; } .nav-tabs > .active > a { background: #f5f5f5; } .nav-tabs > .active > a:hover { background: #f5f5f5; } .tabbable.first-tab-active .tab-content { border-top-right-radius: 0; } #footer, #push { height: 60px; /* .push must be the same height as .footer */ } #footer{ text-align: right; } #footer p { text-align: center; color: gray; border-top: 1px solid #DDD; padding-top: 10px; } #footer a { color: gray; font-weight: bold; } #footer a:hover { color: gray; } .page-header { border-bottom: none; padding-bottom: 0px; margin-bottom: 20px; } /* custom general page styles */ .hero-unit h2, .hero-unit h1{ color: #A30000; } body a, body a{ color: #A30000; } body a:hover{ color: #c20000; } #content a span{ text-decoration: underline; } .request-info { clear:both; } Flask-API-1.1+dfsg/flask_api/static/css/default.css0000644000175000017500000000202313165536712022303 0ustar jonathanjonathan /* The navbar is fixed at >= 980px wide, so add padding to the body to prevent content running up underneath it. */ h1 { font-weight: 500; } h2, h3 { font-weight: 300; } .resource-description, .response-info { margin-bottom: 2em; } .version:before { content: "v"; opacity: 0.6; padding-right: 0.25em; } .version { font-size: 70%; } .format-option { font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace; } .button-form { float: right; margin-right: 1em; } ul.breadcrumb { margin: 58px 0 0 0; } form select, form input, form textarea { width: 90%; } form select[multiple] { height: 150px; } /* To allow tooltips to work on disabled elements */ .disabled-tooltip-shield { position: absolute; top: 0; right: 0; bottom: 0; left: 0; } .errorlist { margin-top: 0.5em; } pre { overflow: auto; word-wrap: normal; white-space: pre; font-size: 12px; } .page-header { border-bottom: none; padding-bottom: 0px; margin-bottom: 20px; } Flask-API-1.1+dfsg/flask_api/static/img/0000755000175000017500000000000013407030450020117 5ustar jonathanjonathanFlask-API-1.1+dfsg/flask_api/static/img/grid.png0000644000175000017500000000266213165536712021575 0ustar jonathanjonathanPNG  IHDRZ2zxǁ pHYs  niTXtXML:com.adobe.xmp ? AIDATxrU@?6@cR‰5<]f,M[13?Ûvffۣޜ/{zt/#tD#BG:"tD#BG9Ɔ%aqtD:"tD#BG:"tD# oaAm4v:"tD#BG:"tD#BG%=,GGD#BG:"tD#BG:ba8:"BG:"tD#BG:"tD K4:"tD#BG:"tD#BGذDlX9#BG:"tD#BG:"t'G|}4:"tD#BG:"tD#BGxK{X9#BG:"tD#BG:"tĆ%bqtD:"tD#BG:"tD#6,h#"tD#BG:"tD#BGaذDs|tls4ȕCo[]A':劎:"tD#BG:rO/x ޞmX~}xq|Xk=GGd %#BG1Y?!cK: #BG{9fǙ8ۆMwk?yގg{#B߷M~0⊎1W#B{rymX^?8Yk}2y#E\ё+;u[:ˎ/I#G6{:ṙym}ur۰xZki|tlݷ)|EoE#BGݻdї:"tDБc>wcS簜8Z\`γ_KgW [l:"tD#BG Θ#'IENDB`Flask-API-1.1+dfsg/flask_api/static/img/glyphicons-halflings.png0000644000175000017500000003073213165536712024773 0ustar jonathanjonathanPNG  IHDR1IDATx}ml\EW^ɺD$|nw';vю8m0kQSnSV;1KGsԩ>UoTU1cƖYuּca&#C,pؚ>kں ULW -sn3Vq~NocI~L{- H8%_M£wB6EW,ĢpY2+(Y@&A/3kXhߍ-aA<>P'\J;(}#Qz:4%m?nfntK*l9J+DIYu1YZ^(]YYEf@ОlXz]Ut u &5-PW}@t|#LY=s܂,w#+R+?Ƌax X0"ea)tG*ԡwVwV^rf%xB(qּ4>WG#lWU<ЁXJVѶlR$kDVrI7:X%X1NEzw;y9z9O%~~uɗ*=Ixcy}Y(ou ±N$^j e\iX񝜬];Y-rѲ&>!zlYaVHVN԰9=]=mRMdOUC JUiT}rWW'ڹu)ʢF"YU#P׾&ܑЅROwyzm$Os? +^FTIEq%&~ >M}]ԖwA? [Nteexn(措BdMTpʥnqqS?bWXmW6x*{V_!VjΧsVL^j XkQjU6sk̩n~[qǸ-` O:G7l"ksRe2vQ=QƼJUX`gQy~ ďKȰE]#P:td\T/u;س:Jc-%'e q ?j"/yh48Zi1|JUu>_N;hxwNU JQU7\j̮bT:B?6oJ1Ί%I UY-Ii4{=rǤ7@)HKJ+f4X8Cd?'j1 N< 39EWo VTGzg# %D0#ܠ3[tiآ( U,]125|Ṋfw7w u+Š]Db]K xbW ՛7|ВX㕛{UcGXk¬|(h)IUa)lp 3luPU]D)/7~4Wt5J}V X0z VM;>Gԙ^|gF:jaZ^)74C#jwr,еSlGu;1vm><)}ZQՖ&mZ:1UMB~ a:/᜗:KWWOҠ&Y2f7cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘g*3fF5LbN2#Tf=C`!ZGUe꣇e2V<1mkS4iϗ*.{N8Xaj~ڀnAx,%fE:|YDVj ¢lg6(:k~MM5?4 ]WO>诋WZiG|QGJeK[YcյpmjE\f/ǎ8&OQ3 .3tt2'-V8pXSrY#J!Q ",ub@FK:u^iy[]<.Cw+W\)b kr-.MtڀMqʄ۰#$^X$"V`T4m~w%Pp1|+&UxY8*r8:k7QЃҀT$Ўƙ S>~Sjs:5q.w&_Z.X=:ވbw` _kd{'0:ds#qi!224nq\9-KUTsSUuVo@;Uz>^=Np>oPO @I@'Gj5o*U>^*ew>ͫʧ᫠Q5 ̈́<$#5Jٻj6e)_ d]2B:^(*:8JYS鬆Kݗ ]U4_rj{5ׇaǑ/yV?GtGb@xPU7O3|鍪 IQ5QGw *(;wf0*PUU<YƔvbt5{2!,}Ҧ:)j2OkΪ' ֊0I.q\(%ojQĖՇa<ԍexAgt'[d;׸`rcdjPFU$UeJI6T&Z}z(z vfuz {}ۿߝݞlxUZ謊.Y岟b%nw@ǩS9|źs%>_o#9\EU~/ځt(r[QZuOo;!MrU]0TcpDő?.cPuF;L_Sb}R/J_+h2$ai UǩS9>Є}76rzu~国4oĨ 1J ^̘~iC޸55G׹]gwsn zTuO=?/zƲc>Οb#7ֻcgkޛTUj*-T=]uu}>ݨNЭ [ ]:%/_ Sz]6D.mD7Uƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1c>J4hPP+A;'G_XKmL5I.},wFFum$S-E-;Õ C3I-`BRx1ғTJݕ;hΊ8 DYJo;Yš5MKɰM;%Pd9KhnD[zgVh,'C p!^M(WK2X>UQ%^p8 ˽^#Ζ؄+.@gCz%ɔ-Pr KX n>=ՔѨeSvRLz5%9UQS \WիK'hp)ô Jrh M0F (f_R5///G+x 1"eS 5 :Tf=+7Qɧ\TEs༬rYs8&k#pSՊ5MTbD܊[Ng5Q\s5PB@[8ɨV1&4Wsy[Ǿ wU2V77jމd^~YfC_h;a.&M i UWpzs`>/"'OI۲y:BzdTq£=йb:"m/-/PWDQǴ͐57m`H%AV!Hԛ׿@"Qzދ|ߒT-*OU^Ҧ6!Cwk|h&Hd5LEYy'ƣ7%*{=)Z%ٝP *G]/8Lw$?8M)\į/#7Ufd7'6\h1 vIfEIr=1w\WKVZHKgZ͡$mx % `j}TuTQJZ*H>*QxkLFTyU-)ôbiA|q`F'+ 4^Qy xH)#t^?@]^`ARSqjgB:rK۷l<2-4YKhgQLxVwP~M Φ0l 3ƅaŊITȀhwJmxIMչ|U7xˆS~2ߕ?kW1kC3];YnSґAeXYz8,'x< k7Kx]$x$vgT#w;o@ z_Vmn|HֵhZg-^TAn- )@4[*9xKƋj>!,Vt:eqn8%ohS(2\Q^aigF3vTUDVlQꅧWc%Ueq4ҝº/U $_Q!>t| ,țG<tC[xTXmf|Q%d#jUՆ|; H[bά#,Ws7NT1~m&ǻ{' \㟾 bBKJo8%!$Qj:/RX)$Sy޳ 䍧RDUg_D軦J\jN֖SU;~?Ohssdƣ}6(T <_4b5 ^N N%8QejF7toMyө`)g[/|?өJuGL坕/=CTܠhdifHcǞG4,`D՞{'xG_p/5@m +$jVH3a"*ũ,,HJҵȸT^Qyo&IÉJUVwWLeM~3tA6rwɤ6տ \0HL%LX5c@HHÃZ|NV+7WM{cig*ȸU7iÉбzd * ?gtX8̝OX:]2ɍ]p^++>AVڛE{ DB.&/56ArxY#ܕy)cKQtȪ~! ;C}ʃtf{6$NVsj wupZ)zŁ|-wg+nMVj/d+U~ͯi:_ix whqr>駃-x뼬)ݷyR=! ì:J/lIkV@n74758Z KJ(Uxz1w)^\ԣzȪ󲦨c2f؍v+6f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘2N oC\F1ִ UZJV̚\4Mgq1z{&YT ,HX~D u\g}x>+YdN̮ol ZX+F[/j+S~2/jV8Jr^ԉ]J}J*ۏ<2԰&JݣjOM@ѯ#0O[SXB^ uze\]dd./xXE f'vO_H${%;kt7ށmő|d{aފ^ǛڎE5ʋBr]W=_SAf(0 oU5q ,_\luz˪uz㻲o=Yi~| 0+=VJت /ލzM\zCL[U:|k*^8"\Wٚ\ .XTjX5 SkFu\1 q'mģ/QUؕ*AɽDNZ׮?_[# ˍ4:^j|5LG ||øBW{6[uQF.1$qF9IHg)\5>C#uXZ$#*<ߐsRv1Tj>Jm>*#( [Fhsש5*jQʼ&&&P犛L[Q1* ;X}Iΰ[Q?qQZ Hݙ֞VEsBCZ9JTK tup˷ /O,.kUdsOHMg4=-)+ؿh2Nw/r|WQn=GIU;'j,vfdzpe$V GTYsBZO1pj:r"nTUSCgr veAۘ˜FC+Ֆ#[JTe'v9-3 Dmӻuuz?0 o hxuY &_54=f07kלU0]D:jdw/+PGUVS<\2uatc^zYRąmC+7#,|:iNw*|^sm|X>Ъ^1\#͹ &%{,2U>ݎ.c05z# ogNO+Q쓭 ,˗-%K\[S_`y+b_94"U+Ύap}I[M,B.NtwHj漬E L߀ 0DX(kڵ NoU{gquz RwkէRx'uZ[3'zyyד%sƕ3jYF\s=m1&VAɼ?k\+]6yモ1gtOIW7al|1 >$]e 7؝WIe?ަL#>| ҭ] pM5MUdI61ԠeǼYGhOn3խR:^k_'Yuuq#p# J2xl>OjcY馃!ڡ+sZ/ D}2AY mpc#<'xSKx`*W[,e|6BH)㶤kjpDU(2qzx9*tqa/, Z[ 0>Ө֜xN)fă@qըFU՝w(a;ˋ>|Tc|w2eiT]*!_\WG{ ]^݅Z5t|6oYHaO@= my^akE.uz]#٥hWv(:,6A߉JFa\ wWex>vetuMYA>).,;ɦCbwjE)W Fӫ@s4e6^Q9oI}4x<.B?B߫#$Hx.x9,a!RTpgd5xBe.L7@* AsduttSVUaRU|I xG߃$T񭟬#_IFMŒ_X@foQIDII?|%$r {ENĸwޕqq?Dؽ}}o/`ӣCTi /ywO rD 9YUD] Ή@s]+'UaL} hrU'7:sU|k)H@hNq#ϵ8y˭Xű#w 1!흉R'7fuד0p!WÖW+Nmp\-ioD$g٠˅%%ÐmV]̱rw*Z}y+L Nouj}xt)lStuqxmNyKUOnDbhf}k>6ufT%{ <񐮸mjFcmUïc;w8@dGFUA& =nq5]iP}z:k⼶-ʓ Κl*'UzaxWFdZzTNRs+# wzgi:MBqtM l#^'Gߣ*^t{=rERnQ$adJl02%Tڊ^<~g?Of*U^?:N+o[PUs|QR']V-L)H K䐞 mYn\4}YVD hR;g-'3aסM Dh}1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌk*Ț4`L$b U4\dt'>HȄ|.+Y+/Gy2OCWv3v,'kia W O6߯E=Hv $LlxI躍/}^]x\3 ɮ5 QT&G9Ay^i}O[5ޱwq4,s JJI.myE^%'VB~dׯ}*j* ~uTk\fKЬ*Y]_v'I˨鑩6Xo'j&uɧngT]oڌ9\*wVHӖ| >:5EF'J ɝ`!A e~_;5ױϊ镋m_&OVi<}"靍hW9X6KPƣ G"ƭ?/O^hCHLciPj)}QQզ#tMg9 xGw~d;_J+RỲ<;e 5/Qs/5N[!a+NPb+ѺI}-t_qU=MKʞY5no*vvbʊ{]| ~ Z{-끇^FVviϵ3Ya=6ndS;-ʹ^;uꪪ^ |=_w+"i&4l#wir|W3U$"J~O@]~tRJVMHw:̦@?>O?vdrtS*$&~1>Z}^nL(]f*&*QaIꝄ|3*O?r?*4Gyz[k/tkQϖWCCKk/x5|S*`ϹγQEwy o KYqTb$-/PtsZNKQ*>ݢU@Џ"JQ;¹& Lx;+T /+O赟> (T?ķD^N*'p$IW֐W~ =J|_UTe7ְP`;CYjk=sU[mߙ-;};2|wo1p0~>0m @Jrǟcٷ4͜?q\UUIV?2L/+Шꄾ< ܇^T ?tj\JrҀB*=km X,n}aՒIadp׷ll{\6v8RꅟҲf1F|Տ;e=\D ,D:ψrxQT◎*|{nS 9~=}ӕG~%j:Dj<ឫ:jO% $T8!jvm|'OЗ¹➱z\vsIv`Ȕʨj-^$-^G Q{m`T#c֞㸝|n.ߪN$O JUVʼt,jg-mסּNV z:(Ι*|1Ux=Yk*t MNNDUhK ؞X(刄Rv!#B_cxRŹoE5Dg>?fXQQ˔|@"աMveC>mO$H#]Y I=)_`k* :a>!X!W^wҒl'<;vwgIt_?Jh`#E:fdx=6Wu<Ӌd2di˂c#h¬c4?<HFYoVpN;ݷJ\ >` (t3{>⦊;;qFx4YcS$w.da*k|Q,+xs^K߫P^nO֮L5mIwl?-.ʲJ8 F B.-:2Ȕ!/A#b_m%I($|PZ[1G{^#o>3mw?'cx[^:Wk/`'=~֥W(gQbfv7UzM3+؍K:4|GCtA+Kʨ{@Ɩ [05E|yn4MIENDB`Flask-API-1.1+dfsg/flask_api/static/img/glyphicons-halflings-white.png0000644000175000017500000002111113165536712026100 0ustar jonathanjonathanPNG  IHDRӳ{PLTEmmmⰰᒒttt󻻻bbbeeeggg𶶶xxx󛛛Ƽ몪֢UUU鿿rOtRNS#_ /oS?C kDOS_6>4!~a @1_'onҋM3BQjp&%!l"Xqr; A[<`am}43/0IPCM!6(*gK&YQGDP,`{VP-x)h7e1]W$1bzSܕcO]U;Zi'y"؆K 64Y*.v@c.};tN%DI !ZЏ5LH26 ɯ" -bE,,)ʏ B>mn6pmRO wm@V#?'CȑZ#qb|$:)/E%nRqChn%i̓}lm ?idd",`H"r.z~(bQU&)5X#EMR<*p[[%.Ọk7lIoJF lV!̡ăuH`&,zRk$|$lXbjߪdU?Σ$HW$U'HE3*խU\}( zhVk}guRk$%|T|ck獳"D_W+.Q)@ƽHbslTDR2Xm#a 3lYzj㒚#! 4J8(cvt]aT D ΅Q?^-_^$:\V $N|=(vZ'q6Z׆B5V!y3K㱿bv4xR]al!IoP@tVyL٪mlڿIUb|[*lke'*WddDӝ}\W_WߝrN?vޫ۲X%0uoui*JVƦb%}i5IYlNE-wςf_W3mI-mQ)S kTC7m<"܌bT|'$ҘR&>O p6tSN\ׯLm\r@3uT b7t.5.q3r0=8TiJ\6uF R32^'ŪxI F8O{%8kJMSȴdBEdWCYO:/ON/I_=xFE! =i:o~ y?''[͓[͓[͓[͓[ͭ.U>$PƦc%]\c:| ,eSZ,oXrX!R@Zv 0>?* <|N60;{ad2v+D^t[q!۞V}fۨϏYeॗ)Vyl|" fUq@Ǽ4Y-Y-!6aB:o%JIUQ|UKO`=\ :0x Pau@!KPdxhw1>$j΍vZdxSUA&[URd7øzk/rU^w:I.VǮc>q.!zSr&2)Wg R -iQ 8Pa\ОU%iݡU_=p Lu(N?0?Æ:]άtB%U|NsorNf ,P !v" Y6hL_@@bscqgv4||0lϟ$S9bʱj#~?o}}7sAPm:IV=n !{{hEࢪ8suoLT$;VscqD3 ༂3.DBB4&V' T `D6Ϸqyj8V*X%@s\jrN$|=5Ά 'mUiKi%CI:ssaƅ`*`=l)>u՘MeuSI_OL_}o&jzp{lu:O)s%Q@$<]f xO%PCbhr2PKpf5Në3^o]eJiB464^tuٲU֌:G4'22YpuG'/Py4?.SBP_>I 1t3ΓBɭɭɭɭVVVVVs]!67(g y@ 4>Q VF}^Xׇڼje26 L%YGh lC})< !EEPZWZV+@†R 5{@ouɐ4&H6ey V݀VťcqZޒrJyByFzFN$Hb*+jՏqэ ګkݿUXle1d0d^-B%} {Y%r*j5Ak5u",:~ҸY~ hSA~6 fulՇf{ȵQtATHZkƭ/_Sn u']b]|m`BāJ,O$du]Zs FL:aǙT4o~by?wpj滥A(x]†f~an֧/^dڲcՇ,!1i&xi_VK@ip̓9Vi%a; L?0J*Ū5U'x^6V[^ {eU|:0=0d۫o*Jq%[YN.sQLud[29I:WnmXlڃ6!lNlVէKUjV\J%UߊBLcKfb>a=b~R]aG%[js@/9MطݘU>yɲX@} Ftg^vO\Ӹwvpz3K5i!$P>ā'VƛL2r@UMKZ6tw맟¦bm1h||]}~0MjA(JJP68C&yr׉e}j_cJ?I0k>šW |Bޝ."TEXd 8!cw*E(J)![W"j_ТeX_XB;oO0~?:PC (.[!Wq%*leY)E<^KZT60.#A\5;Rmtkd/8)5~^0 #Ckgey)ͶԺ6ĥ<(?&uAVm0^h.txR*a':,H|ō l5z;8+e#b'#|}2w(|KcJ l6 w^Տoi3H R ̔9,YgPְ:N [5SR![)]i}`mN4Хv`|;f(FltL8÷Z#AO%Y)NU5YedJE3dZذݣHT1 ;8MjnʏӤqp 1h^<<>yt{?|'j)}YUU{@V/J1F+7䀉[OWO[ yUY!?BD%DWj>-Ai6xz)U R7 d@g\so)a4zf[W+> P> |qLG8vȣlj2Zt+VA6gT *ʆUz(m)CD `He/.:zN9pgo &NC׃އ>Wհ_Hj)Xe6F7pm-`'c.AZ=^e8F;{Rtn(z!S7o Iew3]bܗ85|iϠRJkʱZRO+8U&:]ZieR(JMޗ7Z@5a^\GzsρU*rMezT^:ɬͦX=>$ bi>U&XQoybbGk8 Ҙn).Սo ^MmdZi$soo*{4eLbLٳ""mx:`:mk[geTެ)'0*TB{!I ''''[͓[͓[͓[͓[]Zj Q.e '/yvQ71(Z&X?(_Z){tڀmZWϏ)-C jqn,̋"IvUL!h꛿skAcrN佚фVE40yX~4zʸV㳰%,)fqtpu~  *^0:ܲ33JO(ZB?K^ v]unlWi0p6[착C_5X#[wX3b廫R{NKAe Se|wxso>P\儔ԕ6;nVmfI$V͓J-J%֌0UwYЎSnum藮xz˗VƫIvnW_qLZ"_Xz 8]Ap?C543zw({7e*Ȳ`۰!AQ:KUnz]1yVGaCm0PY ٚUx6TT&hV9V ӬzÑ 1[XzZ9erqJND/gX*9oN6D` {I%Mz9—TQ7f\"j_3~xB'ܷY]*KЌ%"5"qxq~ƕ=jS>jV&~]2xzF1X_yD<#NRB}K/iy !V^˿eJ}/FkA7 S+.(ecJ:zWZ몖wQ~ä́p6,e5,+,tv%O^OO}ן -O7>ekC6wa_C |9*WA)UJg8=:mjUvqysܒLglC6+[FSWg9wV31A ND<$5e(s[ ۨbaF.]KIENDB`Flask-API-1.1+dfsg/flask_api/static/js/0000755000175000017500000000000013435271004017762 5ustar jonathanjonathanFlask-API-1.1+dfsg/flask_api/static/js/default.js0000644000175000017500000000305413165536712021760 0ustar jonathanjonathanfunction getCookie(c_name) { // From http://www.w3schools.com/js/js_cookies.asp var c_value = document.cookie; var c_start = c_value.indexOf(" " + c_name + "="); if (c_start == -1) { c_start = c_value.indexOf(c_name + "="); } if (c_start == -1) { c_value = null; } else { c_start = c_value.indexOf("=", c_start) + 1; var c_end = c_value.indexOf(";", c_start); if (c_end == -1) { c_end = c_value.length; } c_value = unescape(c_value.substring(c_start,c_end)); } return c_value; } // JSON highlighting. prettyPrint(); // Bootstrap tooltips. $('.js-tooltip').tooltip({ delay: 1000 }); // Deal with rounded tab styling after tab clicks. $('a[data-toggle="tab"]:first').on('shown', function (e) { $(e.target).parents('.tabbable').addClass('first-tab-active'); }); $('a[data-toggle="tab"]:not(:first)').on('shown', function (e) { $(e.target).parents('.tabbable').removeClass('first-tab-active'); }); $('a[data-toggle="tab"]').click(function(){ document.cookie="tabstyle=" + this.name + "; path=/"; }); // Store tab preference in cookies & display appropriate tab on load. var selectedTab = null; var selectedTabName = getCookie('tabstyle'); if (selectedTabName) { selectedTab = $('.form-switcher a[name=' + selectedTabName + ']'); } if (selectedTab && selectedTab.length > 0) { // Display whichever tab is selected. selectedTab.tab('show'); } else { // If no tab selected, display rightmost tab. $('.form-switcher a:first').tab('show'); } Flask-API-1.1+dfsg/flask_api/decorators.py0000644000175000017500000000160413165536712020611 0ustar jonathanjonathanfrom functools import wraps from flask import request def set_parsers(*parsers): def decorator(func): @wraps(func) def decorated_function(*args, **kwargs): if len(parsers) == 1 and isinstance(parsers[0], (list, tuple)): request.parser_classes = parsers[0] else: request.parser_classes = parsers return func(*args, **kwargs) return decorated_function return decorator def set_renderers(*renderers): def decorator(func): @wraps(func) def decorated_function(*args, **kwargs): if len(renderers) == 1 and isinstance(renderers[0], (list, tuple)): request.renderer_classes = renderers[0] else: request.renderer_classes = renderers return func(*args, **kwargs) return decorated_function return decorator Flask-API-1.1+dfsg/flask_api/app.py0000644000175000017500000001150513407025124017212 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask import request, Flask, Blueprint from flask._compat import reraise, string_types, text_type from flask_api.exceptions import APIException from flask_api.request import APIRequest from flask_api.response import APIResponse from flask_api.settings import APISettings from flask_api.status import HTTP_204_NO_CONTENT from itertools import chain from werkzeug.exceptions import HTTPException import re import sys from flask_api.compat import is_flask_legacy api_resources = Blueprint( 'flask-api', __name__, url_prefix='/flask-api', template_folder='templates', static_folder='static' ) def urlize_quoted_links(content): return re.sub(r'"(https?://[^"]*)"', r'"\1"', content) class FlaskAPI(Flask): request_class = APIRequest response_class = APIResponse def __init__(self, *args, **kwargs): super(FlaskAPI, self).__init__(*args, **kwargs) self.api_settings = APISettings(self.config) self.register_blueprint(api_resources) self.jinja_env.filters['urlize_quoted_links'] = urlize_quoted_links def preprocess_request(self): request.parser_classes = self.api_settings.DEFAULT_PARSERS request.renderer_classes = self.api_settings.DEFAULT_RENDERERS return super(FlaskAPI, self).preprocess_request() def make_response(self, rv): """ We override this so that we can additionally handle list and dict types by default. """ status_or_headers = headers = None if isinstance(rv, tuple): rv, status_or_headers, headers = rv + (None,) * (3 - len(rv)) if rv is None and status_or_headers == HTTP_204_NO_CONTENT: rv = '' if rv is None and status_or_headers: raise ValueError('View function did not return a response') if isinstance(status_or_headers, (dict, list)): headers, status_or_headers = status_or_headers, None if not isinstance(rv, self.response_class): if isinstance(rv, (text_type, bytes, bytearray, list, dict)): status = status_or_headers rv = self.response_class(rv, headers=headers, status=status) headers = status_or_headers = None else: rv = self.response_class.force_type(rv, request.environ) if status_or_headers is not None: if isinstance(status_or_headers, string_types): rv.status = status_or_headers else: rv.status_code = status_or_headers if headers: rv.headers.extend(headers) return rv def handle_user_exception(self, e): """ We override the default behavior in order to deal with APIException. """ exc_type, exc_value, tb = sys.exc_info() assert exc_value is e if isinstance(e, HTTPException) and not self.trap_http_exception(e): return self.handle_http_exception(e) if isinstance(e, APIException): return self.handle_api_exception(e) blueprint_handlers = () handlers = self.error_handler_spec.get(request.blueprint) if handlers is not None: blueprint_handlers = handlers.get(None, ()) app_handlers = self.error_handler_spec[None].get(None, ()) if is_flask_legacy(): for typecheck, handler in chain(blueprint_handlers, app_handlers): if isinstance(e, typecheck): return handler(e) else: for typecheck, handler in chain(dict(blueprint_handlers).items(), dict(app_handlers).items()): if isinstance(e, typecheck): return handler(e) reraise(exc_type, exc_value, tb) def handle_api_exception(self, exc): content = {'message': exc.detail} status = exc.status_code return self.response_class(content, status=status) def create_url_adapter(self, request): """ We need to override the default behavior slightly here, to ensure the any method-based routing takes account of any method overloading, so that eg PUT requests from the browsable API are routed to the correct view. """ if request is not None: environ = request.environ.copy() environ['REQUEST_METHOD'] = request.method return self.url_map.bind_to_environ(environ, server_name=self.config['SERVER_NAME']) # We need at the very least the server name to be set for this # to work. if self.config['SERVER_NAME'] is not None: return self.url_map.bind( self.config['SERVER_NAME'], script_name=self.config['APPLICATION_ROOT'] or '/', url_scheme=self.config['PREFERRED_URL_SCHEME']) Flask-API-1.1+dfsg/flask_api/request.py0000644000175000017500000001506313165536712020140 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask import Request from flask_api.negotiation import DefaultNegotiation from flask_api.settings import default_settings from werkzeug.datastructures import MultiDict from werkzeug.urls import url_decode_stream from werkzeug.wsgi import get_content_length from werkzeug._compat import to_unicode import io class APIRequest(Request): parser_classes = default_settings.DEFAULT_PARSERS renderer_classes = default_settings.DEFAULT_RENDERERS negotiator_class = DefaultNegotiation empty_data_class = MultiDict # Request parsing... @property def data(self): if not hasattr(self, '_data'): self._parse() return self._data @property def form(self): if not hasattr(self, '_form'): self._parse() return self._form @property def files(self): if not hasattr(self, '_files'): self._parse() return self._files def _parse(self): """ Parse the body of the request, using whichever parser satifies the client 'Content-Type' header. """ if not self.content_type or not self.content_length: self._set_empty_data() return negotiator = self.negotiator_class() parsers = [parser_cls() for parser_cls in self.parser_classes] options = self._get_parser_options() try: parser, media_type = negotiator.select_parser(parsers) ret = parser.parse(self.stream, media_type, **options) except: # Ensure that accessing `request.data` again does not reraise # the exception, so that eg exceptions can handle properly. self._set_empty_data() raise if parser.handles_file_uploads: assert isinstance(ret, tuple) and len(ret) == 2, 'Expected a two-tuple of (data, files)' self._data, self._files = ret else: self._data = ret self._files = self.empty_data_class() self._form = self._data if parser.handles_form_data else self.empty_data_class() def _get_parser_options(self): """ Any additional information to pass to the parser. """ return {'content_length': self.content_length} def _set_empty_data(self): """ If the request does not contain data then return an empty representation. """ self._data = self.empty_data_class() self._form = self.empty_data_class() self._files = self.empty_data_class() # Content negotiation... @property def accepted_renderer(self): if not hasattr(self, '_accepted_renderer'): self._perform_content_negotiation() return self._accepted_renderer @property def accepted_media_type(self): if not hasattr(self, '_accepted_media_type'): self._perform_content_negotiation() return self._accepted_media_type def _perform_content_negotiation(self): """ Determine which of the available renderers should be used for rendering the response content, based on the client 'Accept' header. """ negotiator = self.negotiator_class() renderers = [renderer() for renderer in self.renderer_classes] self._accepted_renderer, self._accepted_media_type = negotiator.select_renderer(renderers) # Method and content type overloading. @property def method(self): if not hasattr(self, '_method'): self._perform_method_overloading() return self._method @property def content_type(self): if not hasattr(self, '_content_type'): self._perform_method_overloading() return self._content_type @property def content_length(self): if not hasattr(self, '_content_length'): self._perform_method_overloading() return self._content_length @property def stream(self): if not hasattr(self, '_stream'): self._perform_method_overloading() return self._stream def _perform_method_overloading(self): """ Perform method and content type overloading. Provides support for browser PUT, PATCH, DELETE & other requests, by specifing a '_method' form field. Also provides support for browser non-form requests (eg JSON), by specifing '_content' and '_content_type' form fields. """ self._method = super(APIRequest, self).method self._stream = super(APIRequest, self).stream self._content_type = self.headers.get('Content-Type') self._content_length = get_content_length(self.environ) if (self._method == 'POST' and self._content_type == 'application/x-www-form-urlencoded'): # Read the request data, then push it back onto the stream again. body = self.get_data() data = url_decode_stream(io.BytesIO(body)) self._stream = io.BytesIO(body) if '_method' in data: # Support browser forms with PUT, PATCH, DELETE & other methods. self._method = data['_method'] if '_content' in data and '_content_type' in data: # Support browser forms with non-form data, such as JSON. body = data['_content'].encode('utf8') self._stream = io.BytesIO(body) self._content_type = data['_content_type'] self._content_length = len(body) # Misc... @property def full_path(self): """ Werzueg's full_path implementation always appends '?', even when the query string is empty. Let's fix that. """ if not self.query_string: return self.path return self.path + u'?' + to_unicode(self.query_string, self.url_charset) # @property # def auth(self): # if not has_attribute(self, '_auth'): # self._authenticate() # return self._auth # def _authenticate(self): # for authentication_class in self.authentication_classes: # authenticator = authentication_class() # try: # auth = authenticator.authenticate(self) # except exceptions.APIException: # self._not_authenticated() # raise # if not auth is None: # self._authenticator = authenticator # self._auth = auth # return # self._not_authenticated() # def _not_authenticated(self): # self._authenticator = None # self._auth = None Flask-API-1.1+dfsg/flask_api/parsers.py0000644000175000017500000000443213255326671020126 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask._compat import text_type from flask_api import exceptions from werkzeug.formparser import MultiPartParser as WerkzeugMultiPartParser from werkzeug.formparser import default_stream_factory from werkzeug.urls import url_decode_stream import json class BaseParser(object): media_type = None handles_file_uploads = False # If set then 'request.files' will be populated. handles_form_data = False # If set then 'request.form' will be populated. def parse(self, stream, media_type, **options): msg = '`parse()` method must be implemented for class "%s"' raise NotImplementedError(msg % self.__class__.__name__) class JSONParser(BaseParser): media_type = 'application/json' def parse(self, stream, media_type, **options): data = stream.read().decode('utf-8') try: return json.loads(data) except ValueError as exc: msg = 'JSON parse error - %s' % text_type(exc) raise exceptions.ParseError(msg) class MultiPartParser(BaseParser): media_type = 'multipart/form-data' handles_file_uploads = True handles_form_data = True def parse(self, stream, media_type, **options): boundary = media_type.params.get('boundary') if boundary is None: msg = 'Multipart message missing boundary in Content-Type header' raise exceptions.ParseError(msg) boundary = boundary.encode('ascii') content_length = options.get('content_length') assert content_length is not None, 'MultiPartParser.parse() requires `content_length` argument' buffer_size = content_length while buffer_size % 4 or buffer_size < 1024: buffer_size += 1 multipart_parser = WerkzeugMultiPartParser(default_stream_factory, buffer_size=buffer_size) try: return multipart_parser.parse(stream, boundary, content_length) except ValueError as exc: msg = 'Multipart parse error - %s' % text_type(exc) raise exceptions.ParseError(msg) class URLEncodedParser(BaseParser): media_type = 'application/x-www-form-urlencoded' handles_form_data = True def parse(self, stream, media_type, **options): return url_decode_stream(stream) Flask-API-1.1+dfsg/flask_api/compat.py0000644000175000017500000000134313407025124017714 0ustar jonathanjonathan# -*- coding: utf-8 -*- from __future__ import unicode_literals, absolute_import from flask import __version__ as flask_version # Markdown is optional try: import markdown from markdown.extensions.toc import TocExtension def apply_markdown(text): """ Simple wrapper around :func:`markdown.markdown` to set the base level of '#' style headers to

. """ extensions = [TocExtension(baselevel=2)] md = markdown.Markdown(extensions=extensions) return md.convert(text) except ImportError: # pragma: no cover - markdown installed for tests apply_markdown = None def is_flask_legacy(): v = flask_version.split(".") return int(v[0]) == 0 and int(v[1]) < 11 Flask-API-1.1+dfsg/flask_api/settings.py0000644000175000017500000000335313165536712020307 0ustar jonathanjonathanfrom flask._compat import string_types import importlib def perform_imports(val, setting_name): """ If the given setting is a string import notation, then perform the necessary import or imports. """ if isinstance(val, string_types): return import_from_string(val, setting_name) elif isinstance(val, (list, tuple)): return [perform_imports(item, setting_name) for item in val] return val def import_from_string(val, setting_name): """ Attempt to import a class from a string representation. """ try: # Nod to tastypie's use of importlib. parts = val.split('.') module_path, class_name = '.'.join(parts[:-1]), parts[-1] module = importlib.import_module(module_path) return getattr(module, class_name) except ImportError as exc: format = "Could not import '%s' for API setting '%s'. %s." msg = format % (val, setting_name, exc) raise ImportError(msg) class APISettings(object): def __init__(self, user_config=None): self.user_config = user_config or {} @property def DEFAULT_PARSERS(self): default = [ 'flask_api.parsers.JSONParser', 'flask_api.parsers.URLEncodedParser', 'flask_api.parsers.MultiPartParser' ] val = self.user_config.get('DEFAULT_PARSERS', default) return perform_imports(val, 'DEFAULT_PARSERS') @property def DEFAULT_RENDERERS(self): default = [ 'flask_api.renderers.JSONRenderer', 'flask_api.renderers.BrowsableAPIRenderer' ] val = self.user_config.get('DEFAULT_RENDERERS', default) return perform_imports(val, 'DEFAULT_RENDERERS') default_settings = APISettings() Flask-API-1.1+dfsg/flask_api/negotiation.py0000644000175000017500000000375513165536712020775 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask import request from flask_api import exceptions from flask_api.mediatypes import MediaType, parse_accept_header class BaseNegotiation(object): def select_parser(self, parsers): msg = '`select_parser()` method must be implemented for class "%s"' raise NotImplementedError(msg % self.__class__.__name__) def select_renderer(self, renderers): msg = '`select_renderer()` method must be implemented for class "%s"' raise NotImplementedError(msg % self.__class__.__name__) class DefaultNegotiation(BaseNegotiation): def select_parser(self, parsers): """ Determine which parser to use for parsing the request body. Returns a two-tuple of (parser, content type). """ content_type_header = request.content_type client_media_type = MediaType(content_type_header) for parser in parsers: server_media_type = MediaType(parser.media_type) if server_media_type.satisfies(client_media_type): return (parser, client_media_type) raise exceptions.UnsupportedMediaType() def select_renderer(self, renderers): """ Determine which renderer to use for rendering the response body. Returns a two-tuple of (renderer, content type). """ accept_header = request.headers.get('Accept', '*/*') for client_media_types in parse_accept_header(accept_header): for renderer in renderers: server_media_type = MediaType(renderer.media_type) for client_media_type in client_media_types: if client_media_type.satisfies(server_media_type): if server_media_type.precedence > client_media_type.precedence: return (renderer, server_media_type) else: return (renderer, client_media_type) raise exceptions.NotAcceptable() Flask-API-1.1+dfsg/flask_api/templates/0000755000175000017500000000000013407030450020052 5ustar jonathanjonathanFlask-API-1.1+dfsg/flask_api/templates/base.html0000644000175000017500000002113213255326671021667 0ustar jonathanjonathan {% block head %} {% block meta %} {% endblock %} {% block title %}Flask API{% endblock %} {% block style %} {% block bootstrap_theme %} {% endblock %} {% endblock %} {% endblock %}
{% block navbar %} {% endblock %}
{% if 'GET' in allowed_methods %}
GET
{% endif %} {% if 'DELETE' in allowed_methods %}
{% endif %}
{% if view_description %}
{{ view_description|safe }}
{% endif %}
{{ request.method }} {{ request.url }}
HTTP {{ status }}{% autoescape off %} {% for key, val in headers.items() %}{{ key }}: {{ val|e }} {% endfor %}
{% if content %}{{ content|urlize_quoted_links }}{% endif %}
{% endautoescape %}
{% if 'POST' in allowed_methods or 'PUT' in allowed_methods or 'PATCH' in allowed_methods %}
{% if 'POST' in allowed_methods %} {% endif %} {% if 'PUT' in allowed_methods %} {% endif %} {% if 'PATCH' in allowed_methods %} {% endif %}
{% endif %}
{% block footer %} {% endblock %} {% block script %} {% endblock %} Flask-API-1.1+dfsg/flask_api/tests/0000755000175000017500000000000013407030450017216 5ustar jonathanjonathanFlask-API-1.1+dfsg/flask_api/tests/test_parsers.py0000644000175000017500000001642413165536712022332 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask import request from flask_api import exceptions, parsers, status, mediatypes, FlaskAPI from flask_api.decorators import set_parsers import io import json import unittest app = FlaskAPI(__name__) @app.route('/', methods=['POST']) def data(): return { 'data': request.data, 'form': request.form, 'files': dict([ (key, {'name': val.filename, 'contents': val.read().decode('utf8')}) for key, val in request.files.items() ]) } class ParserTests(unittest.TestCase): def test_valid_json(self): parser = parsers.JSONParser() stream = io.BytesIO(b'{"key": 1, "other": "two"}') data = parser.parse(stream, 'application/json') self.assertEqual(data, {"key": 1, "other": "two"}) def test_invalid_json(self): parser = parsers.JSONParser() stream = io.BytesIO(b'{key: 1, "other": "two"}') with self.assertRaises(exceptions.ParseError) as context: parser.parse(stream, mediatypes.MediaType('application/json')) detail = str(context.exception) expected_py2 = 'JSON parse error - Expecting property name: line 1 column 1 (char 1)' expected_py3 = 'JSON parse error - Expecting property name enclosed in double quotes: line 1 column 2 (char 1)' self.assertIn(detail, (expected_py2, expected_py3)) def test_invalid_multipart(self): parser = parsers.MultiPartParser() stream = io.BytesIO(b'invalid') media_type = mediatypes.MediaType('multipart/form-data; boundary="foo"') with self.assertRaises(exceptions.ParseError) as context: parser.parse(stream, media_type, content_length=len('invalid')) detail = str(context.exception) expected = 'Multipart parse error - Expected boundary at start of multipart data' self.assertEqual(detail, expected) def test_invalid_multipart_no_boundary(self): parser = parsers.MultiPartParser() stream = io.BytesIO(b'invalid') with self.assertRaises(exceptions.ParseError) as context: parser.parse(stream, mediatypes.MediaType('multipart/form-data')) detail = str(context.exception) expected = 'Multipart message missing boundary in Content-Type header' self.assertEqual(detail, expected) def test_renderer_negotiation_not_implemented(self): parser = parsers.BaseParser() with self.assertRaises(NotImplementedError) as context: parser.parse(None, None) msg = str(context.exception) expected = '`parse()` method must be implemented for class "BaseParser"' self.assertEqual(msg, expected) def test_accessing_json(self): with app.test_client() as client: data = json.dumps({'example': 'example'}) response = client.post('/', data=data, content_type='application/json') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Type'], 'application/json') data = json.loads(response.get_data().decode('utf8')) expected = { "data": {"example": "example"}, "form": {}, "files": {} } self.assertEqual(data, expected) def test_accessing_url_encoded(self): with app.test_client() as client: data = {'example': 'example'} response = client.post('/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Type'], 'application/json') data = json.loads(response.get_data().decode('utf8')) expected = { "data": {"example": "example"}, "form": {"example": "example"}, "files": {} } self.assertEqual(data, expected) def test_accessing_multipart(self): with app.test_client() as client: data = {'example': 'example', 'upload': (io.BytesIO(b'file contents'), 'name.txt')} response = client.post('/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Type'], 'application/json') data = json.loads(response.get_data().decode('utf8')) expected = { "data": {"example": "example"}, "form": {"example": "example"}, "files": {"upload": {"name": "name.txt", "contents": "file contents"}} } self.assertEqual(data, expected) class OverrideParserSettings(unittest.TestCase): def setUp(self): class CustomParser1(parsers.BaseParser): media_type = '*/*' def parse(self, stream, media_type, content_length=None): return 'custom parser 1' class CustomParser2(parsers.BaseParser): media_type = '*/*' def parse(self, stream, media_type, content_length=None): return 'custom parser 2' app = FlaskAPI(__name__) app.config['DEFAULT_PARSERS'] = [CustomParser1] @app.route('/custom_parser_1/', methods=['POST']) def custom_parser_1(): return {'data': request.data} @app.route('/custom_parser_2/', methods=['POST']) @set_parsers([CustomParser2]) def custom_parser_2(): return {'data': request.data} @app.route('/custom_parser_2_as_args/', methods=['POST']) @set_parsers(CustomParser2, CustomParser1) def custom_parser_2_as_args(): return {'data': request.data} self.app = app def test_overridden_parsers_with_settings(self): with self.app.test_client() as client: data = {'example': 'example'} response = client.post('/custom_parser_1/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Type'], 'application/json') data = json.loads(response.get_data().decode('utf8')) expected = { "data": "custom parser 1", } self.assertEqual(data, expected) def test_overridden_parsers_with_decorator(self): with self.app.test_client() as client: data = {'example': 'example'} response = client.post('/custom_parser_2/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Type'], 'application/json') data = json.loads(response.get_data().decode('utf8')) expected = { "data": "custom parser 2", } self.assertEqual(data, expected) def test_overridden_parsers_with_decorator_as_args(self): with self.app.test_client() as client: data = {'example': 'example'} response = client.post('/custom_parser_2_as_args/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Type'], 'application/json') data = json.loads(response.get_data().decode('utf8')) expected = { "data": "custom parser 2", } self.assertEqual(data, expected) Flask-API-1.1+dfsg/flask_api/tests/test_negotiation.py0000644000175000017500000000712013165536712023164 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals import unittest import flask_api from flask_api import exceptions from flask_api.negotiation import BaseNegotiation, DefaultNegotiation app = flask_api.FlaskAPI(__name__) class JSON(object): media_type = 'application/json' class HTML(object): media_type = 'application/html' class URLEncodedForm(object): media_type = 'application/x-www-form-urlencoded' class TestRendererNegotiation(unittest.TestCase): def test_select_renderer_client_preference(self): negotiation = DefaultNegotiation() renderers = [JSON, HTML] headers = {'Accept': 'application/html'} with app.test_request_context(headers=headers): renderer, media_type = negotiation.select_renderer(renderers) self.assertEqual(renderer, HTML) self.assertEqual(str(media_type), 'application/html') def test_select_renderer_no_accept_header(self): negotiation = DefaultNegotiation() renderers = [JSON, HTML] with app.test_request_context(): renderer, media_type = negotiation.select_renderer(renderers) self.assertEqual(renderer, JSON) self.assertEqual(str(media_type), 'application/json') def test_select_renderer_server_preference(self): negotiation = DefaultNegotiation() renderers = [JSON, HTML] headers = {'Accept': '*/*'} with app.test_request_context(headers=headers): renderer, media_type = negotiation.select_renderer(renderers) self.assertEqual(renderer, JSON) self.assertEqual(str(media_type), 'application/json') def test_select_renderer_failed(self): negotiation = DefaultNegotiation() renderers = [JSON, HTML] headers = {'Accept': 'application/xml'} with app.test_request_context(headers=headers): with self.assertRaises(exceptions.NotAcceptable): renderer, media_type = negotiation.select_renderer(renderers) def test_renderer_negotiation_not_implemented(self): negotiation = BaseNegotiation() with self.assertRaises(NotImplementedError) as context: negotiation.select_renderer([]) msg = str(context.exception) expected = '`select_renderer()` method must be implemented for class "BaseNegotiation"' self.assertEqual(msg, expected) class TestParserNegotiation(unittest.TestCase): def test_select_parser(self): negotiation = DefaultNegotiation() parsers = [JSON, URLEncodedForm] headers = {'Content-Type': 'application/x-www-form-urlencoded'} with app.test_request_context(headers=headers): renderer, media_type = negotiation.select_parser(parsers) self.assertEqual(renderer, URLEncodedForm) self.assertEqual(str(media_type), 'application/x-www-form-urlencoded') def test_select_parser_failed(self): negotiation = DefaultNegotiation() parsers = [JSON, URLEncodedForm] headers = {'Content-Type': 'application/xml'} with app.test_request_context(headers=headers): with self.assertRaises(exceptions.UnsupportedMediaType): renderer, media_type = negotiation.select_parser(parsers) def test_parser_negotiation_not_implemented(self): negotiation = BaseNegotiation() with self.assertRaises(NotImplementedError) as context: negotiation.select_parser([]) msg = str(context.exception) expected = '`select_parser()` method must be implemented for class "BaseNegotiation"' self.assertEqual(msg, expected) Flask-API-1.1+dfsg/flask_api/tests/__init__.py0000644000175000017500000000066113165536712021347 0ustar jonathanjonathan# This is a fudge that allows us to easily specify test modules. # For example: # ./runtests test_parsers # ./runtests test_rendereres.RendererTests.test_render_json import os modules = [filename.rsplit('.', 1)[0] for filename in os.listdir(os.path.dirname(__file__)) if filename.endswith('.py') and filename.startswith('test_')] for module in modules: exec("from flask_api.tests.%s import *" % module) Flask-API-1.1+dfsg/flask_api/tests/test_settings.py0000644000175000017500000000141413165536712022504 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask_api.settings import APISettings import unittest class SettingsTests(unittest.TestCase): def test_bad_import(self): settings = APISettings({'DEFAULT_PARSERS': 'foobarz.FailedImport'}) with self.assertRaises(ImportError) as context: settings.DEFAULT_PARSERS msg = str(context.exception) excepted_py2 = ( "Could not import 'foobarz.FailedImport' for API setting " "'DEFAULT_PARSERS'. No module named foobarz." ) excepted_py3 = ( "Could not import 'foobarz.FailedImport' for API setting " "'DEFAULT_PARSERS'. No module named 'foobarz'." ) self.assertIn(msg, (excepted_py2, excepted_py3)) Flask-API-1.1+dfsg/flask_api/tests/test_request.py0000644000175000017500000000317413165536712022341 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask import request from flask_api import exceptions import flask_api import io import unittest app = flask_api.FlaskAPI(__name__) class MediaTypeParsingTests(unittest.TestCase): def test_json_request(self): kwargs = { 'method': 'PUT', 'input_stream': io.BytesIO(b'{"key": 1, "other": "two"}'), 'content_type': 'application/json' } with app.test_request_context(**kwargs): self.assertEqual(request.data, {"key": 1, "other": "two"}) def test_invalid_content_type_request(self): kwargs = { 'method': 'PUT', 'input_stream': io.BytesIO(b'Cannot parse this content type.'), 'content_type': 'text/plain' } with app.test_request_context(**kwargs): with self.assertRaises(exceptions.UnsupportedMediaType): request.data def test_no_content_request(self): """ Ensure that requests with no data do not populate the `.data`, `.form` or `.files` attributes. """ with app.test_request_context(method='PUT'): self.assertFalse(request.data) with app.test_request_context(method='PUT'): self.assertFalse(request.form) with app.test_request_context(method='PUT'): self.assertFalse(request.files) def test_encode_request(self): """ Ensure that `.full_path` is correctly decoded in python 3 """ with app.test_request_context(method='GET', path='/?a=b'): self.assertEqual(request.full_path, '/?a=b') Flask-API-1.1+dfsg/flask_api/tests/test_mediatypes.py0000644000175000017500000001163313165536712023014 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask_api.mediatypes import MediaType, parse_accept_header import unittest class MediaTypeParsingTests(unittest.TestCase): def test_media_type_with_params(self): media = MediaType('application/xml; schema=foobar, q=0.5') self.assertEqual(str(media), 'application/xml; q="0.5", schema="foobar"') self.assertEqual(media.main_type, 'application') self.assertEqual(media.sub_type, 'xml') self.assertEqual(media.full_type, 'application/xml') self.assertEqual(media.params, {'schema': 'foobar', 'q': '0.5'}) self.assertEqual(media.precedence, 3) self.assertEqual(repr(media), '') def test_media_type_with_q_params(self): media = MediaType('application/xml; q=0.5') self.assertEqual(str(media), 'application/xml; q="0.5"') self.assertEqual(media.main_type, 'application') self.assertEqual(media.sub_type, 'xml') self.assertEqual(media.full_type, 'application/xml') self.assertEqual(media.params, {'q': '0.5'}) self.assertEqual(media.precedence, 2) def test_media_type_without_params(self): media = MediaType('application/xml') self.assertEqual(str(media), 'application/xml') self.assertEqual(media.main_type, 'application') self.assertEqual(media.sub_type, 'xml') self.assertEqual(media.full_type, 'application/xml') self.assertEqual(media.params, {}) self.assertEqual(media.precedence, 2) def test_media_type_with_wildcard_sub_type(self): media = MediaType('application/*') self.assertEqual(str(media), 'application/*') self.assertEqual(media.main_type, 'application') self.assertEqual(media.sub_type, '*') self.assertEqual(media.full_type, 'application/*') self.assertEqual(media.params, {}) self.assertEqual(media.precedence, 1) def test_media_type_with_wildcard_main_type(self): media = MediaType('*/*') self.assertEqual(str(media), '*/*') self.assertEqual(media.main_type, '*') self.assertEqual(media.sub_type, '*') self.assertEqual(media.full_type, '*/*') self.assertEqual(media.params, {}) self.assertEqual(media.precedence, 0) class MediaTypeMatchingTests(unittest.TestCase): def test_media_type_includes_params(self): media_type = MediaType('application/json') other = MediaType('application/json; version=1.0') self.assertTrue(media_type.satisfies(other)) def test_media_type_missing_params(self): media_type = MediaType('application/json; version=1.0') other = MediaType('application/json') self.assertFalse(media_type.satisfies(other)) def test_media_type_matching_params(self): media_type = MediaType('application/json; version=1.0') other = MediaType('application/json; version=1.0') self.assertTrue(media_type.satisfies(other)) def test_media_type_non_matching_params(self): media_type = MediaType('application/json; version=1.0') other = MediaType('application/json; version=2.0') self.assertFalse(media_type.satisfies(other)) def test_media_type_main_type_match(self): media_type = MediaType('image/*') other = MediaType('image/png') self.assertTrue(media_type.satisfies(other)) def test_media_type_sub_type_mismatch(self): media_type = MediaType('image/jpeg') other = MediaType('image/png') self.assertFalse(media_type.satisfies(other)) def test_media_type_wildcard_match(self): media_type = MediaType('*/*') other = MediaType('image/png') self.assertTrue(media_type.satisfies(other)) def test_media_type_wildcard_mismatch(self): media_type = MediaType('image/*') other = MediaType('audio/*') self.assertFalse(media_type.satisfies(other)) class AcceptHeaderTests(unittest.TestCase): def test_parse_simple_accept_header(self): parsed = parse_accept_header('*/*, application/json') self.assertEqual(parsed, [ set([MediaType('application/json')]), set([MediaType('*/*')]) ]) def test_parse_complex_accept_header(self): """ The accept header should be parsed into a list of sets of MediaType. The list is an ordering of precedence. Note that we disregard 'q' values when determining precedence, and instead differentiate equal values by using the server preference. """ header = 'application/xml; schema=foo, application/json; q=0.9, application/xml, */*' parsed = parse_accept_header(header) self.assertEqual(parsed, [ set([MediaType('application/xml; schema=foo')]), set([MediaType('application/json; q=0.9'), MediaType('application/xml')]), set([MediaType('*/*')]), ]) Flask-API-1.1+dfsg/flask_api/tests/test_status.py0000644000175000017500000000230213165536712022164 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask_api import status import unittest class TestStatus(unittest.TestCase): def test_status_categories(self): self.assertFalse(status.is_informational(99)) self.assertTrue(status.is_informational(100)) self.assertTrue(status.is_informational(199)) self.assertFalse(status.is_informational(200)) self.assertFalse(status.is_success(199)) self.assertTrue(status.is_success(200)) self.assertTrue(status.is_success(299)) self.assertFalse(status.is_success(300)) self.assertFalse(status.is_redirect(299)) self.assertTrue(status.is_redirect(300)) self.assertTrue(status.is_redirect(399)) self.assertFalse(status.is_redirect(400)) self.assertFalse(status.is_client_error(399)) self.assertTrue(status.is_client_error(400)) self.assertTrue(status.is_client_error(499)) self.assertFalse(status.is_client_error(500)) self.assertFalse(status.is_server_error(499)) self.assertTrue(status.is_server_error(500)) self.assertTrue(status.is_server_error(599)) self.assertFalse(status.is_server_error(600)) Flask-API-1.1+dfsg/flask_api/tests/test_renderers.py0000644000175000017500000001457413165536712022650 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from datetime import datetime from flask.json import JSONEncoder from flask_api import renderers, status, FlaskAPI from flask_api.decorators import set_renderers from flask_api.mediatypes import MediaType import unittest class RendererTests(unittest.TestCase): def _make_app(self): app = FlaskAPI(__name__) @app.route('/_love', methods=['GET']) def love(): return {"test": "I <3 Python"} return app def test_render_json(self): app = self._make_app() renderer = renderers.JSONRenderer() with app.app_context(): content = renderer.render({'example': 'example'}, MediaType('application/json')) expected = '{"example": "example"}' self.assertEqual(content, expected) def test_render_json_with_indent(self): app = self._make_app() renderer = renderers.JSONRenderer() with app.app_context(): content = renderer.render({'example': 'example'}, MediaType('application/json; indent=4')) expected = '{\n "example": "example"\n}' self.assertEqual(content, expected) def test_render_json_with_custom_encoder(self): class CustomJsonEncoder(JSONEncoder): def default(self, o): if isinstance(o, datetime): return o.isoformat() return super(CustomJsonEncoder, self).default(o) app = self._make_app() app.json_encoder = CustomJsonEncoder renderer = renderers.JSONRenderer() date = datetime(2017, 10, 5, 15, 22) with app.app_context(): content = renderer.render(date, MediaType('application/json')) self.assertEqual(content, '"{}"'.format(date.isoformat())) def test_render_browsable_encoding(self): app = FlaskAPI(__name__) @app.route('/_love', methods=['GET']) def love(): return {"test": "I <3 Python"} with app.test_client() as client: response = client.get('/_love', headers={"Accept": "text/html"}) html = str(response.get_data()) self.assertTrue('I <3 Python' in html) self.assertTrue('

Love

' in html) self.assertTrue('/_love' in html) def test_render_browsable_encoding_with_markdown(self): app = FlaskAPI(__name__) @app.route('/_foo', methods=['GET']) def foo(): """Bar: - `qux` """ return {"test": "I <3 Python"} with app.test_client() as client: response = client.get('/_foo', headers={"Accept": "text/html"}) html = str(response.get_data()) print(html) self.assertTrue('

Foo

' in html) self.assertTrue('

Bar:' in html) self.assertTrue('qux' in html) def test_render_browsable_linking(self): app = FlaskAPI(__name__) @app.route('/_happiness', methods=['GET']) def happiness(): return {"url": "http://example.org", "a tag": "
"} with app.test_client() as client: response = client.get('/_happiness', headers={"Accept": "text/html"}) html = str(response.get_data()) self.assertTrue('http://example.org' in html) self.assertTrue('<br />'in html) self.assertTrue('

Happiness

' in html) self.assertTrue('/_happiness' in html) def test_renderer_negotiation_not_implemented(self): renderer = renderers.BaseRenderer() with self.assertRaises(NotImplementedError) as context: renderer.render(None, None) msg = str(context.exception) expected = '`render()` method must be implemented for class "BaseRenderer"' self.assertEqual(msg, expected) class OverrideParserSettings(unittest.TestCase): def setUp(self): class CustomRenderer1(renderers.BaseRenderer): media_type = 'application/example1' def render(self, data, media_type, **options): return 'custom renderer 1' class CustomRenderer2(renderers.BaseRenderer): media_type = 'application/example2' def render(self, data, media_type, **options): return 'custom renderer 2' app = FlaskAPI(__name__) app.config['DEFAULT_RENDERERS'] = [CustomRenderer1] app.config['PROPAGATE_EXCEPTIONS'] = True @app.route('/custom_renderer_1/', methods=['GET']) def custom_renderer_1(): return {'data': 'example'} @app.route('/custom_renderer_2/', methods=['GET']) @set_renderers([CustomRenderer2]) def custom_renderer_2(): return {'data': 'example'} @app.route('/custom_renderer_2_as_args/', methods=['GET']) @set_renderers(CustomRenderer2) def custom_renderer_2_as_args(): return {'data': 'example'} self.app = app def test_overridden_parsers_with_settings(self): with self.app.test_client() as client: response = client.get('/custom_renderer_1/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Type'], 'application/example1') data = response.get_data().decode('utf8') self.assertEqual(data, "custom renderer 1") def test_overridden_parsers_with_decorator(self): with self.app.test_client() as client: data = {'example': 'example'} response = client.get('/custom_renderer_2/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Type'], 'application/example2') data = response.get_data().decode('utf8') self.assertEqual(data, "custom renderer 2") def test_overridden_parsers_with_decorator_as_args(self): with self.app.test_client() as client: data = {'example': 'example'} response = client.get('/custom_renderer_2_as_args/', data=data) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Content-Type'], 'application/example2') data = response.get_data().decode('utf8') self.assertEqual(data, "custom renderer 2") Flask-API-1.1+dfsg/flask_api/tests/test_app.py0000644000175000017500000001537613255326671021441 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask import abort, make_response, request, jsonify from flask_api.decorators import set_renderers from flask_api import exceptions, renderers, status, FlaskAPI import json import unittest app = FlaskAPI(__name__) app.config['TESTING'] = True class JSONVersion1(renderers.JSONRenderer): media_type = 'application/json; api-version="1.0"' class JSONVersion2(renderers.JSONRenderer): media_type = 'application/json; api-version="2.0"' # This is being used to test issue #58, source is taken from flask apierrors doc page class InvalidUsage(Exception): status_code = 400 def __init__(self, message, status_code=None, payload=None): Exception.__init__(self) self.message = message if status_code is not None: self.status_code = status_code self.payload = payload def to_dict(self): rv = dict(self.payload or ()) rv['message'] = self.message return rv @app.errorhandler(InvalidUsage) def handle_invalid_usage(error): response = jsonify(error.to_dict()) response.status_code = error.status_code return response @app.route('/set_status_and_headers/') def set_status_and_headers(): headers = {'Location': 'http://example.com/456'} return {'example': 'content'}, status.HTTP_201_CREATED, headers @app.route('/set_headers/') def set_headers(): headers = {'Location': 'http://example.com/456'} return {'example': 'content'}, headers @app.route('/make_response_view/') def make_response_view(): response = make_response({'example': 'content'}) response.headers['Location'] = 'http://example.com/456' return response @app.route('/none_204_response/') def none_204_response(): return None, status.HTTP_204_NO_CONTENT @app.route('/none_200_response/') def none_200_response(): return None, status.HTTP_200_OK @app.route('/api_exception/') def api_exception(): raise exceptions.PermissionDenied() @app.route('/custom_exception/') def custom_exception(): raise InvalidUsage('Invalid usage test.', status_code=410) @app.route('/custom_exception_no_code/') def custom_exception_no_status_code(): raise InvalidUsage('Invalid usage test.') @app.route('/abort_view/') def abort_view(): abort(status.HTTP_403_FORBIDDEN) @app.route('/options/') def options_view(): return {} @app.route('/accepted_media_type/') @set_renderers([JSONVersion2, JSONVersion1]) def accepted_media_type(): return {'accepted_media_type': str(request.accepted_media_type)} class AppTests(unittest.TestCase): def test_set_status_and_headers(self): with app.test_client() as client: response = client.get('/set_status_and_headers/') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.headers['Location'], 'http://example.com/456') self.assertEqual(response.content_type, 'application/json') expected = '{"example": "content"}' self.assertEqual(response.get_data().decode('utf8'), expected) def test_set_headers(self): with app.test_client() as client: response = client.get('/set_headers/') self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.headers['Location'], 'http://example.com/456') self.assertEqual(response.content_type, 'application/json') expected = '{"example": "content"}' self.assertEqual(response.get_data().decode('utf8'), expected) def test_make_response(self): with app.test_client() as client: response = client.get('/make_response_view/') self.assertEqual(response.content_type, 'application/json') self.assertEqual(response.headers['Location'], 'http://example.com/456') self.assertEqual(response.content_type, 'application/json') expected = '{"example": "content"}' self.assertEqual(response.get_data().decode('utf8'), expected) def test_none_204_response(self): with app.test_client() as client: response = client.get('/none_204_response/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) expected = '' self.assertEqual(response.get_data().decode('utf8'), expected) def test_none_200_response(self): with app.test_client() as client: with self.assertRaises(ValueError): client.get('/none_200_response/') def test_api_exception(self): with app.test_client() as client: response = client.get('/api_exception/') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.content_type, 'application/json') expected = '{"message": "You do not have permission to perform this action."}' self.assertEqual(response.get_data().decode('utf8'), expected) def test_custom_exception(self): with app.test_client() as client: response = client.get('/custom_exception/') self.assertEqual(response.status_code, status.HTTP_410_GONE) self.assertEqual(response.content_type, 'application/json') def test_custom_exception_default_code(self): with app.test_client() as client: response = client.get('/custom_exception_no_code/') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.content_type, 'application/json') def test_abort_view(self): with app.test_client() as client: response = client.get('/abort_view/') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_options_view(self): with app.test_client() as client: response = client.options('/options/') # Errors if `response.response` is `None` response.get_data() def test_accepted_media_type_property(self): with app.test_client() as client: # Explicitly request the "api-version 1.0" renderer. headers = {'Accept': 'application/json; api-version="1.0"'} response = client.get('/accepted_media_type/', headers=headers) data = json.loads(response.get_data().decode('utf8')) expected = {'accepted_media_type': 'application/json; api-version="1.0"'} self.assertEqual(data, expected) # Request the default renderer, which is "api-version 2.0". headers = {'Accept': '*/*'} response = client.get('/accepted_media_type/', headers=headers) data = json.loads(response.get_data().decode('utf8')) expected = {'accepted_media_type': 'application/json; api-version="2.0"'} self.assertEqual(data, expected) Flask-API-1.1+dfsg/flask_api/tests/test_exceptions.py0000644000175000017500000000147613165536712023035 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask_api import exceptions from flask_api import status import unittest class Conflict(exceptions.APIException): status_code = status.HTTP_409_CONFLICT detail = 'Could not update the resource' class TestExceptions(unittest.TestCase): def test_custom_exception(self): try: raise Conflict() except Conflict as exc: self.assertEqual(str(exc), 'Could not update the resource') self.assertEqual(exc.status_code, 409) def test_override_exception_detail(self): try: raise Conflict('A widget with this id already exists') except Conflict as exc: self.assertEqual(str(exc), 'A widget with this id already exists') self.assertEqual(exc.status_code, 409) Flask-API-1.1+dfsg/flask_api/response.py0000644000175000017500000000245713165536712020311 0ustar jonathanjonathan# coding: utf8 from __future__ import unicode_literals from flask import request, Response from flask._compat import text_type class APIResponse(Response): api_return_types = (list, dict) def __init__(self, content=None, *args, **kwargs): super(APIResponse, self).__init__(None, *args, **kwargs) media_type = None if isinstance(content, self.api_return_types) or content == '': renderer = request.accepted_renderer if content != '' or renderer.handles_empty_responses: media_type = request.accepted_media_type options = self.get_renderer_options() content = renderer.render(content, media_type, **options) if self.status_code == 204: self.status_code = 200 # From `werkzeug.wrappers.BaseResponse` if content is None: content = [] if isinstance(content, (text_type, bytes, bytearray)): self.set_data(content) else: self.response = content if media_type is not None: self.headers['Content-Type'] = str(media_type) def get_renderer_options(self): return { 'status': self.status, 'status_code': self.status_code, 'headers': self.headers } Flask-API-1.1+dfsg/PKG-INFO0000644000175000017500000000166313407030450015226 0ustar jonathanjonathanMetadata-Version: 1.1 Name: Flask-API Version: 1.1 Summary: Browsable web APIs for Flask. Home-page: http://www.flaskapi.org Author: Tom Christie Author-email: tom@tomchristie.com License: BSD Description: Browsable web APIs for Flask. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Framework :: Flask Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Internet :: WWW/HTTP