swastik-codes 0.1.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.
- swastik_codes-0.1.0/.gitignore +12 -0
- swastik_codes-0.1.0/LICENSE.md +27 -0
- swastik_codes-0.1.0/PKG-INFO +195 -0
- swastik_codes-0.1.0/README.md +149 -0
- swastik_codes-0.1.0/pyproject.toml +33 -0
- swastik_codes-0.1.0/src/swastik_codes/__init__.py +28 -0
- swastik_codes-0.1.0/src/swastik_codes/__main__.py +33 -0
- swastik_codes-0.1.0/src/swastik_codes/adapters.py +169 -0
- swastik_codes-0.1.0/src/swastik_codes/assets/logo.svg +1 -0
- swastik_codes-0.1.0/src/swastik_codes/assets/manifest.json +6 -0
- swastik_codes-0.1.0/src/swastik_codes/assets/swc-docs.6b428a9eb2.css +1 -0
- swastik_codes-0.1.0/src/swastik_codes/assets/swc-docs.6b428a9eb2.css.br +0 -0
- swastik_codes-0.1.0/src/swastik_codes/assets/swc-docs.6b428a9eb2.css.gz +0 -0
- swastik_codes-0.1.0/src/swastik_codes/assets/swc-docs.a102eaf1ec.js +59 -0
- swastik_codes-0.1.0/src/swastik_codes/assets/swc-docs.a102eaf1ec.js.br +0 -0
- swastik_codes-0.1.0/src/swastik_codes/assets/swc-docs.a102eaf1ec.js.gz +0 -0
- swastik_codes-0.1.0/src/swastik_codes/core.py +443 -0
- swastik_codes-0.1.0/src/swastik_codes/schema.json +111 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Swastik Codes licence
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Shwastik Tech Solutions Pvt Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Swastik Codes Docs (the `@swastik-codes/docs` npm package and the
|
|
6
|
+
`swastik-codes` Python package) is proprietary software of Shwastik Tech
|
|
7
|
+
Solutions Pvt Ltd. It is not open source.
|
|
8
|
+
|
|
9
|
+
You may install it, free of charge, and use it to publish documentation and a
|
|
10
|
+
request collection for your own APIs, in development and in production, under
|
|
11
|
+
the Terms of service at https://swastik.codes/terms/ (section 7, "Our
|
|
12
|
+
intellectual property", in particular).
|
|
13
|
+
|
|
14
|
+
The pages it serves show the Swastik Codes name and logo and a "Run in Swastik
|
|
15
|
+
Codes" button. You may not remove, hide, cover or change them, or make the
|
|
16
|
+
pages appear to be made by anyone else.
|
|
17
|
+
|
|
18
|
+
You may not copy, modify, decompile, disassemble, reverse engineer, resell,
|
|
19
|
+
sublicense or redistribute the software or any part of it, except where the
|
|
20
|
+
law allows it despite this restriction. Installing it through a package
|
|
21
|
+
registry (npm, PyPI) or a company mirror of one, and bundling it unchanged in
|
|
22
|
+
your application, is not redistribution.
|
|
23
|
+
|
|
24
|
+
The software is provided "as is", without warranty of any kind, to the extent
|
|
25
|
+
the law allows.
|
|
26
|
+
|
|
27
|
+
Questions: https://swastik.codes/contact/
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: swastik-codes
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: API documentation and a request collection for FastAPI, Starlette, Flask, Django and any ASGI/WSGI app, from your OpenAPI document. No dependencies.
|
|
5
|
+
Project-URL: Homepage, https://swastik.codes/
|
|
6
|
+
Project-URL: Support, https://swastik.codes/contact/
|
|
7
|
+
Author: Shwastik Tech Solutions Pvt Ltd
|
|
8
|
+
License: # Swastik Codes licence
|
|
9
|
+
|
|
10
|
+
Copyright © 2026 Shwastik Tech Solutions Pvt Ltd. All rights reserved.
|
|
11
|
+
|
|
12
|
+
Swastik Codes Docs (the `@swastik-codes/docs` npm package and the
|
|
13
|
+
`swastik-codes` Python package) is proprietary software of Shwastik Tech
|
|
14
|
+
Solutions Pvt Ltd. It is not open source.
|
|
15
|
+
|
|
16
|
+
You may install it, free of charge, and use it to publish documentation and a
|
|
17
|
+
request collection for your own APIs, in development and in production, under
|
|
18
|
+
the Terms of service at https://swastik.codes/terms/ (section 7, "Our
|
|
19
|
+
intellectual property", in particular).
|
|
20
|
+
|
|
21
|
+
The pages it serves show the Swastik Codes name and logo and a "Run in Swastik
|
|
22
|
+
Codes" button. You may not remove, hide, cover or change them, or make the
|
|
23
|
+
pages appear to be made by anyone else.
|
|
24
|
+
|
|
25
|
+
You may not copy, modify, decompile, disassemble, reverse engineer, resell,
|
|
26
|
+
sublicense or redistribute the software or any part of it, except where the
|
|
27
|
+
law allows it despite this restriction. Installing it through a package
|
|
28
|
+
registry (npm, PyPI) or a company mirror of one, and bundling it unchanged in
|
|
29
|
+
your application, is not redistribution.
|
|
30
|
+
|
|
31
|
+
The software is provided "as is", without warranty of any kind, to the extent
|
|
32
|
+
the law allows.
|
|
33
|
+
|
|
34
|
+
Questions: https://swastik.codes/contact/
|
|
35
|
+
License-File: LICENSE.md
|
|
36
|
+
Keywords: api-docs,django,documentation,fastapi,flask,openapi,starlette,swagger,swagger-ui,swastik-codes
|
|
37
|
+
Classifier: Framework :: Django
|
|
38
|
+
Classifier: Framework :: FastAPI
|
|
39
|
+
Classifier: Framework :: Flask
|
|
40
|
+
Classifier: License :: Other/Proprietary License
|
|
41
|
+
Classifier: Programming Language :: Python :: 3
|
|
42
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware
|
|
43
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
44
|
+
Requires-Python: >=3.8
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# Swastik Codes Docs for Python
|
|
48
|
+
|
|
49
|
+
API documentation and a request collection for your Python web app, built from
|
|
50
|
+
the OpenAPI document you already have. Install it, add one line, and you get two
|
|
51
|
+
pages:
|
|
52
|
+
|
|
53
|
+
| URL | What it is |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `/swc/docs` | Full documentation: introduction, authentication, every request with its parameters, body, responses, code samples (cURL, JavaScript, Python, Go, PHP) and example responses. Search, deep links, light and dark themes. |
|
|
56
|
+
| `/swc/collection` | Every request in a collection tree. Fill in params, auth, headers and body, press **Send**, and read the response (pretty JSON, headers, status, time, size). |
|
|
57
|
+
| `/swc/openapi.json` | The document both pages read, with your edits applied. |
|
|
58
|
+
|
|
59
|
+
Both pages have a **Run in Swastik Codes** button that opens the API in
|
|
60
|
+
[Swastik Codes](https://swastik.codes) as a collection.
|
|
61
|
+
|
|
62
|
+
- **No dependencies.** Standard library only.
|
|
63
|
+
- **Small.** The whole UI is one script and one stylesheet, about 22 KB over the wire, precompressed (brotli and gzip) and cached for a year.
|
|
64
|
+
- **Cheap to serve.** The merged document is built once and cached until your spec or config changes. Spec responses carry an ETag, so repeat visits get `304`.
|
|
65
|
+
|
|
66
|
+
## FastAPI
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install swastik-codes
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from fastapi import FastAPI
|
|
74
|
+
from swastik_codes import mount
|
|
75
|
+
|
|
76
|
+
app = FastAPI(title="My API")
|
|
77
|
+
|
|
78
|
+
# ... your routes ...
|
|
79
|
+
|
|
80
|
+
mount(app, config="swastik-codes.json") # /swc/docs and /swc/collection
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`mount()` reads FastAPI's own cached `app.openapi()`. FastAPI's `/docs` and
|
|
84
|
+
`/redoc` keep working. Turn them off with `FastAPI(docs_url=None, redoc_url=None)`
|
|
85
|
+
if you only want these pages. To serve them at `/docs` instead, use
|
|
86
|
+
`mount(app, prefix="")` together with `docs_url=None`.
|
|
87
|
+
|
|
88
|
+
Starlette works the same way. Pass `spec=` with your document, because Starlette has no `app.openapi()`.
|
|
89
|
+
|
|
90
|
+
## Flask
|
|
91
|
+
|
|
92
|
+
Flask has no OpenAPI generator of its own. Pass the document from the library you use (flask-smorest, apispec, flasgger) or a dict:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from swastik_codes import init_flask
|
|
96
|
+
|
|
97
|
+
init_flask(app, spec=lambda: api.spec.to_dict(), config="swastik-codes.json")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Django
|
|
101
|
+
|
|
102
|
+
Use the document from drf-spectacular (or any generator). Cache it so it is built once:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
# urls.py (root)
|
|
106
|
+
import functools
|
|
107
|
+
from drf_spectacular.generators import SchemaGenerator
|
|
108
|
+
from swastik_codes import django_urls
|
|
109
|
+
|
|
110
|
+
spec = functools.cache(lambda: SchemaGenerator().get_schema(request=None, public=True))
|
|
111
|
+
urlpatterns += django_urls(spec=spec, config="swastik-codes.json")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Any ASGI or WSGI app
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from swastik_codes import SwastikCodesMiddleware, SwastikCodesWSGI
|
|
118
|
+
|
|
119
|
+
app = SwastikCodesMiddleware(app, spec=my_openapi_dict) # ASGI: Litestar, Quart, ...
|
|
120
|
+
wsgi_app = SwastikCodesWSGI(wsgi_app, spec=my_openapi_dict) # WSGI: Bottle, Pyramid, ...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`spec` is always a dict or a function that returns one. A function is called on
|
|
124
|
+
each page load, so it should be cheap or cached. The merge is only redone when it
|
|
125
|
+
returns a different object.
|
|
126
|
+
|
|
127
|
+
## Editing the pages: swastik-codes.json
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python -m swastik_codes init # writes a starter swastik-codes.json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Every field is optional. Your OpenAPI document provides the rest. The file is
|
|
134
|
+
re-read when it changes, so you can edit it and reload the page.
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"title": "Flight OTA API",
|
|
139
|
+
"version": "2.0",
|
|
140
|
+
"description": "Markdown introduction.\n\n## Getting started\n\n1. Get a token.\n2. Send it as a bearer token.",
|
|
141
|
+
"servers": [
|
|
142
|
+
{ "url": "https://sandbox.example.com", "description": "Sandbox" },
|
|
143
|
+
{ "url": "https://api.example.com", "description": "Production" }
|
|
144
|
+
],
|
|
145
|
+
"theme": { "accent": "#0d9488", "mode": "system" },
|
|
146
|
+
"tags": { "Search": { "name": "Flight search", "description": "Find flights." } },
|
|
147
|
+
"groups": [
|
|
148
|
+
{ "name": "Start here", "tags": ["Authentication"] },
|
|
149
|
+
{ "name": "Flights", "tags": ["Search", "Bookings"] }
|
|
150
|
+
],
|
|
151
|
+
"operations": {
|
|
152
|
+
"GET /v2/bookings/{booking_id}": { "summary": "Read a held booking", "description": "Markdown." },
|
|
153
|
+
"deleteCache": { "hidden": true }
|
|
154
|
+
},
|
|
155
|
+
"hide": ["/internal/*", "DELETE /admin/*"],
|
|
156
|
+
"pages": [{ "title": "Errors", "body": "Markdown, tables included." }],
|
|
157
|
+
"links": [{ "label": "Support", "url": "https://example.com/support" }],
|
|
158
|
+
"languages": ["curl", "javascript", "python", "go", "php"]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
| Field | Effect |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `title`, `version`, `description` | Replace `info.*`. The description is Markdown. |
|
|
165
|
+
| `servers` | Base URLs readers pick from (the first is the default). Readers can also enter their own. |
|
|
166
|
+
| `theme` | Accent colour, and the default theme (`system`, `light` or `dark`). |
|
|
167
|
+
| `tags` | Display name and description per tag (each tag is a folder). |
|
|
168
|
+
| `groups` | Sections that hold tags, in order. |
|
|
169
|
+
| `operations` | Per request, keyed by `"METHOD /path"` or `operationId`: `summary`, `description`, `tags`, `deprecated`, `hidden`. |
|
|
170
|
+
| `hide` | Paths to leave out. `*` matches anything. Prefix with a method to hide only that method. |
|
|
171
|
+
| `pages` | Extra Markdown pages under Introduction. |
|
|
172
|
+
| `links` | Links on the introduction. |
|
|
173
|
+
| `languages` | Code sample languages and their order. |
|
|
174
|
+
|
|
175
|
+
The Swastik Codes logo, the "Built with Swastik Codes" credit and the **Run in
|
|
176
|
+
Swastik Codes** button are part of the pages. They have no setting. Keys such as
|
|
177
|
+
`logo` or `hideRunButton` are ignored with a warning.
|
|
178
|
+
|
|
179
|
+
## Notes
|
|
180
|
+
|
|
181
|
+
- **Sending requests.** The collection page sends requests from the browser.
|
|
182
|
+
Requests to the same server always work. Requests to another server need that
|
|
183
|
+
server to allow CORS for your docs origin, the same as Swagger UI.
|
|
184
|
+
- **Credentials.** Tokens you enter are kept in the tab's session storage. They
|
|
185
|
+
are never sent anywhere except with your requests.
|
|
186
|
+
- **Security headers.** The pages are sent with a strict Content-Security-Policy:
|
|
187
|
+
no inline scripts, and scripts and styles from this server only. Pass
|
|
188
|
+
`csp=False` to turn it off.
|
|
189
|
+
- **Run in Swastik Codes** opens `https://swastik.codes/app?import=<your openapi.json>`.
|
|
190
|
+
For an API on `localhost` it offers the file to download instead, because a
|
|
191
|
+
website can't reach your computer.
|
|
192
|
+
|
|
193
|
+
## Licence
|
|
194
|
+
|
|
195
|
+
Proprietary, free to use. See [LICENSE.md](LICENSE.md). © Shwastik Tech Solutions Pvt Ltd.
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Swastik Codes Docs for Python
|
|
2
|
+
|
|
3
|
+
API documentation and a request collection for your Python web app, built from
|
|
4
|
+
the OpenAPI document you already have. Install it, add one line, and you get two
|
|
5
|
+
pages:
|
|
6
|
+
|
|
7
|
+
| URL | What it is |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `/swc/docs` | Full documentation: introduction, authentication, every request with its parameters, body, responses, code samples (cURL, JavaScript, Python, Go, PHP) and example responses. Search, deep links, light and dark themes. |
|
|
10
|
+
| `/swc/collection` | Every request in a collection tree. Fill in params, auth, headers and body, press **Send**, and read the response (pretty JSON, headers, status, time, size). |
|
|
11
|
+
| `/swc/openapi.json` | The document both pages read, with your edits applied. |
|
|
12
|
+
|
|
13
|
+
Both pages have a **Run in Swastik Codes** button that opens the API in
|
|
14
|
+
[Swastik Codes](https://swastik.codes) as a collection.
|
|
15
|
+
|
|
16
|
+
- **No dependencies.** Standard library only.
|
|
17
|
+
- **Small.** The whole UI is one script and one stylesheet, about 22 KB over the wire, precompressed (brotli and gzip) and cached for a year.
|
|
18
|
+
- **Cheap to serve.** The merged document is built once and cached until your spec or config changes. Spec responses carry an ETag, so repeat visits get `304`.
|
|
19
|
+
|
|
20
|
+
## FastAPI
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install swastik-codes
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from fastapi import FastAPI
|
|
28
|
+
from swastik_codes import mount
|
|
29
|
+
|
|
30
|
+
app = FastAPI(title="My API")
|
|
31
|
+
|
|
32
|
+
# ... your routes ...
|
|
33
|
+
|
|
34
|
+
mount(app, config="swastik-codes.json") # /swc/docs and /swc/collection
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`mount()` reads FastAPI's own cached `app.openapi()`. FastAPI's `/docs` and
|
|
38
|
+
`/redoc` keep working. Turn them off with `FastAPI(docs_url=None, redoc_url=None)`
|
|
39
|
+
if you only want these pages. To serve them at `/docs` instead, use
|
|
40
|
+
`mount(app, prefix="")` together with `docs_url=None`.
|
|
41
|
+
|
|
42
|
+
Starlette works the same way. Pass `spec=` with your document, because Starlette has no `app.openapi()`.
|
|
43
|
+
|
|
44
|
+
## Flask
|
|
45
|
+
|
|
46
|
+
Flask has no OpenAPI generator of its own. Pass the document from the library you use (flask-smorest, apispec, flasgger) or a dict:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from swastik_codes import init_flask
|
|
50
|
+
|
|
51
|
+
init_flask(app, spec=lambda: api.spec.to_dict(), config="swastik-codes.json")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Django
|
|
55
|
+
|
|
56
|
+
Use the document from drf-spectacular (or any generator). Cache it so it is built once:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
# urls.py (root)
|
|
60
|
+
import functools
|
|
61
|
+
from drf_spectacular.generators import SchemaGenerator
|
|
62
|
+
from swastik_codes import django_urls
|
|
63
|
+
|
|
64
|
+
spec = functools.cache(lambda: SchemaGenerator().get_schema(request=None, public=True))
|
|
65
|
+
urlpatterns += django_urls(spec=spec, config="swastik-codes.json")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Any ASGI or WSGI app
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from swastik_codes import SwastikCodesMiddleware, SwastikCodesWSGI
|
|
72
|
+
|
|
73
|
+
app = SwastikCodesMiddleware(app, spec=my_openapi_dict) # ASGI: Litestar, Quart, ...
|
|
74
|
+
wsgi_app = SwastikCodesWSGI(wsgi_app, spec=my_openapi_dict) # WSGI: Bottle, Pyramid, ...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`spec` is always a dict or a function that returns one. A function is called on
|
|
78
|
+
each page load, so it should be cheap or cached. The merge is only redone when it
|
|
79
|
+
returns a different object.
|
|
80
|
+
|
|
81
|
+
## Editing the pages: swastik-codes.json
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
python -m swastik_codes init # writes a starter swastik-codes.json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Every field is optional. Your OpenAPI document provides the rest. The file is
|
|
88
|
+
re-read when it changes, so you can edit it and reload the page.
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"title": "Flight OTA API",
|
|
93
|
+
"version": "2.0",
|
|
94
|
+
"description": "Markdown introduction.\n\n## Getting started\n\n1. Get a token.\n2. Send it as a bearer token.",
|
|
95
|
+
"servers": [
|
|
96
|
+
{ "url": "https://sandbox.example.com", "description": "Sandbox" },
|
|
97
|
+
{ "url": "https://api.example.com", "description": "Production" }
|
|
98
|
+
],
|
|
99
|
+
"theme": { "accent": "#0d9488", "mode": "system" },
|
|
100
|
+
"tags": { "Search": { "name": "Flight search", "description": "Find flights." } },
|
|
101
|
+
"groups": [
|
|
102
|
+
{ "name": "Start here", "tags": ["Authentication"] },
|
|
103
|
+
{ "name": "Flights", "tags": ["Search", "Bookings"] }
|
|
104
|
+
],
|
|
105
|
+
"operations": {
|
|
106
|
+
"GET /v2/bookings/{booking_id}": { "summary": "Read a held booking", "description": "Markdown." },
|
|
107
|
+
"deleteCache": { "hidden": true }
|
|
108
|
+
},
|
|
109
|
+
"hide": ["/internal/*", "DELETE /admin/*"],
|
|
110
|
+
"pages": [{ "title": "Errors", "body": "Markdown, tables included." }],
|
|
111
|
+
"links": [{ "label": "Support", "url": "https://example.com/support" }],
|
|
112
|
+
"languages": ["curl", "javascript", "python", "go", "php"]
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Field | Effect |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `title`, `version`, `description` | Replace `info.*`. The description is Markdown. |
|
|
119
|
+
| `servers` | Base URLs readers pick from (the first is the default). Readers can also enter their own. |
|
|
120
|
+
| `theme` | Accent colour, and the default theme (`system`, `light` or `dark`). |
|
|
121
|
+
| `tags` | Display name and description per tag (each tag is a folder). |
|
|
122
|
+
| `groups` | Sections that hold tags, in order. |
|
|
123
|
+
| `operations` | Per request, keyed by `"METHOD /path"` or `operationId`: `summary`, `description`, `tags`, `deprecated`, `hidden`. |
|
|
124
|
+
| `hide` | Paths to leave out. `*` matches anything. Prefix with a method to hide only that method. |
|
|
125
|
+
| `pages` | Extra Markdown pages under Introduction. |
|
|
126
|
+
| `links` | Links on the introduction. |
|
|
127
|
+
| `languages` | Code sample languages and their order. |
|
|
128
|
+
|
|
129
|
+
The Swastik Codes logo, the "Built with Swastik Codes" credit and the **Run in
|
|
130
|
+
Swastik Codes** button are part of the pages. They have no setting. Keys such as
|
|
131
|
+
`logo` or `hideRunButton` are ignored with a warning.
|
|
132
|
+
|
|
133
|
+
## Notes
|
|
134
|
+
|
|
135
|
+
- **Sending requests.** The collection page sends requests from the browser.
|
|
136
|
+
Requests to the same server always work. Requests to another server need that
|
|
137
|
+
server to allow CORS for your docs origin, the same as Swagger UI.
|
|
138
|
+
- **Credentials.** Tokens you enter are kept in the tab's session storage. They
|
|
139
|
+
are never sent anywhere except with your requests.
|
|
140
|
+
- **Security headers.** The pages are sent with a strict Content-Security-Policy:
|
|
141
|
+
no inline scripts, and scripts and styles from this server only. Pass
|
|
142
|
+
`csp=False` to turn it off.
|
|
143
|
+
- **Run in Swastik Codes** opens `https://swastik.codes/app?import=<your openapi.json>`.
|
|
144
|
+
For an API on `localhost` it offers the file to download instead, because a
|
|
145
|
+
website can't reach your computer.
|
|
146
|
+
|
|
147
|
+
## Licence
|
|
148
|
+
|
|
149
|
+
Proprietary, free to use. See [LICENSE.md](LICENSE.md). © Shwastik Tech Solutions Pvt Ltd.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "swastik-codes"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "API documentation and a request collection for FastAPI, Starlette, Flask, Django and any ASGI/WSGI app, from your OpenAPI document. No dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { file = "LICENSE.md" }
|
|
12
|
+
authors = [{ name = "Shwastik Tech Solutions Pvt Ltd" }]
|
|
13
|
+
keywords = ["openapi", "swagger", "swagger-ui", "api-docs", "fastapi", "flask", "django", "starlette", "documentation", "swastik-codes"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Framework :: FastAPI",
|
|
16
|
+
"Framework :: Flask",
|
|
17
|
+
"Framework :: Django",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: Other/Proprietary License",
|
|
20
|
+
"Topic :: Software Development :: Documentation",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://swastik.codes/"
|
|
27
|
+
Support = "https://swastik.codes/contact/"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/swastik_codes"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.sdist]
|
|
33
|
+
include = ["src/swastik_codes", "README.md", "LICENSE.md"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Swastik Codes docs: API documentation and a request collection for Python web apps.
|
|
2
|
+
|
|
3
|
+
FastAPI::
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
from swastik_codes import mount
|
|
7
|
+
|
|
8
|
+
app = FastAPI()
|
|
9
|
+
mount(app, config="swastik-codes.json") # /swc/docs and /swc/collection
|
|
10
|
+
|
|
11
|
+
Flask, Django and any ASGI/WSGI app: see README.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .adapters import SwastikCodesMiddleware, SwastikCodesWSGI, django_urls, init_flask, mount
|
|
15
|
+
from .core import Docs, Response, SwastikCodesWarning, merge_spec
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Docs",
|
|
19
|
+
"Response",
|
|
20
|
+
"SwastikCodesMiddleware",
|
|
21
|
+
"SwastikCodesWSGI",
|
|
22
|
+
"SwastikCodesWarning",
|
|
23
|
+
"django_urls",
|
|
24
|
+
"init_flask",
|
|
25
|
+
"merge_spec",
|
|
26
|
+
"mount",
|
|
27
|
+
]
|
|
28
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""python -m swastik_codes init [swastik-codes.json] — writes a starter configuration."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
STARTER = {
|
|
8
|
+
"theme": {"accent": "#0d9488", "mode": "system"},
|
|
9
|
+
"tags": {},
|
|
10
|
+
"groups": [],
|
|
11
|
+
"operations": {},
|
|
12
|
+
"hide": [],
|
|
13
|
+
"pages": [],
|
|
14
|
+
"links": [],
|
|
15
|
+
"languages": ["curl", "javascript", "python", "go"],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: list) -> int:
|
|
20
|
+
if not argv or argv[0] != "init":
|
|
21
|
+
print("Usage: python -m swastik_codes init [swastik-codes.json]\n\nWrites a starter configuration for the docs and collection pages.")
|
|
22
|
+
return 1 if argv else 0
|
|
23
|
+
target = Path(argv[1] if len(argv) > 1 else "swastik-codes.json")
|
|
24
|
+
if target.exists():
|
|
25
|
+
print(f"{target} already exists: nothing written.", file=sys.stderr)
|
|
26
|
+
return 1
|
|
27
|
+
target.write_text(json.dumps(STARTER, indent=2) + "\n", "utf-8")
|
|
28
|
+
print(f'Wrote {target}. Pass it as config="{target}" and edit it: the pages pick up changes on the next reload.')
|
|
29
|
+
return 0
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Adapters: FastAPI / Starlette, any ASGI app, any WSGI app, Flask and Django."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Iterable, List, Optional
|
|
6
|
+
|
|
7
|
+
from .core import DEFAULT_PREFIX, Config, Docs, Response, SpecSource
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _route_path(scope: dict) -> str:
|
|
11
|
+
path = scope.get("path") or "/"
|
|
12
|
+
root = scope.get("root_path") or ""
|
|
13
|
+
# Newer servers keep root_path inside path; older ones strip it.
|
|
14
|
+
if root and path.startswith(root):
|
|
15
|
+
path = path[len(root):] or "/"
|
|
16
|
+
return path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _asgi_headers(scope: dict) -> dict:
|
|
20
|
+
return {k.decode("latin-1").lower(): v.decode("latin-1") for k, v in scope.get("headers") or []}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def _asgi_send(send: Callable, r: Response) -> None:
|
|
24
|
+
await send({"type": "http.response.start", "status": r.status, "headers": [(k.encode("latin-1"), v.encode("latin-1")) for k, v in r.headers]})
|
|
25
|
+
await send({"type": "http.response.body", "body": r.body})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---- ASGI ----------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SwastikCodesMiddleware:
|
|
32
|
+
"""ASGI middleware for any ASGI app (FastAPI, Starlette, Litestar, Quart, Django ASGI…).
|
|
33
|
+
|
|
34
|
+
app = SwastikCodesMiddleware(app, spec=..., config="swastik-codes.json")
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, app: Any, spec: SpecSource, config: Config = None, prefix: str = DEFAULT_PREFIX, csp: bool = True):
|
|
38
|
+
self.app = app
|
|
39
|
+
self.docs = Docs(spec, config=config, prefix=prefix, csp=csp)
|
|
40
|
+
|
|
41
|
+
async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:
|
|
42
|
+
if scope.get("type") == "http":
|
|
43
|
+
path = _route_path(scope)
|
|
44
|
+
if path == self.docs.prefix or path.startswith(self.docs.prefix + "/") or self.docs.prefix == "":
|
|
45
|
+
r = self.docs.handle(scope.get("method", "GET"), path, _asgi_headers(scope), scope.get("path"))
|
|
46
|
+
if r is not None:
|
|
47
|
+
await _asgi_send(send, r)
|
|
48
|
+
return
|
|
49
|
+
await self.app(scope, receive, send)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def mount(app: Any, prefix: str = DEFAULT_PREFIX, config: Config = None, spec: Optional[SpecSource] = None, csp: bool = True) -> Docs:
|
|
53
|
+
"""Add the pages to a FastAPI (or Starlette) app.
|
|
54
|
+
|
|
55
|
+
from swastik_codes import mount
|
|
56
|
+
mount(app, config="swastik-codes.json") # → /swc/docs and /swc/collection
|
|
57
|
+
|
|
58
|
+
``spec`` defaults to ``app.openapi`` (FastAPI's own cached document).
|
|
59
|
+
"""
|
|
60
|
+
if spec is None:
|
|
61
|
+
if not callable(getattr(app, "openapi", None)):
|
|
62
|
+
raise TypeError("swastik_codes.mount: this app has no .openapi(); pass spec=<your OpenAPI dict or a function>")
|
|
63
|
+
spec = app.openapi
|
|
64
|
+
docs = Docs(spec, config=config, prefix=prefix, csp=csp)
|
|
65
|
+
|
|
66
|
+
class Endpoint: # Starlette calls an object (not a function) as a raw ASGI app
|
|
67
|
+
async def __call__(self, scope: dict, receive: Callable, send: Callable) -> None:
|
|
68
|
+
r = docs.handle(scope.get("method", "GET"), _route_path(scope), _asgi_headers(scope), scope.get("path"))
|
|
69
|
+
if r is None:
|
|
70
|
+
r = Response(404, [("content-type", "text/plain")], b"Not found")
|
|
71
|
+
await _asgi_send(send, r)
|
|
72
|
+
|
|
73
|
+
endpoint = Endpoint()
|
|
74
|
+
|
|
75
|
+
from starlette.routing import Route # FastAPI and Starlette ship it
|
|
76
|
+
|
|
77
|
+
# Raw ASGI endpoints (no Request object), placed ahead of any catch-all routes the app has.
|
|
78
|
+
paths = [docs.prefix, docs.prefix + "/{rest:path}"] if docs.prefix else ["/docs", "/docs/", "/collection", "/collection/", "/openapi.json", "/_swc/{rest:path}"]
|
|
79
|
+
for i, p in enumerate(paths):
|
|
80
|
+
app.router.routes.insert(i, Route(p, endpoint, methods=["GET", "HEAD", "OPTIONS"], include_in_schema=False))
|
|
81
|
+
return docs
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---- WSGI ----------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SwastikCodesWSGI:
|
|
88
|
+
"""WSGI middleware: app.wsgi_app = SwastikCodesWSGI(app.wsgi_app, spec=...)."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, app: Callable, spec: SpecSource, config: Config = None, prefix: str = DEFAULT_PREFIX, csp: bool = True):
|
|
91
|
+
self.app = app
|
|
92
|
+
self.docs = Docs(spec, config=config, prefix=prefix, csp=csp)
|
|
93
|
+
|
|
94
|
+
def __call__(self, environ: dict, start_response: Callable) -> Iterable[bytes]:
|
|
95
|
+
path = environ.get("PATH_INFO") or "/"
|
|
96
|
+
r = self.docs.handle(environ.get("REQUEST_METHOD", "GET"), path, _wsgi_headers(environ), (environ.get("SCRIPT_NAME") or "") + path)
|
|
97
|
+
if r is None:
|
|
98
|
+
return self.app(environ, start_response)
|
|
99
|
+
start_response(f"{r.status} {_REASONS.get(r.status, 'OK')}", r.headers)
|
|
100
|
+
return [r.body]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
_REASONS = {200: "OK", 204: "No Content", 301: "Moved Permanently", 302: "Found", 304: "Not Modified", 404: "Not Found"}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _wsgi_headers(environ: dict) -> dict:
|
|
107
|
+
out = {k[5:].replace("_", "-").lower(): v for k, v in environ.items() if k.startswith("HTTP_")}
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---- Flask ---------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def init_flask(app: Any, spec: SpecSource, config: Config = None, prefix: str = DEFAULT_PREFIX, csp: bool = True) -> Docs:
|
|
115
|
+
"""Add the pages to a Flask app.
|
|
116
|
+
|
|
117
|
+
from swastik_codes import init_flask
|
|
118
|
+
init_flask(app, spec=lambda: api.spec.to_dict(), config="swastik-codes.json")
|
|
119
|
+
"""
|
|
120
|
+
from flask import Response as FlaskResponse, request
|
|
121
|
+
|
|
122
|
+
docs = Docs(spec, config=config, prefix=prefix, csp=csp)
|
|
123
|
+
|
|
124
|
+
def view(rest: str = "") -> Any:
|
|
125
|
+
r = docs.handle(request.method, request.path, {k.lower(): v for k, v in request.headers.items()}, request.full_path if request.query_string else request.path)
|
|
126
|
+
if r is None:
|
|
127
|
+
return FlaskResponse("Not found", status=404, mimetype="text/plain")
|
|
128
|
+
resp = FlaskResponse(r.body, status=r.status)
|
|
129
|
+
resp.headers.clear()
|
|
130
|
+
for k, v in r.headers:
|
|
131
|
+
resp.headers[k] = v
|
|
132
|
+
resp.direct_passthrough = True
|
|
133
|
+
return resp
|
|
134
|
+
|
|
135
|
+
opts = {"methods": ["GET", "HEAD", "OPTIONS"], "provide_automatic_options": False, "strict_slashes": False}
|
|
136
|
+
if docs.prefix:
|
|
137
|
+
app.add_url_rule(docs.prefix, "swastik_codes_root", view, **opts)
|
|
138
|
+
app.add_url_rule(f"{docs.prefix}/<path:rest>", "swastik_codes", view, **opts)
|
|
139
|
+
return docs
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---- Django --------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def django_urls(spec: SpecSource, config: Config = None, prefix: str = DEFAULT_PREFIX, csp: bool = True) -> List[Any]:
|
|
146
|
+
"""URL patterns for Django: ``urlpatterns += django_urls(spec=...)``.
|
|
147
|
+
|
|
148
|
+
Use them in the ROOT urls.py (the prefix is matched against the full path).
|
|
149
|
+
"""
|
|
150
|
+
import re
|
|
151
|
+
|
|
152
|
+
from django.http import HttpResponse
|
|
153
|
+
from django.urls import re_path
|
|
154
|
+
from django.views.decorators.csrf import csrf_exempt
|
|
155
|
+
|
|
156
|
+
docs = Docs(spec, config=config, prefix=prefix, csp=csp)
|
|
157
|
+
|
|
158
|
+
@csrf_exempt
|
|
159
|
+
def view(request: Any, rest: str = "") -> Any:
|
|
160
|
+
r = docs.handle(request.method, request.path_info, {k.lower(): v for k, v in request.headers.items()}, request.path)
|
|
161
|
+
if r is None:
|
|
162
|
+
return HttpResponse("Not found", status=404, content_type="text/plain")
|
|
163
|
+
resp = HttpResponse(r.body, status=r.status)
|
|
164
|
+
for k, v in r.headers:
|
|
165
|
+
resp[k] = v
|
|
166
|
+
return resp
|
|
167
|
+
|
|
168
|
+
p = re.escape(docs.prefix.lstrip("/"))
|
|
169
|
+
return [re_path(rf"^{p}(?:/(?P<rest>.*))?$" if p else r"^(?P<rest>docs/?|collection/?|openapi\.json|_swc/[^/]+)$", view, name="swastik_codes")]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="swc-g" x1="4" y1="2" x2="28" y2="30" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#34d399"/><stop offset=".55" stop-color="#0d9488"/><stop offset="1" stop-color="#0e7490"/></linearGradient></defs><rect width="32" height="32" rx="8.5" fill="url(#swc-g)"/><path d="M21.6 9H13.8a3.5 3.5 0 0 0 0 7h2.5" fill="none" stroke="#fff" stroke-width="3.2" stroke-linejoin="round"/><path d="M16 16h2.2a3.5 3.5 0 0 1 0 7h-7.8" fill="none" stroke="#a7f3d0" stroke-width="3.2" stroke-linejoin="round"/><circle cx="21.8" cy="9" r="2.35" fill="#fff"/><circle cx="10.2" cy="23" r="2.35" fill="#a7f3d0"/></svg>
|