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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/fastapi2postman?cacheSeconds=3600)](https://pypi.org/project/fastapi2postman/)
31
+ [![Python](https://img.shields.io/pypi/pyversions/fastapi2postman?cacheSeconds=3600)](https://pypi.org/project/fastapi2postman/)
32
+ [![CI](https://github.com/ashhadahsan/fastapi2postman/actions/workflows/ci.yml/badge.svg)](https://github.com/ashhadahsan/fastapi2postman/actions/workflows/ci.yml)
33
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/fastapi2postman?cacheSeconds=3600)](https://pypi.org/project/fastapi2postman/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/fastapi2postman?cacheSeconds=3600)](https://pypi.org/project/fastapi2postman/)
5
+ [![CI](https://github.com/ashhadahsan/fastapi2postman/actions/workflows/ci.yml/badge.svg)](https://github.com/ashhadahsan/fastapi2postman/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
@@ -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,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,3 +0,0 @@
1
- [console_scripts]
2
- fastapi2postman = fastapi2postman:main
3
-
@@ -1,4 +0,0 @@
1
- click>=8.0.0
2
- requests>=2.26.0
3
- fastapi>=0.68.1
4
- uvicorn>=0.15.0
@@ -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()
@@ -1,4 +0,0 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
@@ -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
- )