fastapi2postman 0.1.2__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi2postman-0.1.2 → fastapi2postman-0.2.0}/LICENSE +20 -20
- fastapi2postman-0.2.0/PKG-INFO +135 -0
- fastapi2postman-0.2.0/README.md +108 -0
- fastapi2postman-0.2.0/pyproject.toml +80 -0
- fastapi2postman-0.2.0/pyproject.toml.orig +65 -0
- fastapi2postman-0.2.0/src/fastapi2postman/__init__.py +327 -0
- fastapi2postman-0.2.0/src/fastapi2postman/cli.py +89 -0
- fastapi2postman-0.2.0/src/fastapi2postman/py.typed +0 -0
- fastapi2postman-0.1.2/PKG-INFO +0 -105
- fastapi2postman-0.1.2/README.rst +0 -74
- fastapi2postman-0.1.2/fastapi2postman.egg-info/PKG-INFO +0 -105
- fastapi2postman-0.1.2/fastapi2postman.egg-info/SOURCES.txt +0 -10
- fastapi2postman-0.1.2/fastapi2postman.egg-info/dependency_links.txt +0 -1
- fastapi2postman-0.1.2/fastapi2postman.egg-info/entry_points.txt +0 -3
- fastapi2postman-0.1.2/fastapi2postman.egg-info/requires.txt +0 -4
- fastapi2postman-0.1.2/fastapi2postman.egg-info/top_level.txt +0 -1
- fastapi2postman-0.1.2/fastapi2postman.py +0 -72
- fastapi2postman-0.1.2/setup.cfg +0 -4
- fastapi2postman-0.1.2/setup.py +0 -45
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2023 ashhadahsan
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 ashhadahsan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
21
|
SOFTWARE.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi2postman
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Turn a FastAPI app into a Postman collection you can run straight away.
|
|
5
|
+
Keywords: fastapi,postman,newman,openapi,api,cli
|
|
6
|
+
Author: Ashhad Ahsan Rehman
|
|
7
|
+
Author-email: Ashhad Ahsan Rehman <ashhadahsan@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Dist: fastapi>=0.105
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Project-URL: Source, https://github.com/ashhadahsan/fastapi2postman
|
|
25
|
+
Project-URL: Changelog, https://github.com/ashhadahsan/fastapi2postman/blob/main/CHANGELOG.md
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# fastapi2postman
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/fastapi2postman/)
|
|
31
|
+
[](https://pypi.org/project/fastapi2postman/)
|
|
32
|
+
[](https://github.com/ashhadahsan/fastapi2postman/actions/workflows/ci.yml)
|
|
33
|
+
[](https://github.com/ashhadahsan/fastapi2postman/blob/main/LICENSE)
|
|
34
|
+
|
|
35
|
+
**Turn a FastAPI app into a Postman collection you can run straight away.**
|
|
36
|
+
|
|
37
|
+
```console
|
|
38
|
+
$ pip install fastapi2postman
|
|
39
|
+
$ fastapi2postman --app main.py
|
|
40
|
+
output.json: 25 requests
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Import `output.json` into Postman, fill in the collection's variables, and send the login request. The token is saved for every other request.
|
|
44
|
+
|
|
45
|
+
Install it in the same environment as your app, because it imports the app to read its schema. Everything comes from `app.openapi()`, so what shows in your `/docs` shows in the collection.
|
|
46
|
+
|
|
47
|
+
## What's in the collection
|
|
48
|
+
|
|
49
|
+
| In your app | In Postman |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `include_router()` and mounted FastAPI apps | Every route with its full path. Routes with `include_in_schema=False` and mounted apps with `openapi_url=None` stay out |
|
|
52
|
+
| Tags | A folder per tag |
|
|
53
|
+
| A route with several methods | One request per method |
|
|
54
|
+
| Path parameters | Path variables such as `:item_id` with a sample value |
|
|
55
|
+
| Query and header parameters, including those inside `Depends()` | Filled in, with optional ones switched off |
|
|
56
|
+
| Pydantic body models | A sample JSON body that follows your `Field` limits, enums, formats, defaults and declared examples |
|
|
57
|
+
| `Form()` and `File()` | Form fields and file pickers |
|
|
58
|
+
| Docstrings | Request descriptions |
|
|
59
|
+
| Title, version and description | Collection info |
|
|
60
|
+
| `servers`, or `--base-url` | A `{{baseUrl}}` variable |
|
|
61
|
+
|
|
62
|
+
## Auth
|
|
63
|
+
|
|
64
|
+
Auth is found wherever you declared it: on the route, in a nested `Depends()`, in `Security()`, or in the `dependencies=` of a route, `APIRouter`, `include_router()` or `FastAPI`.
|
|
65
|
+
|
|
66
|
+
| In your app | In Postman |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `HTTPBearer` and subclasses, any `OAuth2…` scheme, OpenID Connect | Bearer auth with `{{bearerToken}}` |
|
|
69
|
+
| A dependency with an `authorization` header parameter | Bearer auth with `{{bearerToken}}` |
|
|
70
|
+
| `HTTPBasic` | Basic auth with `{{username}}` and `{{password}}` |
|
|
71
|
+
| `APIKeyHeader`, `APIKeyQuery` | The header or query parameter, set to a variable with the same name |
|
|
72
|
+
| A query parameter, header or form field whose name contains `token`, `api_key`, `secret` or `password` | The same, so `x_token` is sent as `{{x-token}}` |
|
|
73
|
+
|
|
74
|
+
Secret variables start blank, so the file holds no credentials. You fill them in inside Postman.
|
|
75
|
+
|
|
76
|
+
**The token saves itself.** A `POST` route that is your `tokenUrl`, or that returns `access_token` or `accessToken`, gets a script that stores the token in `{{bearerToken}}`. Those requests come first in the collection, so running the whole collection works.
|
|
77
|
+
|
|
78
|
+
**Auth the schema can't show.** Middleware, or a dependency that reads `request.headers`, doesn't appear in OpenAPI. Use `--bearer` to send `{{bearerToken}}` on every request that has no auth of its own.
|
|
79
|
+
|
|
80
|
+
## Pointing it at your app
|
|
81
|
+
|
|
82
|
+
Run it from your project root.
|
|
83
|
+
|
|
84
|
+
| `--app` | Loads |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `main.py` | `app` from that file |
|
|
87
|
+
| `main.py:api` | `api` from that file |
|
|
88
|
+
| `src/shop/main.py` | `app` from that file. Relative imports work if the folder has an `__init__.py` |
|
|
89
|
+
| `shop.main` | `app` from that module |
|
|
90
|
+
| `shop.main:api` | `api` from that module |
|
|
91
|
+
| `shop.main:create_app` | The app that this factory function returns |
|
|
92
|
+
|
|
93
|
+
## Options
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
--app APP the app: main.py, main.py:api or package.main:api. The
|
|
97
|
+
name defaults to app, and may be a factory
|
|
98
|
+
--output OUTPUT output file (default: output.json)
|
|
99
|
+
--name NAME collection name (default: the app's title)
|
|
100
|
+
--base-url BASE_URL where the requests go (default: the app's first server,
|
|
101
|
+
else http://localhost:8000)
|
|
102
|
+
--host HOST shorthand for --base-url http://HOST:PORT
|
|
103
|
+
--port PORT shorthand for --base-url http://HOST:PORT
|
|
104
|
+
--bearer send {{bearerToken}} on every request without auth of
|
|
105
|
+
its own: for auth in middleware, or in a dependency
|
|
106
|
+
reading request.headers, which the app's schema can't
|
|
107
|
+
show
|
|
108
|
+
--version show program's version number and exit
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## From Python
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from fastapi2postman import to_postman
|
|
115
|
+
|
|
116
|
+
collection = to_postman(app, name="Shop", base_url="https://api.example.com")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`to_postman` returns the collection as a dict.
|
|
120
|
+
|
|
121
|
+
## Limits
|
|
122
|
+
|
|
123
|
+
- **Samples are placeholders.** Any `pattern`, and validators that compare fields, can't be read from the schema. Declare `examples=` on the field where a sample gets rejected.
|
|
124
|
+
- **A request holds one auth.** If a route declares several, the first one Postman supports is used. API keys don't count, because they're sent as plain headers or query parameters.
|
|
125
|
+
- **Cookies are left to Postman's cookie jar**, which fills up when you log in. That covers `Cookie()` parameters and `APIKeyCookie`.
|
|
126
|
+
- **The token script reads the top level of a JSON response.** A login returning a bare `token` field or a nested token isn't detected, because a bare `token` could just as well be a reset or invite token.
|
|
127
|
+
- **Only FastAPI apps are followed into mounts.** Other mounted apps have no schema to read.
|
|
128
|
+
|
|
129
|
+
## Contributing
|
|
130
|
+
|
|
131
|
+
See [CONTRIBUTING.md](https://github.com/ashhadahsan/fastapi2postman/blob/main/CONTRIBUTING.md). Each change is checked by running a generated collection in Newman, Postman's command-line runner, against a live app.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT. Inspired by [flask2postman](https://github.com/numberly/flask2postman).
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# fastapi2postman
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fastapi2postman/)
|
|
4
|
+
[](https://pypi.org/project/fastapi2postman/)
|
|
5
|
+
[](https://github.com/ashhadahsan/fastapi2postman/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/ashhadahsan/fastapi2postman/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
**Turn a FastAPI app into a Postman collection you can run straight away.**
|
|
9
|
+
|
|
10
|
+
```console
|
|
11
|
+
$ pip install fastapi2postman
|
|
12
|
+
$ fastapi2postman --app main.py
|
|
13
|
+
output.json: 25 requests
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Import `output.json` into Postman, fill in the collection's variables, and send the login request. The token is saved for every other request.
|
|
17
|
+
|
|
18
|
+
Install it in the same environment as your app, because it imports the app to read its schema. Everything comes from `app.openapi()`, so what shows in your `/docs` shows in the collection.
|
|
19
|
+
|
|
20
|
+
## What's in the collection
|
|
21
|
+
|
|
22
|
+
| In your app | In Postman |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `include_router()` and mounted FastAPI apps | Every route with its full path. Routes with `include_in_schema=False` and mounted apps with `openapi_url=None` stay out |
|
|
25
|
+
| Tags | A folder per tag |
|
|
26
|
+
| A route with several methods | One request per method |
|
|
27
|
+
| Path parameters | Path variables such as `:item_id` with a sample value |
|
|
28
|
+
| Query and header parameters, including those inside `Depends()` | Filled in, with optional ones switched off |
|
|
29
|
+
| Pydantic body models | A sample JSON body that follows your `Field` limits, enums, formats, defaults and declared examples |
|
|
30
|
+
| `Form()` and `File()` | Form fields and file pickers |
|
|
31
|
+
| Docstrings | Request descriptions |
|
|
32
|
+
| Title, version and description | Collection info |
|
|
33
|
+
| `servers`, or `--base-url` | A `{{baseUrl}}` variable |
|
|
34
|
+
|
|
35
|
+
## Auth
|
|
36
|
+
|
|
37
|
+
Auth is found wherever you declared it: on the route, in a nested `Depends()`, in `Security()`, or in the `dependencies=` of a route, `APIRouter`, `include_router()` or `FastAPI`.
|
|
38
|
+
|
|
39
|
+
| In your app | In Postman |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `HTTPBearer` and subclasses, any `OAuth2…` scheme, OpenID Connect | Bearer auth with `{{bearerToken}}` |
|
|
42
|
+
| A dependency with an `authorization` header parameter | Bearer auth with `{{bearerToken}}` |
|
|
43
|
+
| `HTTPBasic` | Basic auth with `{{username}}` and `{{password}}` |
|
|
44
|
+
| `APIKeyHeader`, `APIKeyQuery` | The header or query parameter, set to a variable with the same name |
|
|
45
|
+
| A query parameter, header or form field whose name contains `token`, `api_key`, `secret` or `password` | The same, so `x_token` is sent as `{{x-token}}` |
|
|
46
|
+
|
|
47
|
+
Secret variables start blank, so the file holds no credentials. You fill them in inside Postman.
|
|
48
|
+
|
|
49
|
+
**The token saves itself.** A `POST` route that is your `tokenUrl`, or that returns `access_token` or `accessToken`, gets a script that stores the token in `{{bearerToken}}`. Those requests come first in the collection, so running the whole collection works.
|
|
50
|
+
|
|
51
|
+
**Auth the schema can't show.** Middleware, or a dependency that reads `request.headers`, doesn't appear in OpenAPI. Use `--bearer` to send `{{bearerToken}}` on every request that has no auth of its own.
|
|
52
|
+
|
|
53
|
+
## Pointing it at your app
|
|
54
|
+
|
|
55
|
+
Run it from your project root.
|
|
56
|
+
|
|
57
|
+
| `--app` | Loads |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `main.py` | `app` from that file |
|
|
60
|
+
| `main.py:api` | `api` from that file |
|
|
61
|
+
| `src/shop/main.py` | `app` from that file. Relative imports work if the folder has an `__init__.py` |
|
|
62
|
+
| `shop.main` | `app` from that module |
|
|
63
|
+
| `shop.main:api` | `api` from that module |
|
|
64
|
+
| `shop.main:create_app` | The app that this factory function returns |
|
|
65
|
+
|
|
66
|
+
## Options
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
--app APP the app: main.py, main.py:api or package.main:api. The
|
|
70
|
+
name defaults to app, and may be a factory
|
|
71
|
+
--output OUTPUT output file (default: output.json)
|
|
72
|
+
--name NAME collection name (default: the app's title)
|
|
73
|
+
--base-url BASE_URL where the requests go (default: the app's first server,
|
|
74
|
+
else http://localhost:8000)
|
|
75
|
+
--host HOST shorthand for --base-url http://HOST:PORT
|
|
76
|
+
--port PORT shorthand for --base-url http://HOST:PORT
|
|
77
|
+
--bearer send {{bearerToken}} on every request without auth of
|
|
78
|
+
its own: for auth in middleware, or in a dependency
|
|
79
|
+
reading request.headers, which the app's schema can't
|
|
80
|
+
show
|
|
81
|
+
--version show program's version number and exit
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## From Python
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from fastapi2postman import to_postman
|
|
88
|
+
|
|
89
|
+
collection = to_postman(app, name="Shop", base_url="https://api.example.com")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`to_postman` returns the collection as a dict.
|
|
93
|
+
|
|
94
|
+
## Limits
|
|
95
|
+
|
|
96
|
+
- **Samples are placeholders.** Any `pattern`, and validators that compare fields, can't be read from the schema. Declare `examples=` on the field where a sample gets rejected.
|
|
97
|
+
- **A request holds one auth.** If a route declares several, the first one Postman supports is used. API keys don't count, because they're sent as plain headers or query parameters.
|
|
98
|
+
- **Cookies are left to Postman's cookie jar**, which fills up when you log in. That covers `Cookie()` parameters and `APIKeyCookie`.
|
|
99
|
+
- **The token script reads the top level of a JSON response.** A login returning a bare `token` field or a nested token isn't detected, because a bare `token` could just as well be a reset or invite token.
|
|
100
|
+
- **Only FastAPI apps are followed into mounts.** Other mounted apps have no schema to read.
|
|
101
|
+
|
|
102
|
+
## Contributing
|
|
103
|
+
|
|
104
|
+
See [CONTRIBUTING.md](https://github.com/ashhadahsan/fastapi2postman/blob/main/CONTRIBUTING.md). Each change is checked by running a generated collection in Newman, Postman's command-line runner, against a live app.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT. Inspired by [flask2postman](https://github.com/numberly/flask2postman).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastapi2postman"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Turn a FastAPI app into a Postman collection you can run straight away."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = ["fastapi>=0.105"]
|
|
10
|
+
keywords = [
|
|
11
|
+
"fastapi",
|
|
12
|
+
"postman",
|
|
13
|
+
"newman",
|
|
14
|
+
"openapi",
|
|
15
|
+
"api",
|
|
16
|
+
"cli",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Framework :: FastAPI",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Programming Language :: Python :: 3.15",
|
|
29
|
+
"Topic :: Software Development :: Testing",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[[project.authors]]
|
|
34
|
+
name = "Ashhad Ahsan Rehman"
|
|
35
|
+
email = "ashhadahsan@gmail.com"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Source = "https://github.com/ashhadahsan/fastapi2postman"
|
|
39
|
+
Changelog = "https://github.com/ashhadahsan/fastapi2postman/blob/main/CHANGELOG.md"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
fastapi2postman = "fastapi2postman.cli:main"
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["uv_build>=0.13.6,<0.14"]
|
|
46
|
+
build-backend = "uv_build"
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 120
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
52
|
+
select = [
|
|
53
|
+
"E",
|
|
54
|
+
"F",
|
|
55
|
+
"I",
|
|
56
|
+
"UP",
|
|
57
|
+
"B",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
strict = true
|
|
62
|
+
files = ["src"]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
filterwarnings = ["ignore:Duplicate Operation ID:UserWarning"]
|
|
67
|
+
|
|
68
|
+
[tool.coverage.run]
|
|
69
|
+
source = ["fastapi2postman"]
|
|
70
|
+
|
|
71
|
+
[dependency-groups]
|
|
72
|
+
dev = [
|
|
73
|
+
"coverage>=7.17.2",
|
|
74
|
+
"jsonschema>=4.26.0",
|
|
75
|
+
"mypy>=2.4.1",
|
|
76
|
+
"pytest>=9.2.0",
|
|
77
|
+
"python-multipart>=0.0.33",
|
|
78
|
+
"ruff>=0.17.3",
|
|
79
|
+
"uvicorn>=0.54.0",
|
|
80
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastapi2postman"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Turn a FastAPI app into a Postman collection you can run straight away."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Ashhad Ahsan Rehman", email = "ashhadahsan@gmail.com" }]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = ["fastapi>=0.105"]
|
|
11
|
+
keywords = ["fastapi", "postman", "newman", "openapi", "api", "cli"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Framework :: FastAPI",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Programming Language :: Python :: 3.15",
|
|
23
|
+
"Topic :: Software Development :: Testing",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Source = "https://github.com/ashhadahsan/fastapi2postman"
|
|
29
|
+
Changelog = "https://github.com/ashhadahsan/fastapi2postman/blob/main/CHANGELOG.md"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
fastapi2postman = "fastapi2postman.cli:main"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["uv_build>=0.13.6,<0.14"]
|
|
36
|
+
build-backend = "uv_build"
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 120
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
43
|
+
|
|
44
|
+
[tool.mypy]
|
|
45
|
+
strict = true
|
|
46
|
+
files = ["src"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
# FastAPI warns about every route with several methods; the collection doesn't use operation ids.
|
|
51
|
+
filterwarnings = ["ignore:Duplicate Operation ID:UserWarning"]
|
|
52
|
+
|
|
53
|
+
[tool.coverage.run]
|
|
54
|
+
source = ["fastapi2postman"]
|
|
55
|
+
|
|
56
|
+
[dependency-groups]
|
|
57
|
+
dev = [
|
|
58
|
+
"coverage>=7.17.2",
|
|
59
|
+
"jsonschema>=4.26.0",
|
|
60
|
+
"mypy>=2.4.1",
|
|
61
|
+
"pytest>=9.2.0",
|
|
62
|
+
"python-multipart>=0.0.33",
|
|
63
|
+
"ruff>=0.17.3",
|
|
64
|
+
"uvicorn>=0.54.0",
|
|
65
|
+
]
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""Turn a FastAPI app into a Postman collection you can run straight away."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urljoin, urlsplit
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from starlette.routing import Mount
|
|
11
|
+
|
|
12
|
+
__all__ = ["to_postman"]
|
|
13
|
+
|
|
14
|
+
Json = dict[str, Any]
|
|
15
|
+
|
|
16
|
+
_METHODS = {"get", "put", "post", "delete", "options", "head", "patch", "trace"}
|
|
17
|
+
_FORM_MODES = {"application/x-www-form-urlencoded": "urlencoded", "multipart/form-data": "formdata"}
|
|
18
|
+
# Parameters with these names hold a blank variable: the real value must never end up in a collection people share.
|
|
19
|
+
_SECRET = re.compile(r"token|api[-_]?key|secret|password", re.IGNORECASE)
|
|
20
|
+
_BEARER = {"type": "bearer", "bearer": [{"key": "token", "value": "{{bearerToken}}", "type": "string"}]}
|
|
21
|
+
_BASIC = {
|
|
22
|
+
"type": "basic",
|
|
23
|
+
"basic": [
|
|
24
|
+
{"key": "username", "value": "{{username}}", "type": "string"},
|
|
25
|
+
{"key": "password", "value": "{{password}}", "type": "string"},
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
_FORMATS = {
|
|
29
|
+
"date": "2026-01-01",
|
|
30
|
+
"date-time": "2026-01-01T00:00:00Z",
|
|
31
|
+
"duration": "P1D",
|
|
32
|
+
"email": "user@example.com",
|
|
33
|
+
"ipv4": "127.0.0.1",
|
|
34
|
+
"ipv6": "::1",
|
|
35
|
+
"time": "00:00:00",
|
|
36
|
+
"uri": "https://example.com/",
|
|
37
|
+
"uuid": "00000000-0000-4000-8000-000000000000",
|
|
38
|
+
"uuid4": "00000000-0000-4000-8000-000000000000",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _compact(values: Json) -> Json:
|
|
43
|
+
return {key: value for key, value in values.items() if value}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _resolve(schema: Json, spec: Json) -> Json:
|
|
47
|
+
if "$ref" not in schema:
|
|
48
|
+
return schema
|
|
49
|
+
target = spec
|
|
50
|
+
for part in schema["$ref"].removeprefix("#/").split("/"):
|
|
51
|
+
target = target[part]
|
|
52
|
+
return target
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _sample(schema: Json, spec: Json, seen: tuple[str, ...] = ()) -> Any:
|
|
56
|
+
"""A value for this schema: what it declares, else a placeholder built from its type and limits."""
|
|
57
|
+
ref = schema.get("$ref")
|
|
58
|
+
if ref:
|
|
59
|
+
return None if ref in seen else _sample(_resolve(schema, spec), spec, (*seen, ref)) # a recursive model stops
|
|
60
|
+
if "example" in schema:
|
|
61
|
+
return schema["example"]
|
|
62
|
+
if schema.get("examples"):
|
|
63
|
+
return schema["examples"][0]
|
|
64
|
+
if schema.get("default") is not None:
|
|
65
|
+
return schema["default"]
|
|
66
|
+
if "const" in schema:
|
|
67
|
+
return schema["const"]
|
|
68
|
+
if schema.get("enum"):
|
|
69
|
+
return schema["enum"][0]
|
|
70
|
+
for key in ("anyOf", "oneOf"):
|
|
71
|
+
if key in schema:
|
|
72
|
+
options = [option for option in schema[key] if option.get("type") != "null"]
|
|
73
|
+
return _sample(options[0], spec, seen) if options else None
|
|
74
|
+
if "allOf" in schema:
|
|
75
|
+
parts = [_sample(part, spec, seen) for part in schema["allOf"]]
|
|
76
|
+
if all(isinstance(part, dict) for part in parts):
|
|
77
|
+
return {key: value for part in parts for key, value in part.items()}
|
|
78
|
+
return parts[0]
|
|
79
|
+
if "properties" in schema:
|
|
80
|
+
return {name: _sample(prop, spec, seen) for name, prop in schema["properties"].items()}
|
|
81
|
+
kind = schema.get("type")
|
|
82
|
+
if kind == "object":
|
|
83
|
+
extra = schema.get("additionalProperties")
|
|
84
|
+
return {"key": _sample(extra, spec, seen)} if isinstance(extra, dict) else {}
|
|
85
|
+
if kind == "array":
|
|
86
|
+
item = _sample(schema.get("items", {}), spec, seen)
|
|
87
|
+
return [] if item is None else [item] * schema.get("minItems", 1)
|
|
88
|
+
if kind == "string":
|
|
89
|
+
placeholder = "string".ljust(schema.get("minLength", 0), "x")[: schema.get("maxLength")]
|
|
90
|
+
return _FORMATS.get(schema.get("format", ""), placeholder)
|
|
91
|
+
if kind in ("integer", "number"):
|
|
92
|
+
for limit, step in (("minimum", 0), ("exclusiveMinimum", 1), ("maximum", 0), ("exclusiveMaximum", -1)):
|
|
93
|
+
if limit in schema:
|
|
94
|
+
return schema[limit] + step
|
|
95
|
+
return 1
|
|
96
|
+
return True if kind == "boolean" else None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _example(holder: Json, spec: Json) -> Any:
|
|
100
|
+
"""The example a parameter or media type declares, else a sample of its schema."""
|
|
101
|
+
if "example" in holder:
|
|
102
|
+
return holder["example"]
|
|
103
|
+
for example in holder.get("examples", {}).values():
|
|
104
|
+
if "value" in example:
|
|
105
|
+
return example["value"]
|
|
106
|
+
return _sample(holder.get("schema", {}), spec)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _text(value: Any) -> str:
|
|
110
|
+
"""How a sample looks in a URL, a header or a form."""
|
|
111
|
+
if isinstance(value, list):
|
|
112
|
+
value = value[0] if value else ""
|
|
113
|
+
if value is None:
|
|
114
|
+
return ""
|
|
115
|
+
return value if isinstance(value, str) else json.dumps(value)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _variable(name: str) -> str:
|
|
119
|
+
return "{{" + name + "}}"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _field(name: str, value: Any, description: str | None = None, required: bool = True) -> Json:
|
|
123
|
+
"""A query parameter, header, path variable or form field. Optional ones start switched off."""
|
|
124
|
+
field: Json = {"key": name, "value": _text(value)}
|
|
125
|
+
if description:
|
|
126
|
+
field["description"] = description
|
|
127
|
+
if not required:
|
|
128
|
+
field["disabled"] = True
|
|
129
|
+
return field
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _is_upload(schema: Json) -> bool:
|
|
133
|
+
if schema.get("format") == "binary" or schema.get("contentMediaType") == "application/octet-stream":
|
|
134
|
+
return True
|
|
135
|
+
return any(_is_upload(option) for option in (*schema.get("anyOf", ()), schema.get("items")) if option)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _form_fields(holder: Json, spec: Json, multipart: bool) -> list[Json]:
|
|
139
|
+
schema = _resolve(holder.get("schema", {}), spec)
|
|
140
|
+
fields = []
|
|
141
|
+
for name, prop in schema.get("properties", {}).items():
|
|
142
|
+
value = _variable(name) if _SECRET.search(name) else _sample(prop, spec)
|
|
143
|
+
field = _field(name, value, prop.get("description"), name in schema.get("required", []))
|
|
144
|
+
if multipart and _is_upload(prop):
|
|
145
|
+
del field["value"]
|
|
146
|
+
field.update(type="file", src=[])
|
|
147
|
+
elif multipart:
|
|
148
|
+
field["type"] = "text"
|
|
149
|
+
fields.append(field)
|
|
150
|
+
return fields
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _body(media: str, holder: Json, spec: Json) -> Json:
|
|
154
|
+
mode = _FORM_MODES.get(media)
|
|
155
|
+
if mode:
|
|
156
|
+
return {"mode": mode, mode: _form_fields(holder, spec, multipart=mode == "formdata")}
|
|
157
|
+
sample = _example(holder, spec)
|
|
158
|
+
if media.endswith("json"):
|
|
159
|
+
raw, language = json.dumps(sample, indent=2, ensure_ascii=False), "json"
|
|
160
|
+
else:
|
|
161
|
+
raw, language = _text(sample), "text"
|
|
162
|
+
return {"mode": "raw", "raw": raw, "options": {"raw": {"language": language}}}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _auth_for(scheme: Json) -> Json | None:
|
|
166
|
+
http = scheme.get("scheme", "").lower()
|
|
167
|
+
# OAuth2 and OpenID Connect end in a bearer token too, and logging in fills it (see _token_field).
|
|
168
|
+
if scheme.get("type") in ("oauth2", "openIdConnect") or http == "bearer":
|
|
169
|
+
return _BEARER
|
|
170
|
+
return _BASIC if http == "basic" else None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _request(path: str, method: str, operation: Json, spec: Json) -> Json:
|
|
174
|
+
fields: dict[str, list[Json]] = {"path": [], "query": [], "header": []}
|
|
175
|
+
auth = None
|
|
176
|
+
for param in operation.get("parameters", []):
|
|
177
|
+
where, name = param["in"], param["name"]
|
|
178
|
+
if where == "header" and name.lower() == "authorization":
|
|
179
|
+
auth = _BEARER # a dependency reading the header itself
|
|
180
|
+
elif where in fields: # cookies are left to Postman's cookie jar, which logging in fills
|
|
181
|
+
secret = where != "path" and _SECRET.search(name)
|
|
182
|
+
value = _variable(name) if secret else _example(param, spec)
|
|
183
|
+
fields[where].append(_field(name, value, param.get("description"), param.get("required", False)))
|
|
184
|
+
|
|
185
|
+
schemes = spec.get("components", {}).get("securitySchemes", {})
|
|
186
|
+
# An operation's own list wins, and an empty one means public even when the whole API declares security.
|
|
187
|
+
security = operation["security"] if "security" in operation else spec.get("security", [])
|
|
188
|
+
for requirement in security:
|
|
189
|
+
for scheme_name in requirement:
|
|
190
|
+
scheme = schemes.get(scheme_name, {})
|
|
191
|
+
if scheme.get("type") == "apiKey" and scheme["in"] in fields:
|
|
192
|
+
# A plain field, not Postman's API key auth: a request holds one auth, and a key can come with a token.
|
|
193
|
+
fields[scheme["in"]].append(_field(scheme["name"], _variable(scheme["name"])))
|
|
194
|
+
else:
|
|
195
|
+
auth = auth or _auth_for(scheme)
|
|
196
|
+
|
|
197
|
+
body = None
|
|
198
|
+
content = operation.get("requestBody", {}).get("content", {})
|
|
199
|
+
if content:
|
|
200
|
+
media, holder = next(iter(content.items()))
|
|
201
|
+
body = _body(media, holder, spec)
|
|
202
|
+
if body["mode"] == "raw": # Postman sets the Content-Type of a form itself
|
|
203
|
+
fields["header"].insert(0, {"key": "Content-Type", "value": media})
|
|
204
|
+
|
|
205
|
+
route = path.replace("{", ":").replace("}", "")
|
|
206
|
+
enabled = [f"{field['key']}={field['value']}" for field in fields["query"] if "disabled" not in field]
|
|
207
|
+
url = {
|
|
208
|
+
"raw": _variable("baseUrl") + route + ("?" + "&".join(enabled) if enabled else ""),
|
|
209
|
+
"host": [_variable("baseUrl")],
|
|
210
|
+
"path": route.split("/")[1:],
|
|
211
|
+
"query": fields["query"],
|
|
212
|
+
"variable": fields["path"],
|
|
213
|
+
}
|
|
214
|
+
return _compact(
|
|
215
|
+
{
|
|
216
|
+
"method": method.upper(),
|
|
217
|
+
"header": fields["header"],
|
|
218
|
+
"url": _compact(url),
|
|
219
|
+
"description": operation.get("description"),
|
|
220
|
+
"auth": auth,
|
|
221
|
+
"body": body,
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _token_field(path: str, method: str, operation: Json, spec: Json) -> str | None:
|
|
227
|
+
"""The response field holding a bearer token, when this route is the one that logs you in."""
|
|
228
|
+
if method != "post":
|
|
229
|
+
return None # a GET reads a token, it doesn't start a session
|
|
230
|
+
for status, response in operation.get("responses", {}).items():
|
|
231
|
+
schema = response.get("content", {}).get("application/json", {}).get("schema", {})
|
|
232
|
+
properties = _resolve(schema, spec).get("properties", {})
|
|
233
|
+
# ponytail: OAuth2's own field names only. A bare `token` could be a reset or invite token; add it if asked.
|
|
234
|
+
for field in ("access_token", "accessToken"):
|
|
235
|
+
if status.startswith("2") and field in properties:
|
|
236
|
+
return field
|
|
237
|
+
route = "/" + path.strip("/")
|
|
238
|
+
for scheme in spec.get("components", {}).get("securitySchemes", {}).values():
|
|
239
|
+
for flow in scheme.get("flows", {}).values():
|
|
240
|
+
token_path = urlsplit(flow.get("tokenUrl", "")).path.strip("/")
|
|
241
|
+
if token_path and route.endswith("/" + token_path):
|
|
242
|
+
return "access_token"
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _item(path: str, method: str, operation: Json, spec: Json) -> Json:
|
|
247
|
+
item = {
|
|
248
|
+
"name": operation.get("summary") or f"{method.upper()} {path}",
|
|
249
|
+
"request": _request(path, method, operation, spec),
|
|
250
|
+
}
|
|
251
|
+
token_field = _token_field(path, method, operation, spec)
|
|
252
|
+
if token_field:
|
|
253
|
+
script = [
|
|
254
|
+
"// Keeps the token from this response for the requests that need one.",
|
|
255
|
+
"if (pm.response.code < 300) {",
|
|
256
|
+
f" const token = pm.response.json().{token_field};",
|
|
257
|
+
" if (token) pm.collectionVariables.set('bearerToken', token);",
|
|
258
|
+
"}",
|
|
259
|
+
]
|
|
260
|
+
item["event"] = [{"listen": "test", "script": {"type": "text/javascript", "exec": script}}]
|
|
261
|
+
return item
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _specs(app: FastAPI, prefix: str = "") -> Iterator[tuple[str, Json]]:
|
|
265
|
+
"""The OpenAPI schema of the app and of each FastAPI app mounted in it, with the path each one is under."""
|
|
266
|
+
yield prefix, app.openapi()
|
|
267
|
+
for route in app.routes:
|
|
268
|
+
if isinstance(route, Mount) and isinstance(route.app, FastAPI) and route.app.openapi_url:
|
|
269
|
+
yield from _specs(route.app, prefix + route.path)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def to_postman(app: FastAPI, *, name: str | None = None, base_url: str | None = None, bearer: bool = False) -> Json:
|
|
273
|
+
"""Build a Postman collection (format v2.1) from a FastAPI app.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
app: The app. Everything comes from `app.openapi()`, so whatever shows in its docs shows here, including
|
|
277
|
+
parameters and auth declared in `Depends()`, at any depth.
|
|
278
|
+
name: The collection name. Defaults to the app's title.
|
|
279
|
+
base_url: Where the requests go. Defaults to the app's first server, else http://localhost:8000.
|
|
280
|
+
bearer: Send `{{bearerToken}}` on every request that declares no auth of its own. For auth OpenAPI can't see:
|
|
281
|
+
middleware, or a dependency reading `request.headers`.
|
|
282
|
+
"""
|
|
283
|
+
specs = list(_specs(app))
|
|
284
|
+
main_spec = specs[0][1]
|
|
285
|
+
servers = main_spec.get("servers", [])
|
|
286
|
+
server = servers[0]["url"] if servers else ""
|
|
287
|
+
|
|
288
|
+
requests = []
|
|
289
|
+
for prefix, spec in specs:
|
|
290
|
+
for path, path_item in spec.get("paths", {}).items():
|
|
291
|
+
for method, operation in path_item.items():
|
|
292
|
+
if method in _METHODS:
|
|
293
|
+
tags = operation.get("tags") or [None]
|
|
294
|
+
requests.append((tags[0], _item(prefix + path, method, operation, spec)))
|
|
295
|
+
requests.sort(key=lambda request: "event" not in request[1]) # logging in comes first, so a full run has the token
|
|
296
|
+
|
|
297
|
+
items: list[Json] = []
|
|
298
|
+
folders: dict[str, Json] = {}
|
|
299
|
+
for tag, item in requests:
|
|
300
|
+
if tag is None:
|
|
301
|
+
items.append(item)
|
|
302
|
+
continue
|
|
303
|
+
if tag not in folders:
|
|
304
|
+
folders[tag] = {"name": tag, "item": []}
|
|
305
|
+
items.append(folders[tag])
|
|
306
|
+
folders[tag]["item"].append(item)
|
|
307
|
+
|
|
308
|
+
info = main_spec["info"]
|
|
309
|
+
collection: Json = {
|
|
310
|
+
"info": _compact(
|
|
311
|
+
{
|
|
312
|
+
"name": name or info["title"],
|
|
313
|
+
"description": info.get("description"),
|
|
314
|
+
"version": info.get("version"),
|
|
315
|
+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
|
316
|
+
}
|
|
317
|
+
),
|
|
318
|
+
"item": items,
|
|
319
|
+
}
|
|
320
|
+
if bearer:
|
|
321
|
+
collection["auth"] = _BEARER
|
|
322
|
+
secrets = re.findall(r"\{\{([\w-]+)\}\}", json.dumps(collection))
|
|
323
|
+
variables = {"baseUrl": (base_url or urljoin("http://localhost:8000", server)).rstrip("/")} | {
|
|
324
|
+
secret: "" for secret in secrets if secret != "baseUrl"
|
|
325
|
+
}
|
|
326
|
+
collection["variable"] = [{"key": key, "value": value} for key, value in variables.items()]
|
|
327
|
+
return collection
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""The fastapi2postman command."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from importlib import import_module, metadata
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
|
|
12
|
+
from . import to_postman
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _module_of(path: Path) -> tuple[str, Path]:
|
|
16
|
+
"""The dotted name of a file and the folder to import it from, which is above its package if it's in one."""
|
|
17
|
+
parts = [path.stem]
|
|
18
|
+
root = path.parent
|
|
19
|
+
while (root / "__init__.py").is_file():
|
|
20
|
+
parts.insert(0, root.name)
|
|
21
|
+
root = root.parent
|
|
22
|
+
return ".".join(parts), root
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_app(spec: str) -> FastAPI:
|
|
26
|
+
target, attribute = spec, "app"
|
|
27
|
+
if ":" in spec and not Path(spec).is_file(): # a colon can be part of a path: C:\project\app.py
|
|
28
|
+
target, attribute = spec.rsplit(":", 1)
|
|
29
|
+
|
|
30
|
+
name = target
|
|
31
|
+
search = [os.getcwd()] # a console script has its own folder on sys.path, not the project's
|
|
32
|
+
if Path(target).is_file():
|
|
33
|
+
name, root = _module_of(Path(target).resolve())
|
|
34
|
+
search.insert(0, str(root))
|
|
35
|
+
sys.path[:0] = search
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
module = import_module(name)
|
|
39
|
+
except ModuleNotFoundError as error:
|
|
40
|
+
if error.name != name and not name.startswith(f"{error.name}."):
|
|
41
|
+
raise # something the app itself imports is missing, and the traceback says what
|
|
42
|
+
raise SystemExit(
|
|
43
|
+
f"fastapi2postman: cannot find {target!r}: give a .py file, or a module importable from {os.getcwd()}"
|
|
44
|
+
) from error
|
|
45
|
+
if not hasattr(module, attribute):
|
|
46
|
+
raise SystemExit(f"fastapi2postman: {name} has no attribute {attribute!r}")
|
|
47
|
+
|
|
48
|
+
app = getattr(module, attribute)
|
|
49
|
+
if not isinstance(app, FastAPI) and callable(app):
|
|
50
|
+
app = app() # a factory
|
|
51
|
+
if not isinstance(app, FastAPI):
|
|
52
|
+
raise SystemExit(f"fastapi2postman: {spec} gives {type(app).__name__}, not a FastAPI app")
|
|
53
|
+
return app
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> None:
|
|
57
|
+
parser = argparse.ArgumentParser(
|
|
58
|
+
prog="fastapi2postman", description="Turn a FastAPI app into a Postman collection you can run straight away."
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--app",
|
|
62
|
+
required=True,
|
|
63
|
+
help="the app: main.py, main.py:api or package.main:api. The name defaults to app, and may be a factory",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument("--output", default="output.json", help="output file (default: %(default)s)")
|
|
66
|
+
parser.add_argument("--name", help="collection name (default: the app's title)")
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--base-url", help="where the requests go (default: the app's first server, else http://localhost:8000)"
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument("--host", help="shorthand for --base-url http://HOST:PORT")
|
|
71
|
+
parser.add_argument("--port", type=int, help="shorthand for --base-url http://HOST:PORT")
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--bearer",
|
|
74
|
+
action="store_true",
|
|
75
|
+
help="send {{bearerToken}} on every request without auth of its own: for auth in middleware, or in a "
|
|
76
|
+
"dependency reading request.headers, which the app's schema can't show",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument("--version", action="version", version=metadata.version("fastapi2postman"))
|
|
79
|
+
args = parser.parse_args()
|
|
80
|
+
|
|
81
|
+
base_url = args.base_url
|
|
82
|
+
if not base_url and (args.host or args.port):
|
|
83
|
+
base_url = f"http://{args.host or 'localhost'}:{args.port or 8000}"
|
|
84
|
+
|
|
85
|
+
collection = to_postman(_load_app(args.app), name=args.name, base_url=base_url, bearer=args.bearer)
|
|
86
|
+
text = json.dumps(collection, indent=2, ensure_ascii=False)
|
|
87
|
+
Path(args.output).write_text(text + "\n", encoding="utf-8")
|
|
88
|
+
count = sum(len(entry.get("item", [entry])) for entry in collection["item"])
|
|
89
|
+
print(f"{args.output}: {count} requests")
|
|
File without changes
|
fastapi2postman-0.1.2/PKG-INFO
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: fastapi2postman
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: FastAPI APIs to Postman collections
|
|
5
|
-
Home-page: https://github.com/ashhadahsan/fastapi2postman
|
|
6
|
-
Author: Ashhad Ahsan Rehman
|
|
7
|
-
Author-email: ashhadahsan@gmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Platform: OS Independent
|
|
10
|
-
Classifier: Development Status :: 3 - Alpha
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
-
Classifier: Environment :: Console
|
|
14
|
-
Classifier: Environment :: Web Environment
|
|
15
|
-
Classifier: Framework :: FastAPI
|
|
16
|
-
Classifier: Intended Audience :: Developers
|
|
17
|
-
Classifier: Intended Audience :: End Users/Desktop
|
|
18
|
-
Classifier: Intended Audience :: System Administrators
|
|
19
|
-
Classifier: Operating System :: OS Independent
|
|
20
|
-
Classifier: Programming Language :: Python :: 3
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
25
|
-
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
|
-
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
|
|
27
|
-
Classifier: Topic :: Software Development :: Testing
|
|
28
|
-
Classifier: Topic :: Utilities
|
|
29
|
-
License-File: LICENSE
|
|
30
|
-
|
|
31
|
-
.. _Postman: https://www.postman.com/
|
|
32
|
-
.. _FastAPI: https://fastapi.tiangolo.com/
|
|
33
|
-
.. _flask2postman: https://github.com/numberly/flask2postman/
|
|
34
|
-
|
|
35
|
-
================
|
|
36
|
-
fastapi2postman
|
|
37
|
-
================
|
|
38
|
-
|
|
39
|
-
.. image:: https://img.shields.io/pypi/v/flask2postman.svg
|
|
40
|
-
:target: https://pypi.org/project/fastapi2postman
|
|
41
|
-
.. image:: https://img.shields.io/github/license/numberly/flask2postman.svg
|
|
42
|
-
:target: https://github.com/ashhadahsan/fastapi2postman/blob/main/LICENSE
|
|
43
|
-
|
|
44
|
-
|
|
|
45
|
-
|
|
46
|
-
A tool that creates a Postman_ collection from a FastAPI_ application.
|
|
47
|
-
|
|
48
|
-
Inspired from flask2postman_
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Install
|
|
52
|
-
=======
|
|
53
|
-
|
|
54
|
-
.. code-block:: sh
|
|
55
|
-
|
|
56
|
-
$ pip install fastapi2postman
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Example
|
|
60
|
-
=======
|
|
61
|
-
|
|
62
|
-
Let's say that you have a FastAPI project called "app.py" and you want to generate a Postman collection out of it.
|
|
63
|
-
|
|
64
|
-
You just need to tell :code:`fastapi2postman`
|
|
65
|
-
|
|
66
|
-
.. code-block:: sh
|
|
67
|
-
|
|
68
|
-
$ fastapi2postman --app app.py
|
|
69
|
-
|
|
70
|
-
If you want to change the name of the output file use --output flag, the default name is ofcousre output.json
|
|
71
|
-
|
|
72
|
-
.. code-block:: sh
|
|
73
|
-
|
|
74
|
-
$ fastapi2postman --app app.py --output output.json
|
|
75
|
-
|
|
76
|
-
This will generate the JSON configuration, and write it into the
|
|
77
|
-
:code:`output.json` file. You can then import this file into Postman ("Import
|
|
78
|
-
Collection" button), and profit:
|
|
79
|
-
|
|
80
|
-
On a side note, you can see that endpoint's docstrings are automatically
|
|
81
|
-
imported as descriptions.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Usage
|
|
85
|
-
=====
|
|
86
|
-
|
|
87
|
-
.. code-block:: sh
|
|
88
|
-
|
|
89
|
-
usage: fastapi2postman [-h] [--output OUTPUT] [--name NAME] [--host HOST]
|
|
90
|
-
[--port PORT] --app APP
|
|
91
|
-
|
|
92
|
-
optional arguments:
|
|
93
|
-
-h, --help show this help message and exit
|
|
94
|
-
--output OUTPUT Output file
|
|
95
|
-
--name NAME Collection name
|
|
96
|
-
--host HOST API host
|
|
97
|
-
--port PORT API port
|
|
98
|
-
--app APP Path to FastAPI application instance
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
License
|
|
102
|
-
=======
|
|
103
|
-
|
|
104
|
-
MIT
|
|
105
|
-
|
fastapi2postman-0.1.2/README.rst
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
.. _Postman: https://www.postman.com/
|
|
2
|
-
.. _FastAPI: https://fastapi.tiangolo.com/
|
|
3
|
-
.. _flask2postman: https://github.com/numberly/flask2postman/
|
|
4
|
-
|
|
5
|
-
================
|
|
6
|
-
fastapi2postman
|
|
7
|
-
================
|
|
8
|
-
|
|
9
|
-
.. image:: https://img.shields.io/pypi/v/flask2postman.svg
|
|
10
|
-
:target: https://pypi.org/project/fastapi2postman
|
|
11
|
-
.. image:: https://img.shields.io/github/license/numberly/flask2postman.svg
|
|
12
|
-
:target: https://github.com/ashhadahsan/fastapi2postman/blob/main/LICENSE
|
|
13
|
-
|
|
14
|
-
|
|
|
15
|
-
|
|
16
|
-
A tool that creates a Postman_ collection from a FastAPI_ application.
|
|
17
|
-
|
|
18
|
-
Inspired from flask2postman_
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Install
|
|
22
|
-
=======
|
|
23
|
-
|
|
24
|
-
.. code-block:: sh
|
|
25
|
-
|
|
26
|
-
$ pip install fastapi2postman
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Example
|
|
30
|
-
=======
|
|
31
|
-
|
|
32
|
-
Let's say that you have a FastAPI project called "app.py" and you want to generate a Postman collection out of it.
|
|
33
|
-
|
|
34
|
-
You just need to tell :code:`fastapi2postman`
|
|
35
|
-
|
|
36
|
-
.. code-block:: sh
|
|
37
|
-
|
|
38
|
-
$ fastapi2postman --app app.py
|
|
39
|
-
|
|
40
|
-
If you want to change the name of the output file use --output flag, the default name is ofcousre output.json
|
|
41
|
-
|
|
42
|
-
.. code-block:: sh
|
|
43
|
-
|
|
44
|
-
$ fastapi2postman --app app.py --output output.json
|
|
45
|
-
|
|
46
|
-
This will generate the JSON configuration, and write it into the
|
|
47
|
-
:code:`output.json` file. You can then import this file into Postman ("Import
|
|
48
|
-
Collection" button), and profit:
|
|
49
|
-
|
|
50
|
-
On a side note, you can see that endpoint's docstrings are automatically
|
|
51
|
-
imported as descriptions.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
Usage
|
|
55
|
-
=====
|
|
56
|
-
|
|
57
|
-
.. code-block:: sh
|
|
58
|
-
|
|
59
|
-
usage: fastapi2postman [-h] [--output OUTPUT] [--name NAME] [--host HOST]
|
|
60
|
-
[--port PORT] --app APP
|
|
61
|
-
|
|
62
|
-
optional arguments:
|
|
63
|
-
-h, --help show this help message and exit
|
|
64
|
-
--output OUTPUT Output file
|
|
65
|
-
--name NAME Collection name
|
|
66
|
-
--host HOST API host
|
|
67
|
-
--port PORT API port
|
|
68
|
-
--app APP Path to FastAPI application instance
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
License
|
|
72
|
-
=======
|
|
73
|
-
|
|
74
|
-
MIT
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: fastapi2postman
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: FastAPI APIs to Postman collections
|
|
5
|
-
Home-page: https://github.com/ashhadahsan/fastapi2postman
|
|
6
|
-
Author: Ashhad Ahsan Rehman
|
|
7
|
-
Author-email: ashhadahsan@gmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Platform: OS Independent
|
|
10
|
-
Classifier: Development Status :: 3 - Alpha
|
|
11
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
-
Classifier: Environment :: Console
|
|
14
|
-
Classifier: Environment :: Web Environment
|
|
15
|
-
Classifier: Framework :: FastAPI
|
|
16
|
-
Classifier: Intended Audience :: Developers
|
|
17
|
-
Classifier: Intended Audience :: End Users/Desktop
|
|
18
|
-
Classifier: Intended Audience :: System Administrators
|
|
19
|
-
Classifier: Operating System :: OS Independent
|
|
20
|
-
Classifier: Programming Language :: Python :: 3
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
25
|
-
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
26
|
-
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
|
|
27
|
-
Classifier: Topic :: Software Development :: Testing
|
|
28
|
-
Classifier: Topic :: Utilities
|
|
29
|
-
License-File: LICENSE
|
|
30
|
-
|
|
31
|
-
.. _Postman: https://www.postman.com/
|
|
32
|
-
.. _FastAPI: https://fastapi.tiangolo.com/
|
|
33
|
-
.. _flask2postman: https://github.com/numberly/flask2postman/
|
|
34
|
-
|
|
35
|
-
================
|
|
36
|
-
fastapi2postman
|
|
37
|
-
================
|
|
38
|
-
|
|
39
|
-
.. image:: https://img.shields.io/pypi/v/flask2postman.svg
|
|
40
|
-
:target: https://pypi.org/project/fastapi2postman
|
|
41
|
-
.. image:: https://img.shields.io/github/license/numberly/flask2postman.svg
|
|
42
|
-
:target: https://github.com/ashhadahsan/fastapi2postman/blob/main/LICENSE
|
|
43
|
-
|
|
44
|
-
|
|
|
45
|
-
|
|
46
|
-
A tool that creates a Postman_ collection from a FastAPI_ application.
|
|
47
|
-
|
|
48
|
-
Inspired from flask2postman_
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Install
|
|
52
|
-
=======
|
|
53
|
-
|
|
54
|
-
.. code-block:: sh
|
|
55
|
-
|
|
56
|
-
$ pip install fastapi2postman
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
Example
|
|
60
|
-
=======
|
|
61
|
-
|
|
62
|
-
Let's say that you have a FastAPI project called "app.py" and you want to generate a Postman collection out of it.
|
|
63
|
-
|
|
64
|
-
You just need to tell :code:`fastapi2postman`
|
|
65
|
-
|
|
66
|
-
.. code-block:: sh
|
|
67
|
-
|
|
68
|
-
$ fastapi2postman --app app.py
|
|
69
|
-
|
|
70
|
-
If you want to change the name of the output file use --output flag, the default name is ofcousre output.json
|
|
71
|
-
|
|
72
|
-
.. code-block:: sh
|
|
73
|
-
|
|
74
|
-
$ fastapi2postman --app app.py --output output.json
|
|
75
|
-
|
|
76
|
-
This will generate the JSON configuration, and write it into the
|
|
77
|
-
:code:`output.json` file. You can then import this file into Postman ("Import
|
|
78
|
-
Collection" button), and profit:
|
|
79
|
-
|
|
80
|
-
On a side note, you can see that endpoint's docstrings are automatically
|
|
81
|
-
imported as descriptions.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Usage
|
|
85
|
-
=====
|
|
86
|
-
|
|
87
|
-
.. code-block:: sh
|
|
88
|
-
|
|
89
|
-
usage: fastapi2postman [-h] [--output OUTPUT] [--name NAME] [--host HOST]
|
|
90
|
-
[--port PORT] --app APP
|
|
91
|
-
|
|
92
|
-
optional arguments:
|
|
93
|
-
-h, --help show this help message and exit
|
|
94
|
-
--output OUTPUT Output file
|
|
95
|
-
--name NAME Collection name
|
|
96
|
-
--host HOST API host
|
|
97
|
-
--port PORT API port
|
|
98
|
-
--app APP Path to FastAPI application instance
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
License
|
|
102
|
-
=======
|
|
103
|
-
|
|
104
|
-
MIT
|
|
105
|
-
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.rst
|
|
3
|
-
fastapi2postman.py
|
|
4
|
-
setup.py
|
|
5
|
-
fastapi2postman.egg-info/PKG-INFO
|
|
6
|
-
fastapi2postman.egg-info/SOURCES.txt
|
|
7
|
-
fastapi2postman.egg-info/dependency_links.txt
|
|
8
|
-
fastapi2postman.egg-info/entry_points.txt
|
|
9
|
-
fastapi2postman.egg-info/requires.txt
|
|
10
|
-
fastapi2postman.egg-info/top_level.txt
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
fastapi2postman
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
from fastapi.routing import APIRoute
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def register_routes(app):
|
|
9
|
-
routes = []
|
|
10
|
-
for route in app.routes:
|
|
11
|
-
if isinstance(route, APIRoute):
|
|
12
|
-
routes.append(
|
|
13
|
-
{
|
|
14
|
-
"name": route.name,
|
|
15
|
-
"method": ",".join(route.methods),
|
|
16
|
-
"url": str(route.path),
|
|
17
|
-
"path": str(route.path).replace("{", ":").replace("}", ""),
|
|
18
|
-
}
|
|
19
|
-
)
|
|
20
|
-
return routes
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def main():
|
|
24
|
-
parser = argparse.ArgumentParser()
|
|
25
|
-
parser.add_argument("--output", type=str, help="Output file", default="output.json")
|
|
26
|
-
parser.add_argument(
|
|
27
|
-
"--name", type=str, help="Collection name", default="FastAPI API"
|
|
28
|
-
)
|
|
29
|
-
parser.add_argument("--host", type=str, help="API host", default="localhost")
|
|
30
|
-
parser.add_argument("--port", type=int, help="API port", default=8000)
|
|
31
|
-
parser.add_argument(
|
|
32
|
-
"--app",
|
|
33
|
-
type=str,
|
|
34
|
-
help="Path to FastAPI application instance",
|
|
35
|
-
default="app.py",
|
|
36
|
-
required=True,
|
|
37
|
-
)
|
|
38
|
-
args = parser.parse_args()
|
|
39
|
-
|
|
40
|
-
app_path = args.app
|
|
41
|
-
app_dir, app_file = os.path.split(app_path)
|
|
42
|
-
sys.path.append(app_dir)
|
|
43
|
-
app_name, _ = os.path.splitext(app_file)
|
|
44
|
-
app_module = __import__(app_name)
|
|
45
|
-
app = getattr(app_module, "app")
|
|
46
|
-
|
|
47
|
-
routes = register_routes(app)
|
|
48
|
-
|
|
49
|
-
collection = {
|
|
50
|
-
"info": {
|
|
51
|
-
"name": args.name,
|
|
52
|
-
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
|
53
|
-
},
|
|
54
|
-
"item": [],
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
for route in routes:
|
|
58
|
-
item = {
|
|
59
|
-
"name": route["name"],
|
|
60
|
-
"request": {
|
|
61
|
-
"url": f'http://{args.host}:{args.port}{route["url"]}',
|
|
62
|
-
"method": route["method"],
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
collection["item"].append(item)
|
|
66
|
-
|
|
67
|
-
with open(args.output, "w") as f:
|
|
68
|
-
json.dump(collection, f, indent=4)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if __name__ == "__main__":
|
|
72
|
-
main()
|
fastapi2postman-0.1.2/setup.cfg
DELETED
fastapi2postman-0.1.2/setup.py
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
from setuptools import setup, find_packages
|
|
2
|
-
|
|
3
|
-
with open("README.rst") as file:
|
|
4
|
-
long_description = file.read()
|
|
5
|
-
setup(
|
|
6
|
-
name="fastapi2postman",
|
|
7
|
-
version="0.1.2",
|
|
8
|
-
description="FastAPI APIs to Postman collections",
|
|
9
|
-
long_description=long_description,
|
|
10
|
-
author="Ashhad Ahsan Rehman",
|
|
11
|
-
license="MIT",
|
|
12
|
-
author_email="ashhadahsan@gmail.com",
|
|
13
|
-
url="https://github.com/ashhadahsan/fastapi2postman",
|
|
14
|
-
packages=find_packages(),
|
|
15
|
-
platforms=["OS Independent"],
|
|
16
|
-
install_requires=[
|
|
17
|
-
"click>=8.0.0",
|
|
18
|
-
"requests>=2.26.0",
|
|
19
|
-
"fastapi>=0.68.1",
|
|
20
|
-
"uvicorn>=0.15.0",
|
|
21
|
-
],
|
|
22
|
-
classifiers=[
|
|
23
|
-
"Development Status :: 3 - Alpha",
|
|
24
|
-
"License :: OSI Approved :: MIT License",
|
|
25
|
-
"Development Status :: 5 - Production/Stable",
|
|
26
|
-
"Environment :: Console",
|
|
27
|
-
"Environment :: Web Environment",
|
|
28
|
-
"Framework :: FastAPI",
|
|
29
|
-
"Intended Audience :: Developers",
|
|
30
|
-
"Intended Audience :: End Users/Desktop",
|
|
31
|
-
"Intended Audience :: System Administrators",
|
|
32
|
-
"Operating System :: OS Independent",
|
|
33
|
-
"Programming Language :: Python :: 3",
|
|
34
|
-
"Programming Language :: Python :: 3.6",
|
|
35
|
-
"Programming Language :: Python :: 3.7",
|
|
36
|
-
"Programming Language :: Python :: 3.8",
|
|
37
|
-
"Programming Language :: Python :: 3.9",
|
|
38
|
-
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
39
|
-
"Topic :: Internet :: WWW/HTTP :: Site Management",
|
|
40
|
-
"Topic :: Software Development :: Testing",
|
|
41
|
-
"Topic :: Utilities",
|
|
42
|
-
],
|
|
43
|
-
py_modules=["fastapi2postman"],
|
|
44
|
-
entry_points={"console_scripts": ["fastapi2postman = fastapi2postman:main"]},
|
|
45
|
-
)
|