def test_func__schema_in_doc__ok__many_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @hapic.with_api_doc() @hapic.input_body(MySchema(many=True)) def my_controller(): return {"name": "test"} app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("definitions", {}).get("MySchema", {}) schema_def = doc.get("definitions", {}).get("MySchema", {}) assert schema_def.get("properties", {}).get("name", {}).get("type") assert doc.get("paths").get("/paper").get("post").get("parameters")[0] schema_ref = doc.get("paths").get("/paper").get("post").get( "parameters")[0] assert schema_ref.get("in") == "body" assert schema_ref.get("name") == "body" assert schema_ref["schema"] == { "items": { "$ref": "#/definitions/MySchema" }, "type": "array", }
def test_unit__generate_output_stream_doc__ok__nominal_case( self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class OuputStreamItemSchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @hapic.with_api_doc() @hapic.output_stream(OuputStreamItemSchema()) async def get_users(request, hapic_data): pass app = web.Application(debug=True) app.router.add_get("/", get_users) hapic.set_context( AiohttpContext( app, default_error_builder=MarshmallowDefaultErrorBuilder())) doc = hapic.generate_doc("aiohttp", "testing") # INFO BS 2019-04-15: Prevent keep of OrderedDict doc = json.loads(json.dumps(doc)) assert "/" in doc.get("paths") assert "get" in doc["paths"]["/"] assert "200" in doc["paths"]["/"]["get"].get("responses", {}) assert { "items": { "$ref": "#/definitions/OuputStreamItemSchema" }, "type": "array" } == doc["paths"]["/"]["get"]["responses"]["200"]["schema"]
def test_func__schema_with_many__ok__with_exclude(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): first_name = marshmallow.fields.String(required=True) last_name = marshmallow.fields.String(required=False) @hapic.with_api_doc() @hapic.output_body(MySchema(many=True, exclude=("last_name", ))) def my_controller(hapic_data=None): pass app.route("/", method="GET", callback=my_controller) doc = hapic.generate_doc() assert { "MySchema_without_last_name": { "type": "object", "properties": { "first_name": { "type": "string" } }, "required": ["first_name"], } } == doc["definitions"]
def test_func__input_files_doc__ok__one_file_and_text(self): hapic = Hapic() hapic.set_context(MyContext()) app = bottle.Bottle() class MySchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) class MyFilesSchema(marshmallow.Schema): file_abc = marshmallow.fields.Raw(required=True) @hapic.with_api_doc() @hapic.input_files(MyFilesSchema()) @hapic.input_body(MySchema()) def my_controller(hapic_data=None): assert hapic_data assert hapic_data.files app.route('/upload', method='POST', callback=my_controller) doc = hapic.generate_doc(app) assert doc assert '/upload' in doc['paths'] assert 'consume' in doc['paths']['/upload']['post'] assert 'multipart/form-data' in doc['paths']['/upload']['post'][ 'consume'] assert 'parameters' in doc['paths']['/upload']['post'] assert { 'name': 'file_abc', 'required': True, 'in': 'formData', 'type': 'file', } in doc['paths']['/upload']['post']['parameters']
def test_func__input_files_doc__ok__one_file(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): file_abc = marshmallow.fields.Raw(required=True) @hapic.with_api_doc() @hapic.input_files(MySchema()) def my_controller(hapic_data=None): assert hapic_data assert hapic_data.files app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc assert "/upload" in doc["paths"] assert "consumes" in doc["paths"]["/upload"]["post"] assert "multipart/form-data" in doc["paths"]["/upload"]["post"][ "consumes"] assert "parameters" in doc["paths"]["/upload"]["post"] assert { "name": "file_abc", "required": True, "in": "formData", "type": "file" } in doc["paths"]["/upload"]["post"]["parameters"]
def test_func_schema_in_doc__ok__additionals_fields__file(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.Raw(required=True, description="a description", example="00010") @hapic.with_api_doc() @hapic.input_files(MySchema()) def my_controller(): return app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc assert "/upload" in doc["paths"] assert "consumes" in doc["paths"]["/upload"]["post"] assert "multipart/form-data" in doc["paths"]["/upload"]["post"][ "consumes"] assert doc.get("paths").get("/upload").get("post").get("parameters")[0] field = doc.get("paths").get("/upload").get("post").get( "parameters")[0] assert field[ "description"] == "a description\n\n*example value: 00010*" # INFO - G.M - 01-06-2018 - Field example not allowed here, # added in description instead assert "example" not in field assert field["in"] == "formData" assert field["type"] == "file" assert field["required"] is True
def test_func__errors__http_status_as_int_description(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MyException(Exception): pass @hapic.with_api_doc() @hapic.handle_exception(MyException, http_code=400) def my_controller(hapic_data=None): assert hapic_data app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("paths") assert "/upload" in doc["paths"] assert "post" in doc["paths"]["/upload"] assert "responses" in doc["paths"]["/upload"]["post"] assert "400" in doc["paths"]["/upload"]["post"]["responses"] assert { "description": "400", "schema": { "$ref": "#/definitions/DefaultErrorSchema" }, } == doc["paths"]["/upload"]["post"]["responses"]["400"]
def test_unit__generate_doc__ok__nominal_case(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) class InputPathSchema(marshmallow.Schema): username = marshmallow.fields.String(required=True) class InputQuerySchema(marshmallow.Schema): show_deleted = marshmallow.fields.Boolean(required=False) class UserSchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) @hapic.with_api_doc() @hapic.input_path(InputPathSchema()) @hapic.input_query(InputQuerySchema()) @hapic.output_body(UserSchema()) async def get_user(request, hapic_data): pass @hapic.with_api_doc() @hapic.input_path(InputPathSchema()) @hapic.output_body(UserSchema()) async def post_user(request, hapic_data): pass app = web.Application(debug=True) app.router.add_get("/{username}", get_user) app.router.add_post("/{username}", post_user) hapic.set_context( AiohttpContext(app, default_error_builder=MarshmallowDefaultErrorBuilder()) ) doc = hapic.generate_doc("aiohttp", "testing") # INFO BS 2019-04-15: Prevent keep of OrderedDict doc = json.loads(json.dumps(doc)) assert "UserSchema" in doc.get("definitions") assert {"name": {"type": "string"}} == doc["definitions"]["UserSchema"].get("properties") assert "/{username}" in doc.get("paths") assert "get" in doc["paths"]["/{username}"] assert "post" in doc["paths"]["/{username}"] assert [ {"name": "username", "in": "path", "required": True, "type": "string"}, {"name": "show_deleted", "in": "query", "required": False, "type": "boolean"}, ] == doc["paths"]["/{username}"]["get"]["parameters"] assert { "200": {"schema": {"$ref": "#/definitions/UserSchema"}, "description": "200"} } == doc["paths"]["/{username}"]["get"]["responses"] assert [{"name": "username", "in": "path", "required": True, "type": "string"}] == doc[ "paths" ]["/{username}"]["post"]["parameters"] assert { "200": {"schema": {"$ref": "#/definitions/UserSchema"}, "description": "200"} } == doc["paths"]["/{username}"]["get"]["responses"]
def test_func__errors__multiple_same_http_status_description(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MyFirstException(Exception): pass class MySecondException(Exception): "Just a docstring" class MyThirdException(Exception): "Docstring not used" pass class MyFourthException(Exception): pass @hapic.with_api_doc() @hapic.handle_exception(MyFirstException, http_code=HTTPStatus.BAD_REQUEST) @hapic.handle_exception(MySecondException, http_code=HTTPStatus.BAD_REQUEST) @hapic.handle_exception(MyThirdException, http_code=HTTPStatus.BAD_REQUEST, description="explicit description") @hapic.handle_exception(MyFourthException, http_code=400) def my_controller(hapic_data=None): assert hapic_data app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("paths") assert "/upload" in doc["paths"] assert "post" in doc["paths"]["/upload"] assert "responses" in doc["paths"]["/upload"]["post"] assert "400" in doc["paths"]["/upload"]["post"]["responses"] assert "description" assert doc["paths"]["/upload"]["post"]["responses"]["400"][ "description"] descriptions = doc["paths"]["/upload"]["post"]["responses"]["400"][ "description"].split("\n\n") assert "BAD_REQUEST: Bad request syntax or unsupported method" in descriptions assert "explicit description" in descriptions assert "400" in descriptions assert "Just a docstring" in descriptions assert "Docstring not used" not in descriptions assert doc["paths"]["/upload"]["post"]["responses"]["400"]["schema"] assert { "$ref": "#/definitions/DefaultErrorSchema" } == doc["paths"]["/upload"]["post"]["responses"]["400"]["schema"]
def test_func_schema_in_doc__ok__additionals_fields__forms__string(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.String( required=True, description="a description", example="00010", format="binary", enum=["01000", "11111"], maxLength=5, minLength=5, # Theses none string specific parameters should disappear # in query/path maximum=400, # exclusiveMaximun=False, # minimum=0, # exclusiveMinimum=True, # multipleOf=1, ) @hapic.with_api_doc() @hapic.input_forms(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() assert "multipart/form-data" in doc["paths"]["/paper"]["post"][ "consumes"] assert doc.get("paths").get("/paper").get("post").get("parameters")[0] field = doc.get("paths").get("/paper").get("post").get("parameters")[0] assert field[ "description"] == "a description\n\n*example value: 00010*" # INFO - G.M - 01-06-2018 - Field example not allowed here, # added in description instead assert "example" not in field assert field["format"] == "binary" assert field["in"] == "formData" assert field["type"] == "string" assert field["maxLength"] == 5 assert field["minLength"] == 5 assert field["required"] is True assert field["enum"] == ["01000", "11111"] assert "maximum" not in field
def test_func__output_file_doc__ok__nominal_case(self): hapic = Hapic() hapic.set_context(MyContext()) app = bottle.Bottle() @hapic.with_api_doc() @hapic.output_file('image/jpeg') def my_controller(): return b'101010100101' app.route('/avatar', method='GET', callback=my_controller) doc = hapic.generate_doc(app) assert doc assert '/avatar' in doc['paths'] assert 'produce' in doc['paths']['/avatar']['get'] assert 'image/jpeg' in doc['paths']['/avatar']['get']['produce'] assert 200 in doc['paths']['/avatar']['get']['responses']
def test_func__output_file_doc__ok__nominal_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) @hapic.with_api_doc() @hapic.output_file(["image/jpeg"]) def my_controller(): return b"101010100101" app.route("/avatar", method="GET", callback=my_controller) doc = hapic.generate_doc() assert doc assert "/avatar" in doc["paths"] assert "produces" in doc["paths"]["/avatar"]["get"] assert "image/jpeg" in doc["paths"]["/avatar"]["get"]["produces"] assert "200" in doc["paths"]["/avatar"]["get"]["responses"]
def test_func__tags__ok__nominal_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) @hapic.with_api_doc(tags=["foo", "bar"]) def my_controller(hapic_data=None): assert hapic_data assert hapic_data.files app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("paths") assert "/upload" in doc["paths"] assert "post" in doc["paths"]["/upload"] assert "tags" in doc["paths"]["/upload"]["post"] assert ["foo", "bar"] == doc["paths"]["/upload"]["post"]["tags"]
def test_func_schema_in_doc__ok__additionals_fields__body__number(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.Integer( required=True, description="a number", example="12", format="int64", enum=[4, 6], # Theses none string specific parameters should disappear # in query/path maximum=14, exclusiveMaximun=False, minimum=0, exclusiveMinimum=True, multipleOf=2, ) @hapic.with_api_doc() @hapic.input_body(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() schema_field = (doc.get("definitions", {}).get("MySchema", {}).get("properties", {}).get("category", {})) assert schema_field assert schema_field["description"] == "a number" assert schema_field["example"] == "12" assert schema_field["format"] == "int64" assert schema_field["type"] == "integer" assert schema_field["maximum"] == 14 assert schema_field["minimum"] == 0 assert schema_field["exclusiveMinimum"] is True assert schema_field["enum"] == [4, 6] assert schema_field["multipleOf"] == 2
def test_func_schema_in_doc__ok__additionals_fields__path__number(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.Integer( required=True, description="a number", example="12", format="int64", enum=[4, 6], # Theses none string specific parameters should disappear # in query/path maximum=14, exclusiveMaximun=False, minimum=0, exclusiveMinimum=True, multipleOf=2, ) @hapic.with_api_doc() @hapic.input_path(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("paths").get("/paper").get("post").get("parameters")[0] field = doc.get("paths").get("/paper").get("post").get("parameters")[0] assert field["description"] == "a number\n\n*example value: 12*" # INFO - G.M - 01-06-2018 - Field example not allowed here, # added in description instead assert "example" not in field assert field["format"] == "int64" assert field["in"] == "path" assert field["type"] == "integer" assert field["maximum"] == 14 assert field["minimum"] == 0 assert field["exclusiveMinimum"] is True assert field["required"] is True assert field["enum"] == [4, 6] assert field["multipleOf"] == 2
def test_unit__generate_doc_with_wildcard__ok__fixed_methods(self, aiohttp_client, loop): hapic = Hapic(async_=True, processor_class=MarshmallowProcessor) @hapic.with_api_doc() async def a_proxy(request): pass app = web.Application(debug=True) app.router.add_route(hdrs.METH_ANY, path="/", handler=a_proxy) hapic.set_context( AiohttpContext(app, default_error_builder=MarshmallowDefaultErrorBuilder()) ) doc = hapic.generate_doc("aiohttp", "testing", wildcard_method_replacement=["head"]) # INFO BS 2019-04-15: Prevent keep of OrderedDict doc = json.loads(json.dumps(doc)) assert 1 == len(doc["paths"]["/"]) assert "head" in doc["paths"]["/"]
def test_func__docstring__ok__simple_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) @hapic.with_api_doc() def my_controller(hapic_data=None): """ Hello doc """ assert hapic_data assert hapic_data.files app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("paths") assert "/upload" in doc["paths"] assert "post" in doc["paths"]["/upload"] assert "description" in doc["paths"]["/upload"]["post"] assert "Hello doc" == doc["paths"]["/upload"]["post"]["description"]
def test_func__schema_in_doc__ok__many_and_exclude_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): name = marshmallow.fields.String(required=True) name2 = marshmallow.fields.String(required=True) @hapic.with_api_doc() @hapic.input_body(MySchema(exclude=("name2", ), many=True)) def my_controller(): return {"name": "test"} app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() definitions = doc.get("definitions", {}) # TODO - G-M - Find better way to find our new schema # Do Better test when we were able to set correctly schema name # according to content schema_name = None for elem in definitions.keys(): if elem != "MySchema": schema_name = elem break assert schema_name schema_def = definitions[schema_name] assert schema_def.get("properties", {}).get("name", {}).get("type") == "string" assert doc.get("paths").get("/paper").get("post").get("parameters")[0] schema_ref = doc.get("paths").get("/paper").get("post").get( "parameters")[0] assert schema_ref.get("in") == "body" assert schema_ref["schema"] == { "items": { "$ref": "#/definitions/{}".format(schema_name) }, "type": "array", }
def test_func__enum__nominal_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.String( validate=OneOf(["foo", "bar"])) @hapic.with_api_doc() @hapic.input_body(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() assert ["foo", "bar"] == doc.get("definitions", {}).get("MySchema", {}).get("properties", {}).get("category", {}).get("enum")
def test_func__errors__nominal_case(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) @hapic.with_api_doc() @hapic.handle_exception() def my_controller(hapic_data=None): assert hapic_data app.route("/upload", method="POST", callback=my_controller) doc = hapic.generate_doc() assert doc.get("paths") assert "/upload" in doc["paths"] assert "post" in doc["paths"]["/upload"] assert "responses" in doc["paths"]["/upload"]["post"] assert "500" in doc["paths"]["/upload"]["post"]["responses"] assert { "description": Exception.__doc__, "schema": { "$ref": "#/definitions/DefaultErrorSchema" }, } == doc["paths"]["/upload"]["post"]["responses"]["500"]
def test_func_schema_in_doc__ok__additionals_fields__body__string(self): hapic = Hapic(processor_class=MarshmallowProcessor) app = AgnosticApp() hapic.set_context(AgnosticContext(app=app)) class MySchema(marshmallow.Schema): category = marshmallow.fields.String( required=True, description="a description", example="00010", format="binary", enum=["01000", "11111"], maxLength=5, minLength=5, ) @hapic.with_api_doc() @hapic.input_body(MySchema()) def my_controller(): return app.route("/paper", method="POST", callback=my_controller) doc = hapic.generate_doc() schema_field = (doc.get("definitions", {}).get("MySchema", {}).get("properties", {}).get("category", {})) assert schema_field assert schema_field["description"] == "a description" assert schema_field["example"] == "00010" assert schema_field["format"] == "binary" assert schema_field["type"] == "string" assert schema_field["maxLength"] == 5 assert schema_field["minLength"] == 5 assert schema_field["enum"] == ["01000", "11111"]
@hapic.with_api_doc() @hapic.input_path(EmptyPath) @hapic.input_body(PartialSensor) @hapic.output_body(Sensor) async def PATCH_sensor(request, hapic_data: HapicData): print(hapic_data.body) if hapic_data.body.name and sensor.name != hapic_data.body.name: sensor.name = hapic_data.body.name if hapic_data.body.location: if sensor.location != hapic_data.body.location: sensor.location = hapic_data.body.location return sensor app = web.Application() app.add_routes([ web.get(r"/about", GET_about), web.get(r"/sensor", GET_sensor), web.patch(r"/sensor", PATCH_sensor), ]) hapic.set_context( AiohttpContext(app, default_error_builder=SerpycoDefaultErrorBuilder())) hapic.add_documentation_view("/api/doc", "DOC", "Generated doc") print(json.dumps(hapic.generate_doc())) aiohttp_autoreload.start() web.run_app(app)
del resp.headers["Content-Type"] return resp def bind(self, app: flask.Flask): app.add_url_rule("/about", view_func=self.about) app.add_url_rule("/users", view_func=self.get_users) app.add_url_rule("/users2", view_func=self.get_users2) app.add_url_rule("/users/<id>", view_func=self.get_user) app.add_url_rule("/users/", view_func=self.add_user, methods=["POST"]) app.add_url_rule("/users/<id>", view_func=self.del_user, methods=["DELETE"]) if __name__ == "__main__": app = flask.Flask(__name__) controllers = FlaskController() controllers.bind(app) hapic.set_context( FlaskContext(app, default_error_builder=MarshmallowDefaultErrorBuilder())) time.sleep(1) s = json.dumps( hapic.generate_doc(title="Fake API", description="just an example of hapic API")) time.sleep(1) # print swagger doc print(s) # Run app app.run(host="localhost", port=8082, debug=True)
BottleContext(app, default_error_builder=MarshmallowDefaultErrorBuilder())) print("") print("") print("GENERATING OPENAPI DOCUMENTATION") doc_title = "Demo API documentation" doc_description = ("This documentation has been generated from " "code. You can see it using swagger: " "http://editor2.swagger.io/") # TODO: add support for documentation view in bottle # hapic.add_documentation_view('/doc/', doc_title, doc_description) openapi_file_name = "api-documentation.json" with open(openapi_file_name, "w") as openapi_file_handle: openapi_file_handle.write( json.dumps( hapic.generate_doc(title=doc_title, description=doc_description))) print("Documentation generated in {}".format(openapi_file_name)) time.sleep(1) print("") print("") print("RUNNING BOTTLE SERVER NOW") # TODO: add support for documentation view in bottle # print('DOCUMENTATION AVAILABLE AT /doc/') # Run app app.run(host="127.0.0.1", port=8081, debug=True)
controllers = BottleController() controllers.bind(app) hapic.set_context(BottleContext(app, default_error_builder=SerpycoDefaultErrorBuilder())) print("") print("") print("GENERATING OPENAPI DOCUMENTATION") doc_title = "Demo API documentation" doc_description = ( "This documentation has been generated from " "code. You can see it using swagger: " "http://editor2.swagger.io/" ) hapic.add_documentation_view("/doc/", doc_title, doc_description) openapi_file_name = "api-documentation.json" with open(openapi_file_name, "w") as openapi_file_handle: openapi_file_handle.write( json.dumps(hapic.generate_doc(title=doc_title, description=doc_description)) ) print("Documentation generated in {}".format(openapi_file_name)) time.sleep(1) print("") print("") print("RUNNING BOTTLE SERVER NOW") print("DOCUMENTATION AVAILABLE AT /doc/") # Run app app.run(host="127.0.0.1", port=8081, debug=True)