├── .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 |