def test_blueprint_pagination_response_tuple(self, app): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') client = app.test_client() @blp.route('/response') @blp.response() @blp.paginate(Page) def func_response(): return [1, 2] @blp.route('/response_code') @blp.response() @blp.paginate(Page) def func_response_code(): return [1, 2], 201 @blp.route('/response_headers') @blp.response() @blp.paginate(Page) def func_response_headers(): return [1, 2], {'X-header': 'test'} @blp.route('/response_code_headers') @blp.response() @blp.paginate(Page) def func_response_code_headers(): return [1, 2], 201, {'X-header': 'test'} @blp.route('/response_wrong_tuple') @blp.response() @blp.paginate(Page) def func_response_wrong_tuple(): return [1, 2], 201, {'X-header': 'test'}, 'extra' @blp.route('/response_tuple_subclass') @blp.response() @blp.paginate(Page) def func_response_tuple_subclass(): class MyTuple(tuple): pass return MyTuple((1, 2)) api.register_blueprint(blp) response = client.get('/test/response') assert response.status_code == 200 assert response.json == [1, 2] response = client.get('/test/response_code') assert response.status_code == 201 assert response.json == [1, 2] response = client.get('/test/response_headers') assert response.status_code == 200 assert response.json == [1, 2] assert response.headers['X-header'] == 'test' response = client.get('/test/response_code_headers') assert response.status_code == 201 assert response.json == [1, 2] assert response.headers['X-header'] == 'test' response = client.get('/test/response_wrong_tuple') assert response.status_code == 500 response = client.get('/test/response_tuple_subclass') assert response.status_code == 200 assert response.json == [1, 2]
def test_examples(self, app, blueprint_fixture, schemas): blueprint, bp_schema = blueprint_fixture api = Api(app) api.register_blueprint(blueprint) client = app.test_client() @contextmanager def assert_counters( schema_load, schema_dump, etag_schema_load, etag_schema_dump ): """ Check number of calls to dump/load methods of schemas. """ schemas.DocSchema.reset_load_count() schemas.DocSchema.reset_dump_count() schemas.DocEtagSchema.reset_load_count() schemas.DocEtagSchema.reset_dump_count() yield assert schemas.DocSchema.load_count == schema_load assert schemas.DocSchema.dump_count == schema_dump assert schemas.DocEtagSchema.load_count == etag_schema_load assert schemas.DocEtagSchema.dump_count == etag_schema_dump # GET collection without ETag: OK with assert_counters(0, 1, 0, 1 if bp_schema == 'ETag schema' else 0): response = client.get('/test/') assert response.status_code == 200 list_etag = response.headers['ETag'] assert len(response.json) == 0 assert json.loads(response.headers['X-Pagination']) == { 'total': 0, 'total_pages': 0, } # GET collection with correct ETag: Not modified with assert_counters(0, 1, 0, 1 if bp_schema == 'ETag schema' else 0): response = client.get('/test/', headers={'If-None-Match': list_etag}) assert response.status_code == 304 # POST item_1 item_1_data = {'field': 0} with assert_counters(1, 1, 0, 1 if bp_schema == 'ETag schema' else 0): response = client.post( '/test/', data=json.dumps(item_1_data), content_type='application/json' ) assert response.status_code == 200 item_1_id = response.json['item_id'] # GET collection with wrong/outdated ETag: OK with assert_counters(0, 1, 0, 1 if bp_schema == 'ETag schema' else 0): response = client.get('/test/', headers={'If-None-Match': list_etag}) assert response.status_code == 200 list_etag = response.headers['ETag'] assert len(response.json) == 1 assert response.json[0] == {'field': 0, 'item_id': 1} assert json.loads(response.headers['X-Pagination']) == { 'total': 1, 'total_pages': 1, 'page': 1, 'first_page': 1, 'last_page': 1, } # GET by ID without ETag: OK with assert_counters(0, 1, 0, 1 if bp_schema == 'ETag schema' else 0): response = client.get('/test/{}'.format(item_1_id)) assert response.status_code == 200 item_etag = response.headers['ETag'] # GET by ID with correct ETag: Not modified with assert_counters( 0, 0 if bp_schema == 'No schema' else 1, 0, 1 if bp_schema == 'ETag schema' else 0, ): response = client.get( '/test/{}'.format(item_1_id), headers={'If-None-Match': item_etag} ) assert response.status_code == 304 # PUT without ETag: Precondition required error item_1_data['field'] = 1 with assert_counters(0, 0, 0, 0): response = client.put( '/test/{}'.format(item_1_id), data=json.dumps(item_1_data), content_type='application/json', ) assert response.status_code == 428 # PUT with correct ETag: OK with assert_counters( 1, 2 if bp_schema == 'Schema' else 1, 0, 2 if bp_schema == 'ETag schema' else 0, ): response = client.put( '/test/{}'.format(item_1_id), data=json.dumps(item_1_data), content_type='application/json', headers={'If-Match': item_etag}, ) assert response.status_code == 200 new_item_etag = response.headers['ETag'] # PUT with wrong/outdated ETag: Precondition failed error item_1_data['field'] = 2 with assert_counters( 1, 1 if bp_schema == 'Schema' else 0, 0, 1 if bp_schema == 'ETag schema' else 0, ): response = client.put( '/test/{}'.format(item_1_id), data=json.dumps(item_1_data), content_type='application/json', headers={'If-Match': item_etag}, ) assert response.status_code == 412 # GET by ID with wrong/outdated ETag: OK with assert_counters(0, 1, 0, 1 if bp_schema == 'ETag schema' else 0): response = client.get( '/test/{}'.format(item_1_id), headers={'If-None-Match': item_etag} ) assert response.status_code == 200 # GET collection with pagination set to 1 element per page with assert_counters(0, 1, 0, 1 if bp_schema == 'ETag schema' else 0): response = client.get( '/test/', headers={'If-None-Match': list_etag}, query_string={'page': 1, 'page_size': 1}, ) assert response.status_code == 200 list_etag = response.headers['ETag'] assert len(response.json) == 1 assert response.json[0] == {'field': 1, 'item_id': 1} assert json.loads(response.headers['X-Pagination']) == { 'total': 1, 'total_pages': 1, 'page': 1, 'first_page': 1, 'last_page': 1, } # POST item_2 item_2_data = {'field': 1} with assert_counters(1, 1, 0, 1 if bp_schema == 'ETag schema' else 0): response = client.post( '/test/', data=json.dumps(item_2_data), content_type='application/json' ) assert response.status_code == 200 # GET collection with pagination set to 1 element per page # Content is the same (item_1) but pagination metadata has changed # so we don't get a 304 and the data is returned again with assert_counters(0, 1, 0, 1 if bp_schema == 'ETag schema' else 0): response = client.get( '/test/', headers={'If-None-Match': list_etag}, query_string={'page': 1, 'page_size': 1}, ) assert response.status_code == 200 list_etag = response.headers['ETag'] assert len(response.json) == 1 assert response.json[0] == {'field': 1, 'item_id': 1} assert json.loads(response.headers['X-Pagination']) == { 'total': 2, 'total_pages': 2, 'page': 1, 'first_page': 1, 'last_page': 2, 'next_page': 2, } # DELETE without ETag: Precondition required error with assert_counters(0, 0, 0, 0): response = client.delete('/test/{}'.format(item_1_id)) assert response.status_code == 428 # DELETE with wrong/outdated ETag: Precondition failed error with assert_counters( 0, 1 if bp_schema == 'Schema' else 0, 0, 1 if bp_schema == 'ETag schema' else 0, ): response = client.delete( '/test/{}'.format(item_1_id), headers={'If-Match': item_etag} ) assert response.status_code == 412 # DELETE with correct ETag: No Content with assert_counters( 0, 1 if bp_schema == 'Schema' else 0, 0, 1 if bp_schema == 'ETag schema' else 0, ): response = client.delete( '/test/{}'.format(item_1_id), headers={'If-Match': new_item_etag} ) assert response.status_code == 204
def register_blueprints(app, *blps): api = Api(app=app) for blp in blps: api.register_blueprint(blp) return api
def test_blueprint_response_tuple(self, app): api = Api(app) blp = Blueprint('test', __name__, url_prefix='/test') client = app.test_client() @blp.route('/response') @blp.response() def func_response(): return {} @blp.route('/response_code_int') @blp.response() def func_response_code_int(): return {}, 201 @blp.route('/response_code_str') @blp.response() def func_response_code_str(): return {}, '201 CREATED' @blp.route('/response_headers') @blp.response() def func_response_headers(): return {}, {'X-header': 'test'} @blp.route('/response_code_int_headers') @blp.response() def func_response_code_int_headers(): return {}, 201, {'X-header': 'test'} @blp.route('/response_code_str_headers') @blp.response() def func_response_code_str_headers(): return {}, '201 CREATED', {'X-header': 'test'} @blp.route('/response_wrong_tuple') @blp.response() def func_response_wrong_tuple(): return {}, 201, {'X-header': 'test'}, 'extra' @blp.route('/response_tuple_subclass') @blp.response() def func_response_tuple_subclass(): class MyTuple(tuple): pass return MyTuple((1, 2)) api.register_blueprint(blp) response = client.get('/test/response') assert response.status_code == 200 assert response.json == {} response = client.get('/test/response_code_int') assert response.status_code == 201 assert response.status == '201 CREATED' assert response.json == {} response = client.get('/test/response_code_str') assert response.status_code == 201 assert response.status == '201 CREATED' assert response.json == {} response = client.get('/test/response_headers') assert response.status_code == 200 assert response.json == {} assert response.headers['X-header'] == 'test' response = client.get('/test/response_code_int_headers') assert response.status_code == 201 assert response.status == '201 CREATED' assert response.json == {} assert response.headers['X-header'] == 'test' response = client.get('/test/response_code_str_headers') assert response.status_code == 201 assert response.status == '201 CREATED' assert response.json == {} assert response.headers['X-header'] == 'test' response = client.get('/test/response_wrong_tuple') assert response.status_code == 500 response = client.get('/test/response_tuple_subclass') assert response.status_code == 200 assert response.json == [1, 2]
from flask_rest_api import Api api = Api()
def app_with_etag(request, collection, schemas, app): """Return a basic API sample with ETag""" as_method_view = request.param DocSchema = schemas.DocSchema DocEtagSchema = schemas.DocEtagSchema blp = Blueprint('test', __name__, url_prefix='/test') if as_method_view: @blp.route('/') class Resource(MethodView): @blp.etag(DocEtagSchema(many=True)) @blp.response( DocSchema(many=True)) def get(self): return collection.items @blp.etag(DocEtagSchema) @blp.arguments(DocSchema) @blp.response(DocSchema, code=201) def post(self, new_item): return collection.post(new_item) @blp.route('/<int:item_id>') class ResourceById(MethodView): def _get_item(self, item_id): try: return collection.get_by_id(item_id) except ItemNotFound: abort(404) @blp.etag(DocEtagSchema) @blp.response(DocSchema) def get(self, item_id): return self._get_item(item_id) @blp.etag(DocEtagSchema) @blp.arguments(DocSchema) @blp.response(DocSchema) def put(self, new_item, item_id): item = self._get_item(item_id) blp.check_etag(item, DocEtagSchema) return collection.put(item_id, new_item) @blp.etag(DocEtagSchema) @blp.response(code=204) def delete(self, item_id): item = self._get_item(item_id) blp.check_etag(item, DocEtagSchema) del collection.items[collection.items.index(item)] else: @blp.route('/') @blp.etag(DocEtagSchema(many=True)) @blp.response(DocSchema(many=True)) def get_resources(): return collection.items @blp.route('/', methods=('POST',)) @blp.etag(DocEtagSchema) @blp.arguments(DocSchema) @blp.response(DocSchema, code=201) def post_resource(new_item): return collection.post(new_item) def _get_item(item_id): try: return collection.get_by_id(item_id) except ItemNotFound: abort(404) @blp.route('/<int:item_id>') @blp.etag(DocEtagSchema) @blp.response(DocSchema) def get_resource(item_id): return _get_item(item_id) @blp.route('/<int:item_id>', methods=('PUT',)) @blp.etag(DocEtagSchema) @blp.arguments(DocSchema) @blp.response(DocSchema) def put_resource(new_item, item_id): item = _get_item(item_id) blp.check_etag(item) return collection.put(item_id, new_item) @blp.route('/<int:item_id>', methods=('DELETE',)) @blp.etag(DocEtagSchema) @blp.response(code=204) def delete_resource(item_id): item = _get_item(item_id) blp.check_etag(item) del collection.items[collection.items.index(item)] api = Api(app) api.register_blueprint(blp) return app
OPENAPI_SWAGGER_UI_PATH = "/swagger" OPENAPI_SWAGGER_URL = "/swagger" API_SPEC_OPTIONS = {"x-internal-id": "2"} CELERY_BROKER_URL = "redis://localhost:6379/0" CELERY_RESULT_BACKEND = "redis://localhost:6379/0" app = Flask(__name__) # Celery configuration app.config["CELERY_BROKER_URL"] = "redis://localhost:6379/0" app.config["CELERY_RESULT_BACKEND"] = "redis://localhost:6379/0" app.config["UPLOAD_FOLDER"] = "downloads" app.config["API_TITLE"] = "My API" app.config["API_VERSION"] = "v1" app.config["OPENAPI_VERSION"] = "3.0.2" app.config["OPENAPI_URL_PREFIX"] = "/doc" app.config["OPENAPI_REDOC_PATH"] = "/redoc" app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger" app.config["OPENAPI_SWAGGER_URL"] = "/swagger" # Initialize extensions # Initialize Celery celery = Celery(app.name, broker=app.config["CELERY_BROKER_URL"]) # celery.autodiscover_tasks() celery.conf.update(app.config) api = Api(app)
"""REST api extension initialization""" from flask_rest_api import Api from flask_rest_api import abort, Blueprint, Page, check_etag, set_etag # noqa from .. import marshmallow as ext_ma from .converters import UUIDConverter from .hateoas import ma_hateoas from .custom_fields import FileField from .pagination import SQLCursorPage # noqa from .schemas import ErrorSchema, Float from .hateoas_apispec_plugin import HateoasPlugin rest_api = Api() def init_app(app): """Initialize REST api""" hateoas_plugin = HateoasPlugin() rest_api.init_app(app, spec_kwargs={'plugins': (hateoas_plugin, )}) ma_hateoas.init_app(app) # Register UUIDConverter in Flask and in doc rest_api.register_converter(UUIDConverter, 'string', 'UUID', name='uuid') # Register hateoas custom Marshmallow fields in doc rest_api.register_field(ext_ma.fields.StrictDateTime, 'string', 'date-time') rest_api.register_field(ext_ma.fields.StringList, 'array', None)
def create_app_mock(config_cls=None): """Return a basic API sample""" class AlbumSchema(ma.Schema): """Album resource schema""" class Meta: """Album schema Meta properties""" strict = True id = ma.fields.Integer() name = ma.fields.String() # Smart hyperlinking, hateoas style ! _links = ma_hateoas.Hyperlinks( schema={ 'self': ma_hateoas.UrlFor(endpoint='albums.AlbumResourceById', album_id='<id>'), 'collection': ma_hateoas.UrlFor(endpoint='albums.AlbumResources') }) _embedded = ma_hateoas.Hyperlinks( schema={ 'songs': { '_links': { 'collection': ma_hateoas.UrlFor(endpoint='songs.SongResources', album_id='<id>') } } }) blp_albums = Blueprint('albums', __name__, url_prefix='/albums') @blp_albums.route('/') class AlbumResources(MethodView): """Album resources endpoints""" @blp_albums.arguments(AlbumSchema, location='query') @blp_albums.response(AlbumSchema(many=True)) @blp_albums.paginate(Page) def get(self, args): """Return a list of resources""" album_datas = [{ 'id': 0, 'name': 'Freak Out!' }, { 'id': 1, 'name': 'Absolutely Free' }] return album_datas @blp_albums.arguments(AlbumSchema) @blp_albums.response(AlbumSchema, code=201) def post(self, new_item): """Create and return a resource""" return new_item @blp_albums.route('/<int:album_id>') class AlbumResourceById(MethodView): """Album resource endpoints""" @blp_albums.response(AlbumSchema) def get(self, album_id): """Return a resource from its ID""" album_data = {'id': album_id, 'name': 'Freak Out!'} return album_data class SongSchema(ma.Schema): """Song resource schema""" class Meta: """Song schema Meta properties""" strict = True id = ma.fields.Integer() name = ma.fields.String() album_id = ma.fields.Integer() # Smart hyperlinking, hateoas style ! _links = ma_hateoas.Hyperlinks({ 'self': ma_hateoas.UrlFor(endpoint='songs.SongResourceById', song_id='<id>'), 'collection': ma_hateoas.UrlFor(endpoint='songs.SongResources'), 'parent': ma_hateoas.UrlFor(endpoint='albums.AlbumResourceById', album_id='<album_id>') }) blp_songs = Blueprint('songs', __name__, url_prefix='/songs') @blp_songs.route('/') class SongResources(MethodView): """Song resources endpoints""" @blp_songs.arguments(SongSchema, location='query') @blp_songs.response(SongSchema(many=True)) @blp_songs.paginate(Page) def get(self, args): """Return a list of resources""" song_datas = [{ 'id': 0, 'name': 'Hungry Freaks Daddy', 'album_id': 0 }, { 'id': 1, 'name': 'I Ain\'t Got No Heart', 'album_id': 0 }] return song_datas @blp_songs.arguments(SongSchema) @blp_songs.response(SongSchema, code=201) def post(self, new_item): """Create and return a resource""" return new_item @blp_songs.route('/<int:song_id>') class SongResourceById(MethodView): """Song resource endpoints""" @blp_songs.response(SongSchema) def get(self, song_id): """Return a resource from its ID""" song_data = { 'id': song_id, 'name': 'Hungry Freaks Daddy', 'album_id': 0 } return song_data app = Flask('API Test') app.response_class = JSONResponse if config_cls: app.config.from_object(config_cls) api = Api(app) api.register_blueprint(blp_albums) api.register_blueprint(blp_songs) return app