├── .gitignore ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── api ├── paths │ ├── basic.py │ ├── categories.py │ └── pets.py ├── project.py ├── requests │ └── pets.py └── schemas │ ├── categories.py │ ├── errors.py │ └── pets.py ├── build.py ├── config.py ├── internal ├── helpers.py └── statuses.py ├── misc ├── api.yaml ├── screen.png └── templates │ └── swagger_ui.html └── swagger_ui.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | __pycache__/ 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | FLASK_APP="swagger_ui.py" FLASK_ENV="development" python -m flask run --port=8060 3 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | apispec = {extras = ["validation", "yaml"],version = "==4.0.0"} 10 | marshmallow = "==3.8.0" 11 | Flask = "==1.1.2" 12 | 13 | [requires] 14 | python_version = "3.7" 15 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "98498cd9029468738e9c25858cf5e92a238903853d2523759a3a365ccbb0a650" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "apispec": { 20 | "extras": [ 21 | "validation", 22 | "yaml" 23 | ], 24 | "hashes": [ 25 | "sha256:20d271f7c8d130719be223fdb122af391ff8d59fb24958c793f632305b87f8ed", 26 | "sha256:360e28e5e84a4d7023b16de2b897327fe3da63ddc8e01f9165b9113b7fe1c48a" 27 | ], 28 | "index": "pypi", 29 | "version": "==4.0.0" 30 | }, 31 | "attrs": { 32 | "hashes": [ 33 | "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594", 34 | "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc" 35 | ], 36 | "version": "==20.2.0" 37 | }, 38 | "certifi": { 39 | "hashes": [ 40 | "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", 41 | "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" 42 | ], 43 | "version": "==2020.6.20" 44 | }, 45 | "chardet": { 46 | "hashes": [ 47 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 48 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 49 | ], 50 | "version": "==3.0.4" 51 | }, 52 | "click": { 53 | "hashes": [ 54 | "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", 55 | "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" 56 | ], 57 | "version": "==7.1.2" 58 | }, 59 | "flask": { 60 | "hashes": [ 61 | "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", 62 | "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" 63 | ], 64 | "index": "pypi", 65 | "version": "==1.1.2" 66 | }, 67 | "idna": { 68 | "hashes": [ 69 | "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", 70 | "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" 71 | ], 72 | "version": "==2.10" 73 | }, 74 | "importlib-metadata": { 75 | "hashes": [ 76 | "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", 77 | "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3" 78 | ], 79 | "markers": "python_version < '3.8'", 80 | "version": "==2.0.0" 81 | }, 82 | "itsdangerous": { 83 | "hashes": [ 84 | "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", 85 | "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" 86 | ], 87 | "version": "==1.1.0" 88 | }, 89 | "jinja2": { 90 | "hashes": [ 91 | "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", 92 | "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" 93 | ], 94 | "version": "==2.11.2" 95 | }, 96 | "jsonschema": { 97 | "hashes": [ 98 | "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163", 99 | "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a" 100 | ], 101 | "version": "==3.2.0" 102 | }, 103 | "markupsafe": { 104 | "hashes": [ 105 | "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", 106 | "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", 107 | "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", 108 | "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", 109 | "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", 110 | "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", 111 | "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", 112 | "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", 113 | "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", 114 | "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", 115 | "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", 116 | "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", 117 | "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", 118 | "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", 119 | "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", 120 | "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", 121 | "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", 122 | "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", 123 | "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", 124 | "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", 125 | "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", 126 | "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", 127 | "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", 128 | "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", 129 | "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", 130 | "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", 131 | "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", 132 | "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", 133 | "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", 134 | "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", 135 | "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", 136 | "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", 137 | "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" 138 | ], 139 | "version": "==1.1.1" 140 | }, 141 | "marshmallow": { 142 | "hashes": [ 143 | "sha256:2272273505f1644580fbc66c6b220cc78f893eb31f1ecde2af98ad28011e9811", 144 | "sha256:47911dd7c641a27160f0df5fd0fe94667160ffe97f70a42c3cc18388d86098cc" 145 | ], 146 | "index": "pypi", 147 | "version": "==3.8.0" 148 | }, 149 | "openapi-spec-validator": { 150 | "hashes": [ 151 | "sha256:6dd75e50c94f1bb454d0e374a56418e7e06a07affb2c7f1df88564c5d728dac3", 152 | "sha256:79381a69b33423ee400ae1624a461dae7725e450e2e306e32f2dd8d16a4d85cb", 153 | "sha256:ec1b01a00e20955a527358886991ae34b4b791b253027ee9f7df5f84b59d91c7" 154 | ], 155 | "version": "==0.2.9" 156 | }, 157 | "prance": { 158 | "extras": [ 159 | "osv" 160 | ], 161 | "hashes": [ 162 | "sha256:86ec64378036471efdd9681648d8da6b39e5143ea2e6981b7863a33fbc75d739", 163 | "sha256:b87504dffb40c1e29aca0ca01f5f3dbb1941f29fd32c3a5e0fbd90ae37f749a3" 164 | ], 165 | "version": "==0.19.0" 166 | }, 167 | "pyrsistent": { 168 | "hashes": [ 169 | "sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e" 170 | ], 171 | "version": "==0.17.3" 172 | }, 173 | "pyyaml": { 174 | "hashes": [ 175 | "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", 176 | "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", 177 | "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", 178 | "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", 179 | "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", 180 | "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", 181 | "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", 182 | "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", 183 | "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", 184 | "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", 185 | "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" 186 | ], 187 | "version": "==5.3.1" 188 | }, 189 | "requests": { 190 | "hashes": [ 191 | "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", 192 | "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" 193 | ], 194 | "version": "==2.24.0" 195 | }, 196 | "semver": { 197 | "hashes": [ 198 | "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4", 199 | "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f" 200 | ], 201 | "version": "==2.13.0" 202 | }, 203 | "six": { 204 | "hashes": [ 205 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 206 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 207 | ], 208 | "version": "==1.15.0" 209 | }, 210 | "urllib3": { 211 | "hashes": [ 212 | "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2", 213 | "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e" 214 | ], 215 | "version": "==1.25.11" 216 | }, 217 | "werkzeug": { 218 | "hashes": [ 219 | "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", 220 | "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" 221 | ], 222 | "version": "==1.0.1" 223 | }, 224 | "zipp": { 225 | "hashes": [ 226 | "sha256:16522f69653f0d67be90e8baa4a46d66389145b734345d68a257da53df670903", 227 | "sha256:c1532a8030c32fd52ff6a288d855fe7adef5823ba1d26a29a68fd6314aa72baa" 228 | ], 229 | "version": "==3.3.1" 230 | } 231 | }, 232 | "develop": {} 233 | } 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAPI 3 Generator 2 | 3 | The goal of this project is to create a generator that conveniently creates 4 | API definitions in the OpenAPI 3 format using [marshmallow](https://marshmallow.readthedocs.io/) classes 5 | and saves them into a YAML file. 6 | 7 | You can think this project as programmable API definitions/documentation for your API (your API can be written in any language, not only in Python). 8 | 9 | Python is used here just for convenience of describing classes and has less code yet strong typing. 10 | 11 | Then you can inject the generated YAML file with Swagger UI to any project (just a page that renders 12 | Swagger UI HTML code which requests the generated YAML file). 13 | 14 | #### Why I made the project 15 | 16 | I was developing a project in PHP using this library [php-swagger](https://github.com/zircote/swagger-php). It 17 | has ability to describe an API using annotations in docstrings of classes/methods (you can find alternative 18 | projects for your programming language, the idea behind is to describe API's definitions something near the code: 19 | routes, paths, views, etc). 20 | 21 | But this method has some problems to me: 22 | 23 | - I couldn't describe the API's definitions as real code when the interpreter/compiler could fix my issues 24 | with schemas/paths 25 | - I had issues with spaces in docstrings and it looked ugly and messy 26 | - I wanted to have versioned pages of my API (i.e.: v1.0, v1.1, v1.2) 27 | - I wanted to make validation of the resulted API definitions after writing it 28 | 29 | ### What does the project do? 30 | 31 | - Generates a YAML file of your API with paths, schemes, requests, security methods in the [OpenAPI 3](https://swagger.io/specification/) format 32 | - Runs a Flask web server for prototyping (Flask is used only for development purposes, it is not meant here to 33 | create an API using Flask, but you can do it as well) 34 | 35 | ### Structure of the project 36 | 37 | - `config.py` : basic information about the API (title, description, support contacts, servers, tags info and etc) 38 | - `swagger_ui.py` : a Flask web server for prototyping 39 | - `build.py` : a build script to create the `api.yaml` file 40 | - `api/paths` : your API paths 41 | - `api/requests` : your API requests 42 | - `api/schemas` : your API schemas for responses and request bodies 43 | - `api/project.py` : here you add your classes of schemas, requests and paths 44 | 45 | ### Requirements 46 | 47 | Tools: 48 | 49 | - Python 3.7 50 | - [pipenv](https://docs.pipenv.org) 51 | 52 | Knowledge in: 53 | 54 | - Swagger 55 | - Python 56 | - YAML format 57 | 58 | ## How to use? 59 | 60 | Just fork or copy this project to your computer using git: 61 | 62 | ``` 63 | git clone https://github.com/egorsmkv/openapi3-generator.git 64 | cd openapi3-generator 65 | ``` 66 | 67 | After the cloning, initialize the environment: 68 | 69 | ```bash 70 | # install all dependencies 71 | pipenv install 72 | 73 | # enter the environment 74 | pipenv shell 75 | ``` 76 | 77 | ### One-time mode 78 | 79 | To create an `api.yaml` file with your API definitions in the OpenAPI 3 format: 80 | 81 | ```bash 82 | # create an api.yaml file 83 | python build.py 84 | ``` 85 | 86 | ### Prototyping mode 87 | 88 | If you are prototyping your API then you can run a Flask web server with pre-defined Swagger UI using [GNU make](https://www.gnu.org/software/make/) and start 89 | to create your API by editing schemas, paths and other objects. 90 | 91 | The web server is running in **debug mode**, it will reload the app each time when you change something 92 | in the project and after any change you can just refresh a page to see new version your API definitions in Swagger UI. 93 | 94 | ```bash 95 | make run 96 | 97 | # * Running on http://127.0.0.1:8060/ (Press CTRL+C to quit) 98 | # * Restarting with stat 99 | # * Debugger is active! 100 | ``` 101 | 102 | If you don't have GNU make, use the following commands: 103 | 104 | ```bash 105 | export FLASK_APP="swagger_ui.py" 106 | export FLASK_ENV="development" 107 | 108 | python -m flask run --port=8060 109 | ``` 110 | 111 | You can find the Swagger UI page on http://127.0.0.1:8060 in your browser. 112 | 113 | ## Demo 114 | 115 | 116 | -------------------------------------------------------------------------------- /api/paths/basic.py: -------------------------------------------------------------------------------- 1 | from apispec import APISpec 2 | 3 | from internal.helpers import method, response, response_text 4 | from internal.statuses import OK, REQUEST_URI_TOO_LONG, DEFAULT 5 | 6 | 7 | def add_paths(spec: APISpec): 8 | spec.path( 9 | path='/ping', 10 | operations=dict( 11 | get=method( 12 | tags=['default'], 13 | summary='Ping our API', 14 | responses=[ 15 | response_text(OK, 'It must be a PONG'), 16 | ], 17 | ), 18 | ), 19 | ) 20 | 21 | spec.path( 22 | path='/tests/long-uri', 23 | operations=dict( 24 | get=method( 25 | tags=['default'], 26 | summary='If the URI is long then throw an error', 27 | responses=[ 28 | response(REQUEST_URI_TOO_LONG, dict(description='Request URI too large.')), 29 | response_text(DEFAULT, 'Unexpected error'), 30 | ], 31 | ), 32 | ), 33 | ) 34 | -------------------------------------------------------------------------------- /api/paths/categories.py: -------------------------------------------------------------------------------- 1 | from apispec import APISpec 2 | 3 | from internal.statuses import OK 4 | from internal.helpers import param, method, response_json 5 | from api.schemas.categories import CategorySchema 6 | 7 | 8 | def add_paths(spec: APISpec): 9 | spec.path( 10 | path='/category/{id}', 11 | parameters=[ 12 | param('id', 'Identifier of a category') 13 | ], 14 | operations=dict( 15 | get=method( 16 | tags=['categories'], 17 | responses=[ 18 | response_json(OK, 'Information about a category', CategorySchema), 19 | ] 20 | ) 21 | ), 22 | ) 23 | -------------------------------------------------------------------------------- /api/paths/pets.py: -------------------------------------------------------------------------------- 1 | from apispec import APISpec 2 | 3 | from internal.helpers import param, method, bearer, response_json 4 | from internal.statuses import OK, NOT_FOUND 5 | from api.schemas.errors import ApiErrorSchema 6 | from api.requests.pets import CreatePetRequest, UpdatePetRequest 7 | from api.schemas.pets import PetSchema 8 | 9 | 10 | def add_paths(spec: APISpec): 11 | spec.path( 12 | path='/pet/{id}', 13 | parameters=[ 14 | param('id', 'Identifier of a Pet') 15 | ], 16 | operations=dict( 17 | get=method( 18 | tags=['pets'], 19 | summary='Get a pet', 20 | responses=[ 21 | response_json(OK, 'An object a Pet', PetSchema), 22 | response_json(NOT_FOUND, 'An object does not exist', ApiErrorSchema), 23 | ], 24 | operation_id='getPet', 25 | security=[ 26 | bearer(), 27 | ] 28 | ), 29 | post=method( 30 | tags=['pets'], 31 | summary='Create a pet', 32 | request_body=CreatePetRequest, 33 | responses=[ 34 | response_json(OK, 'A pet is created', PetSchema), 35 | ], 36 | operation_id='postPet', 37 | security=[ 38 | bearer(), 39 | ] 40 | ), 41 | patch=method( 42 | tags=['pets'], 43 | summary='Update a pet', 44 | request_body=UpdatePetRequest, 45 | responses=[ 46 | response_json(OK, 'Response an object of updated pet', PetSchema), 47 | ], 48 | operation_id='patchPet', 49 | security=[ 50 | bearer(), 51 | ] 52 | ), 53 | ), 54 | ) 55 | -------------------------------------------------------------------------------- /api/project.py: -------------------------------------------------------------------------------- 1 | from apispec import APISpec 2 | from apispec.ext.marshmallow import MarshmallowPlugin 3 | 4 | from internal.helpers import security_jwt, add_schema 5 | from api.schemas.categories import CategorySchema 6 | from api.requests.pets import CreatePetRequest, UpdatePetRequest 7 | from api.schemas.pets import PetSchema, CreatePetBody, UpdatePetBody 8 | from config import * 9 | 10 | from api.paths import basic, pets, categories 11 | 12 | spec = APISpec( 13 | title=TITLE, 14 | version=VERSION, 15 | openapi_version=OPENAPI_VERSION, 16 | info=dict( 17 | description=DESCRIPTION, 18 | termsOfService=TERMS_OF_USE, 19 | contact=dict( 20 | name=CONTACT_NAME, 21 | url=CONTACT_URL, 22 | email=CONTACT_EMAIL 23 | ), 24 | license=dict( 25 | name=LICENSE_NAME, 26 | url=LICENSE_URL, 27 | ) 28 | ), 29 | servers=SERVERS, 30 | tags=TAGS, 31 | externalDocs=dict(url=EXTERNAL_DOCS_URL, description=EXTERNAL_DOCS_DESCRIPTION), 32 | plugins=[ 33 | MarshmallowPlugin(), 34 | ], 35 | ) 36 | 37 | # spec.components.security_scheme('ApiKey', security_api_key()) 38 | spec.components.security_scheme('Bearer', security_jwt()) 39 | 40 | # add schemas 41 | add_schema(spec, CategorySchema) 42 | add_schema(spec, PetSchema) 43 | add_schema(spec, [ 44 | CreatePetBody, 45 | UpdatePetBody, 46 | ]) 47 | 48 | # add paths 49 | modules = [ 50 | basic, 51 | categories, 52 | pets 53 | ] 54 | for module in modules: 55 | module.add_paths(spec) 56 | 57 | # list all request classes 58 | requests = [ 59 | CreatePetRequest, 60 | UpdatePetRequest, 61 | ] 62 | -------------------------------------------------------------------------------- /api/requests/pets.py: -------------------------------------------------------------------------------- 1 | from api.schemas.pets import CreatePetBody, UpdatePetBody 2 | from internal.helpers import FORMAT_JSON 3 | 4 | 5 | class CreatePetRequest: 6 | description = 'JSON object with data to create a new pet' 7 | required = True 8 | format = FORMAT_JSON 9 | name = 'CreatePetRequest' 10 | schema = CreatePetBody 11 | 12 | 13 | class UpdatePetRequest: 14 | description = 'JSON object with data to update a pet' 15 | required = True 16 | format = FORMAT_JSON 17 | name = 'UpdatePetRequest' 18 | schema = UpdatePetBody 19 | -------------------------------------------------------------------------------- /api/schemas/categories.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class CategorySchema(Schema): 5 | id = fields.Int( 6 | dump_only=True, 7 | description='ID', 8 | example=1, 9 | ) 10 | 11 | name = fields.Str( 12 | description='Name', 13 | required=True, 14 | example='Pug dogs', 15 | ) 16 | 17 | size = fields.Int( 18 | format='int64', 19 | description='Number of elements in the category', 20 | example=20, 21 | ) 22 | -------------------------------------------------------------------------------- /api/schemas/errors.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, fields 2 | 3 | 4 | class ApiErrorSchema(Schema): 5 | code = fields.Int( 6 | required=True, 7 | ) 8 | 9 | message = fields.Str( 10 | required=True, 11 | ) 12 | -------------------------------------------------------------------------------- /api/schemas/pets.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from marshmallow import Schema, fields 3 | 4 | from api.schemas.categories import CategorySchema 5 | 6 | 7 | class CreatePetBody(Schema): 8 | name = fields.Str(required=True) 9 | 10 | 11 | class UpdatePetBody(Schema): 12 | name = fields.Str() 13 | categories = fields.List( 14 | fields.Nested(CategorySchema), 15 | ) 16 | 17 | 18 | class PetSchema(Schema): 19 | categories = fields.List( 20 | fields.Nested(CategorySchema), 21 | description='Has these categories', 22 | ) 23 | 24 | name = fields.Str() 25 | 26 | created_at = fields.DateTime( 27 | dump_only=True, 28 | default=dt.datetime.utcnow, 29 | title='Created At', 30 | description='When this item was created' 31 | ) 32 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | from api.project import spec, requests 2 | from internal.helpers import add_request_bodies 3 | 4 | 5 | def save(data): 6 | with open('api.yaml', 'w') as f: 7 | f.write(data) 8 | 9 | 10 | if __name__ == '__main__': 11 | yaml = add_request_bodies(spec, requests) 12 | 13 | print(yaml) 14 | 15 | save(yaml) 16 | 17 | print() 18 | print() 19 | print() 20 | 21 | print('The content was saved into the api.yaml file!') 22 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | TITLE = 'Simple API' 2 | DESCRIPTION = 'This is a simple project that demonstrates the OpenAPI 3 Generator project' 3 | VERSION = '1.0.0' 4 | 5 | TERMS_OF_USE = 'http://example.com/terms/' 6 | 7 | CONTACT_NAME = 'API Support' 8 | CONTACT_URL = 'http://example.com/support' 9 | CONTACT_EMAIL = 'support@example.com' 10 | 11 | EXTERNAL_DOCS_DESCRIPTION = 'More docs about this API' 12 | EXTERNAL_DOCS_URL = 'http://docs.example.com' 13 | 14 | LICENSE_NAME = 'MIT' 15 | LICENSE_URL = 'https://en.wikipedia.org/wiki/MIT_License' 16 | 17 | OPENAPI_VERSION = '3.0.3' 18 | 19 | SERVERS = [ 20 | dict(url='https://production.example.com', description='Production server'), 21 | dict( 22 | url='https://{customer}.stage.example.com:{port}', 23 | description='Stage server for a customer', 24 | variables=dict( 25 | username=dict( 26 | default='mark', 27 | description='A customer identifier', 28 | ), 29 | port=dict( 30 | default='8001', 31 | enum=['8001', '443'], 32 | description='A customer identifier', 33 | ), 34 | ) 35 | ), 36 | dict(url='http://localhost:8000', description='Development server'), 37 | ] 38 | 39 | TAGS = [ 40 | dict( 41 | name='pets', 42 | description='Everything about your Pets', 43 | externalDocs=dict(description='Find out more', url='http://swagger.io') 44 | ), 45 | dict( 46 | name='categories', 47 | description='Everything about categories', 48 | ), 49 | dict( 50 | name='default', 51 | description='Basic paths' 52 | ) 53 | ] 54 | -------------------------------------------------------------------------------- /internal/helpers.py: -------------------------------------------------------------------------------- 1 | from apispec import APISpec 2 | from apispec.yaml_utils import dict_to_yaml 3 | 4 | FORMAT_JSON = 'application/json' 5 | FORMAT_TEXT = 'text/plain' 6 | 7 | 8 | def response(status: str, value: dict): 9 | return dict( 10 | key=status, 11 | value=value 12 | ) 13 | 14 | 15 | def response_json(status: str, description: str, schema): 16 | return dict( 17 | key=status, 18 | value=dict( 19 | description=description, 20 | content={FORMAT_JSON: dict(schema=schema.__name__)} 21 | ) 22 | ) 23 | 24 | 25 | def response_text(status: str, description: str): 26 | return dict( 27 | key=status, 28 | value=dict( 29 | description=description, 30 | content={FORMAT_TEXT: dict(schema=dict(type='string'))} 31 | ) 32 | ) 33 | 34 | 35 | def method(responses, tags=None, summary=None, request_body=None, operation_id=None, security=None): 36 | if not tags: 37 | tags = [] 38 | 39 | data_responses = {} 40 | for it in responses: 41 | data_responses[it['key']] = it['value'] 42 | 43 | data = dict( 44 | responses=data_responses, 45 | tags=tags, 46 | ) 47 | 48 | if request_body: 49 | ref = {'$ref': f'#/components/requestBodies/{request_body().name}'} 50 | data['requestBody'] = ref 51 | 52 | if summary: 53 | data['summary'] = summary 54 | 55 | if operation_id: 56 | data['operationId'] = operation_id 57 | 58 | if security: 59 | data['security'] = security 60 | 61 | return data 62 | 63 | 64 | def add_request_bodies(doc, requests): 65 | doc_raw = doc.to_dict() 66 | doc_raw['components']['requestBodies'] = make_requests(requests) 67 | 68 | return dict_to_yaml(doc_raw) 69 | 70 | 71 | def add_schema(spec: APISpec, schema): 72 | if isinstance(schema, list): 73 | for item in schema: 74 | spec.components.schema(item.__name__, schema=item) 75 | else: 76 | spec.components.schema(schema.__name__, schema=schema) 77 | 78 | 79 | def make_request(request): 80 | ref_name = f'#/components/schemas/{request.schema.__name__}' 81 | 82 | return dict( 83 | key=request.name, 84 | value=dict( 85 | description=request.description, 86 | required=request.required, 87 | content={request.format: dict(schema={'$ref': ref_name})} 88 | ) 89 | ) 90 | 91 | 92 | def make_requests(lst: list): 93 | requests = {} 94 | 95 | for item in lst: 96 | data = make_request(item) 97 | requests[data['key']] = data['value'] 98 | 99 | return requests 100 | 101 | 102 | def param(name: str, description: str): 103 | return {'name': name, 'in': 'path', 'required': True, 'description': description, 'schema': {'type': 'string'}} 104 | 105 | 106 | def security_api_key(): 107 | return {'type': 'apiKey', 'in': 'header', 'name': 'X-API-Key'} 108 | 109 | 110 | def security_jwt(): 111 | return {'type': 'http', 'scheme': 'bearer', 'bearerFormat': 'JWT'} 112 | 113 | 114 | def api_key(): 115 | return {'ApiKey': []} 116 | 117 | 118 | def bearer(): 119 | return {'Bearer': []} 120 | -------------------------------------------------------------------------------- /internal/statuses.py: -------------------------------------------------------------------------------- 1 | # informational 2 | CONTINUE = '100' 3 | SWITCHING_PROTOCOLS = '101' 4 | PROCESSING = '102' 5 | 6 | # success 7 | OK = '200' 8 | CREATED = '201' 9 | ACCEPTED = '202' 10 | NON_AUTHORITATIVE_INFORMATION = '203' 11 | NO_CONTENT = '204' 12 | RESET_CONTENT = '205' 13 | PARTIAL_CONTENT = '206' 14 | MULTI_STATUS = '207' 15 | ALREADY_REPORTED = '208' 16 | IM_USED = '226' 17 | 18 | # redirection 19 | MULTIPLE_CHOICES = '300' 20 | MOVED_PERMANENTLY = '301' 21 | FOUND = '302' 22 | SEE_OTHER = '303' 23 | NOT_MODIFIED = '304' 24 | USE_PROXY = '305' 25 | TEMPORARY_REDIRECT = '307' 26 | PERMANENT_REDIRECT = '308' 27 | 28 | # client error 29 | BAD_REQUEST = '400' 30 | UNAUTHORIZED = '401' 31 | PAYMENT_REQUIRED = '402' 32 | FORBIDDEN = '403' 33 | NOT_FOUND = '404' 34 | METHOD_NOT_ALLOWED = '405' 35 | NOT_ACCEPTABLE = '406' 36 | PROXY_AUTHENTICATION_REQUIRED = '407' 37 | REQUEST_TIMEOUT = '408' 38 | CONFLICT = '409' 39 | GONE = '410' 40 | LENGTH_REQUIRED = '411' 41 | PRECONDITION_FAILED = '412' 42 | REQUEST_ENTITY_TOO_LARGE = '413' 43 | REQUEST_URI_TOO_LONG = '414' 44 | UNSUPPORTED_MEDIA_TYPE = '415' 45 | REQUESTED_RANGE_NOT_SATISFIABLE = '416' 46 | EXPECTATION_FAILED = '417' 47 | MISDIRECTED_REQUEST = '421' 48 | UNPROCESSABLE_ENTITY = '422' 49 | LOCKED = '423' 50 | FAILED_DEPENDENCY = '424' 51 | UPGRADE_REQUIRED = '426' 52 | PRECONDITION_REQUIRED = '428' 53 | TOO_MANY_REQUESTS = '429' 54 | REQUEST_HEADER_FIELDS_TOO_LARGE = '431' 55 | 56 | # server errors 57 | INTERNAL_SERVER_ERROR = '500' 58 | NOT_IMPLEMENTED = '501' 59 | BAD_GATEWAY = '502' 60 | SERVICE_UNAVAILABLE = '503' 61 | GATEWAY_TIMEOUT = '504' 62 | HTTP_VERSION_NOT_SUPPORTED = '505' 63 | VARIANT_ALSO_NEGOTIATES = '506' 64 | INSUFFICIENT_STORAGE = '507' 65 | LOOP_DETECTED = '508' 66 | NOT_EXTENDED = '510' 67 | NETWORK_AUTHENTICATION_REQUIRED = '511' 68 | 69 | # not HTTP statuses but related to OpenAPI 70 | DEFAULT = 'default' 71 | -------------------------------------------------------------------------------- /misc/api.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | requestBodies: 3 | CreatePetRequest: 4 | content: 5 | application/json: 6 | schema: 7 | $ref: '#/components/schemas/CreatePetBody' 8 | description: JSON object with data to create a new pet 9 | required: true 10 | UpdatePetRequest: 11 | content: 12 | application/json: 13 | schema: 14 | $ref: '#/components/schemas/UpdatePetBody' 15 | description: JSON object with data to update a pet 16 | required: true 17 | schemas: 18 | ApiError: 19 | properties: 20 | code: 21 | type: integer 22 | message: 23 | type: string 24 | required: 25 | - code 26 | - message 27 | type: object 28 | CategorySchema: 29 | properties: 30 | id: 31 | description: ID 32 | example: 1 33 | readOnly: true 34 | type: integer 35 | name: 36 | description: Name 37 | example: Pug dogs 38 | type: string 39 | size: 40 | description: Number of elements in the category 41 | example: 20 42 | format: int64 43 | type: integer 44 | required: 45 | - name 46 | type: object 47 | CreatePetBody: 48 | properties: 49 | name: 50 | type: string 51 | required: 52 | - name 53 | type: object 54 | PetSchema: 55 | properties: 56 | categories: 57 | description: Has these categories 58 | items: 59 | $ref: '#/components/schemas/CategorySchema' 60 | type: array 61 | created_at: 62 | description: When this item was created 63 | format: date-time 64 | readOnly: true 65 | title: Created At 66 | type: string 67 | name: 68 | type: string 69 | type: object 70 | UpdatePetBody: 71 | properties: 72 | categories: 73 | items: 74 | $ref: '#/components/schemas/CategorySchema' 75 | type: array 76 | name: 77 | type: string 78 | type: object 79 | securitySchemes: 80 | Bearer: 81 | bearerFormat: JWT 82 | scheme: bearer 83 | type: http 84 | externalDocs: 85 | description: More docs about this API 86 | url: http://docs.example.com 87 | info: 88 | contact: 89 | email: support@example.com 90 | name: API Support 91 | url: http://example.com/support 92 | description: This is a simple project that demonstrates the OpenAPI 3 Generator 93 | project 94 | license: 95 | name: MIT 96 | url: https://en.wikipedia.org/wiki/MIT_License 97 | termsOfService: http://example.com/terms/ 98 | title: Simple API 99 | version: 1.0.0 100 | openapi: 3.0.3 101 | paths: 102 | /ping: 103 | get: 104 | responses: 105 | '200': 106 | content: 107 | text/plain: 108 | schema: 109 | type: string 110 | description: It must be a PONG 111 | summary: Ping our API 112 | tags: 113 | - default 114 | /tests/long-uri: 115 | get: 116 | responses: 117 | '414': 118 | description: Request URI too large. 119 | default: 120 | content: 121 | text/plain: 122 | schema: 123 | type: string 124 | description: Unexpected error 125 | summary: If the URI is long then throw an error 126 | tags: 127 | - default 128 | /category/{id}: 129 | get: 130 | responses: 131 | '200': 132 | content: 133 | application/json: 134 | schema: 135 | $ref: '#/components/schemas/CategorySchema' 136 | description: Information about a category 137 | tags: 138 | - categories 139 | parameters: 140 | - description: Identifier of a category 141 | in: path 142 | name: id 143 | required: true 144 | schema: 145 | type: string 146 | /pet/{id}: 147 | get: 148 | operationId: getPet 149 | responses: 150 | '200': 151 | content: 152 | application/json: 153 | schema: 154 | $ref: '#/components/schemas/PetSchema' 155 | description: An object a Pet 156 | '404': 157 | content: 158 | application/json: 159 | schema: 160 | $ref: '#/components/schemas/ApiError' 161 | description: An object does not exist 162 | security: 163 | - Bearer: [] 164 | summary: Get a pet 165 | tags: 166 | - pets 167 | parameters: 168 | - description: Identifier of a Pet 169 | in: path 170 | name: id 171 | required: true 172 | schema: 173 | type: string 174 | patch: 175 | operationId: patchPet 176 | requestBody: 177 | $ref: '#/components/requestBodies/UpdatePetRequest' 178 | responses: 179 | '200': 180 | content: 181 | application/json: 182 | schema: 183 | $ref: '#/components/schemas/PetSchema' 184 | description: Response an object of updated pet 185 | security: 186 | - Bearer: [] 187 | summary: Update a pet 188 | tags: 189 | - pets 190 | post: 191 | operationId: postPet 192 | requestBody: 193 | $ref: '#/components/requestBodies/CreatePetRequest' 194 | responses: 195 | '200': 196 | content: 197 | application/json: 198 | schema: 199 | $ref: '#/components/schemas/PetSchema' 200 | description: A pet is created 201 | security: 202 | - Bearer: [] 203 | summary: Create a pet 204 | tags: 205 | - pets 206 | servers: 207 | - description: Production server 208 | url: https://production.example.com 209 | - description: Stage server for a customer 210 | url: https://{customer}.stage.example.com:{port} 211 | variables: 212 | port: 213 | default: '8001' 214 | description: A customer identifier 215 | enum: 216 | - '8001' 217 | - '443' 218 | username: 219 | default: mark 220 | description: A customer identifier 221 | - description: Development server 222 | url: http://localhost:8000 223 | tags: 224 | - description: Everything about your Pets 225 | externalDocs: 226 | description: Find out more 227 | url: http://swagger.io 228 | name: pets 229 | - description: Everything about categories 230 | name: categories 231 | - description: Basic paths 232 | name: default 233 | -------------------------------------------------------------------------------- /misc/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egorsmkv/openapi3-generator/d39196ee52504d5931be7e76fd99a6397c233a62/misc/screen.png -------------------------------------------------------------------------------- /misc/templates/swagger_ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger: API 7 | 8 | 9 | 10 | 11 | 12 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | 75 | 76 | 77 | 78 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /swagger_ui.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | from api.project import spec, requests 3 | from internal.helpers import add_request_bodies 4 | 5 | app = Flask(__name__, template_folder='misc/templates') 6 | 7 | 8 | @app.route('/swagger.yaml') 9 | def swagger_yaml(): 10 | return add_request_bodies(spec, requests) 11 | 12 | 13 | @app.route('/') 14 | def index(): 15 | return render_template('swagger_ui.html', url='http://127.0.0.1:8060/swagger.yaml') 16 | --------------------------------------------------------------------------------