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.
@@ -0,0 +1,12 @@
1
+ /target
2
+ **/target
3
+ node_modules/
4
+ dist/
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+ *.log
9
+ .DS_Store
10
+ apps/desktop/src-tauri/gen/
11
+ /target-*
12
+ /deploy/downloads/
@@ -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>
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "0.1.0",
3
+ "js": "swc-docs.a102eaf1ec.js",
4
+ "css": "swc-docs.6b428a9eb2.css",
5
+ "logo": "logo.svg"
6
+ }