sveltekit-python-vercel 0.4.1 → 1.0.3-beta.8b30fdb
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.
- package/README.md +45 -116
- package/esm/src/vite/mod.js +3 -23
- package/esm/src/vite/sveltekit_python_vercel/build.py +110 -35
- package/esm/src/vite/sveltekit_python_vercel/deploy.py +132 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<p align="middle">
|
|
2
2
|
<img width="100" alt="image" src="https://user-images.githubusercontent.com/20548516/218344678-d41f4c4a-6b1b-48cc-8553-2b9fbe2169d6.png"/>
|
|
3
|
-
<img width="100" alt="image" src="https://
|
|
4
|
-
<img width="100" alt="image" src="https://
|
|
3
|
+
<img width="100" alt="image" src="https://avatars.githubusercontent.com/u/1525981?s=200&v=4"/>
|
|
4
|
+
<img width="100" alt="image" src="https://avatars.githubusercontent.com/u/14985020?s=200&v=4"/>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
# sveltekit-python-vercel
|
|
@@ -22,7 +22,7 @@ Write Python endpoints in [SvelteKit](https://kit.svelte.dev/) and seamlessly de
|
|
|
22
22
|
## Current Features
|
|
23
23
|
|
|
24
24
|
- Write `+server.py` files nearly the same way you would write `+server.js` files
|
|
25
|
-
- Deploy
|
|
25
|
+
- Deploy automatically to Vercel Serverless (Python 3.12 runtime)
|
|
26
26
|
|
|
27
27
|
## Installing
|
|
28
28
|
|
|
@@ -36,17 +36,15 @@ Write Python endpoints in [SvelteKit](https://kit.svelte.dev/) and seamlessly de
|
|
|
36
36
|
import { sveltekit } from "@sveltejs/kit/vite";
|
|
37
37
|
import { sveltekit_python_vercel } from "sveltekit-python-vercel/vite";
|
|
38
38
|
|
|
39
|
-
export default defineConfig(
|
|
40
|
-
|
|
41
|
-
plugins: [sveltekit_python_vercel(), sveltekit()],
|
|
42
|
-
};
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
plugins: [sveltekit(), ...(await sveltekit_python_vercel())],
|
|
43
41
|
});
|
|
44
42
|
```
|
|
45
43
|
|
|
46
44
|
- Update your `svelte.config.js`:
|
|
47
45
|
|
|
48
46
|
```javascript
|
|
49
|
-
import adapter from "@sveltejs/adapter-vercel";
|
|
47
|
+
import adapter from "@sveltejs/adapter-vercel";
|
|
50
48
|
import { vitePreprocess } from "@sveltejs/kit/vite";
|
|
51
49
|
|
|
52
50
|
/** @type {import('@sveltejs/kit').Config} */
|
|
@@ -63,23 +61,12 @@ Write Python endpoints in [SvelteKit](https://kit.svelte.dev/) and seamlessly de
|
|
|
63
61
|
|
|
64
62
|
- Update your `vercel.json`
|
|
65
63
|
|
|
66
|
-
- The build command
|
|
67
|
-
-
|
|
64
|
+
- The build command first runs `vite build` (which generates `.vercel/output/` via the SvelteKit adapter), then runs our script to write the Python function into that same output directory and patch the routing config.
|
|
65
|
+
- No `routes` or `functions` keys are needed — routing is handled automatically via the [Vercel Build Output API](https://vercel.com/docs/build-output-api/v3).
|
|
68
66
|
|
|
69
67
|
```json
|
|
70
68
|
{
|
|
71
|
-
"buildCommand": "node ./node_modules/sveltekit-python-vercel/esm/src/vite/sveltekit_python_vercel/bin.mjs
|
|
72
|
-
"functions": {
|
|
73
|
-
"api/**/*.py": {
|
|
74
|
-
"runtime": "@vercel/python@3.0.7"
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
"routes": [
|
|
78
|
-
{
|
|
79
|
-
"src": "/api/(.*)",
|
|
80
|
-
"dest": "api/index.py"
|
|
81
|
-
}
|
|
82
|
-
]
|
|
69
|
+
"buildCommand": "vite build; node ./node_modules/sveltekit-python-vercel/esm/src/vite/sveltekit_python_vercel/bin.mjs"
|
|
83
70
|
}
|
|
84
71
|
```
|
|
85
72
|
|
|
@@ -87,112 +74,59 @@ Write Python endpoints in [SvelteKit](https://kit.svelte.dev/) and seamlessly de
|
|
|
87
74
|
|
|
88
75
|
## Testing Locally
|
|
89
76
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- Run `poetry init` to create a new virtual environment, and follow the steps. Or simply create a `pyproject.toml` like the one below.
|
|
93
|
-
|
|
94
|
-
```toml
|
|
95
|
-
[tool.poetry]
|
|
96
|
-
name = "sveltekit-python-example"
|
|
97
|
-
version = "0.1.0"
|
|
98
|
-
description = ""
|
|
99
|
-
authors = ["Your Name <email@gmail.com>"]
|
|
100
|
-
readme = "README.md"
|
|
101
|
-
|
|
102
|
-
[tool.poetry.dependencies]
|
|
103
|
-
python = "^3.9"
|
|
104
|
-
fastapi = "^0.95.2"
|
|
105
|
-
uvicorn = "^0.22.0"
|
|
106
|
-
|
|
107
|
-
[tool.poetry.group.dev.dependencies]
|
|
108
|
-
watchfiles = "^0.19.0"
|
|
109
|
-
|
|
110
|
-
[build-system]
|
|
111
|
-
requires = ["poetry-core"]
|
|
112
|
-
build-backend = "poetry.core.masonry.api"
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
- Required packages are python3.9 (that is what Vercel's runtime uses), `fastapi`, and `uvicorn`.
|
|
116
|
-
- Install whatever other dependencies you need from pypi using `poetry add package-name`
|
|
77
|
+
[uv](https://docs.astral.sh/uv/) is recommended for managing your Python environment.
|
|
117
78
|
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
|
|
79
|
+
- Run `uv init --python 3.12` to create a `pyproject.toml` pinned to Python 3.12 (the same version Vercel's runtime uses).
|
|
80
|
+
- Add the required packages: `uv add fastapi uvicorn`
|
|
81
|
+
- Add any other dependencies you need: `uv add numpy pandas ...`
|
|
82
|
+
- Run your dev server inside uv:
|
|
83
|
+
- `uv run pnpm dev`
|
|
84
|
+
- You should see both the usual SvelteKit server start and the uvicorn server (by default on `http://0.0.0.0:8000`) in the console.
|
|
121
85
|
|
|
122
86
|
## Deploying to Vercel
|
|
123
87
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
- When you make changes to your python endpoints, you have to manually regenerate the `/api` folder by running:
|
|
127
|
-
1. `poetry export -f requirements.txt --output requirements.txt --without-hashes`
|
|
128
|
-
2. `node ./node_modules/sveltekit-python-vercel/esm/src/vite/sveltekit_python_vercel/bin.mjs`
|
|
129
|
-
- Then commit `requirements.txt` and the changes in `/api` and push.
|
|
88
|
+
Just push to your repository — no extra steps required.
|
|
130
89
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
...
|
|
137
|
-
"py-update": "poetry export -f requirements.txt --output requirements.txt --without-hashes; node ./node_modules/sveltekit-python-vercel/esm/src/vite/sveltekit_python_vercel/bin.mjs"
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
- and then just run `pnpm py-update`
|
|
90
|
+
- The `buildCommand` in `vercel.json` handles everything automatically:
|
|
91
|
+
1. `vite build` runs the SvelteKit build and writes `.vercel/output/` via `@sveltejs/adapter-vercel`
|
|
92
|
+
2. `bin.mjs` then writes your Python endpoints into `.vercel/output/functions/` using the [Build Output API](https://vercel.com/docs/build-output-api/v3) and patches the routing config
|
|
93
|
+
- Your `+server.py` files and dependency declarations (`requirements.txt`, `pyproject.toml`, `Pipfile`, etc.) are bundled automatically — there is no need to commit an `/api` folder or manually generate a `requirements.txt`.
|
|
94
|
+
- Python packages are pre-installed into the function bundle at build time using `pip install --target`, so they are available in Vercel's raw Python 3.12 Lambda environment without any extra configuration.
|
|
141
95
|
|
|
142
96
|
## Example
|
|
143
97
|
|
|
144
98
|
- Frontend: `/src/routes/py/+page.svelte`
|
|
145
99
|
|
|
146
100
|
```html
|
|
147
|
-
<script
|
|
148
|
-
let a = 0;
|
|
149
|
-
let b = 0;
|
|
150
|
-
let total = 0;
|
|
101
|
+
<script>
|
|
102
|
+
let a = $state(0);
|
|
103
|
+
let b = $state(0);
|
|
104
|
+
let total = $state(0);
|
|
151
105
|
|
|
152
106
|
async function pyAddPost() {
|
|
153
|
-
const
|
|
107
|
+
const res = await fetch("/py", {
|
|
154
108
|
method: "POST",
|
|
155
109
|
body: JSON.stringify({ a, b }),
|
|
156
|
-
headers: {
|
|
157
|
-
"content-type": "application/json",
|
|
158
|
-
},
|
|
110
|
+
headers: { "content-type": "application/json" },
|
|
159
111
|
});
|
|
160
|
-
|
|
161
|
-
total = res.sum;
|
|
112
|
+
total = (await res.json()).sum;
|
|
162
113
|
}
|
|
163
114
|
|
|
164
115
|
async function pyAddGet() {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
headers: {
|
|
168
|
-
"content-type": "application/json",
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
let res = await response.json();
|
|
173
|
-
total = res.sum;
|
|
116
|
+
const res = await fetch(`/py?a=${a}&b=${b}`);
|
|
117
|
+
total = (await res.json()).sum;
|
|
174
118
|
}
|
|
175
119
|
</script>
|
|
176
120
|
|
|
177
|
-
<h1>
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
<br />
|
|
188
|
-
|
|
189
|
-
<h3>GET Example</h3>
|
|
190
|
-
<form>
|
|
191
|
-
<input type="number" name="a" placeholder="Number 1" bind:value="{a}" />
|
|
192
|
-
<input type="number" name="b" placeholder="Number 2" bind:value="{b}" />
|
|
193
|
-
<button on:click|preventDefault="{pyAddGet}">Add</button>
|
|
194
|
-
</form>
|
|
195
|
-
<h4>Total: {total}</h4>
|
|
121
|
+
<h1>SvelteKit page with a Python backend</h1>
|
|
122
|
+
|
|
123
|
+
<label>a: <input type="number" bind:value={a} /></label>
|
|
124
|
+
<label>b: <input type="number" bind:value={b} /></label>
|
|
125
|
+
|
|
126
|
+
<button type="button" onclick={pyAddPost}>POST</button>
|
|
127
|
+
<button type="button" onclick={pyAddGet}>GET</button>
|
|
128
|
+
|
|
129
|
+
<p>Total: {total}</p>
|
|
196
130
|
```
|
|
197
131
|
|
|
198
132
|
- Backend: `/src/routes/py/+server.py`
|
|
@@ -212,17 +146,12 @@ Note:
|
|
|
212
146
|
|
|
213
147
|
async def GET(a: float, b: float):
|
|
214
148
|
return {"sum": a + b}
|
|
215
|
-
|
|
216
149
|
```
|
|
217
150
|
|
|
218
151
|
### Backend Caveats
|
|
219
152
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
- `GET` endpoints are directly fed the parameters from the url, so when you define an endpoint
|
|
223
|
-
- All other endpoints are fed the body as a JSON. The recommended way to deal with this is to use a pydantic model and pass it as the singular input to the function.
|
|
224
|
-
|
|
225
|
-
See the example above.
|
|
153
|
+
- `GET` endpoints receive query parameters directly as function arguments. Type annotations are used for coercion (e.g. `a: float` parses `?a=3` as `3.0`).
|
|
154
|
+
- All other HTTP methods receive the request body as JSON. The recommended pattern is a Pydantic model as the single argument — FastAPI handles validation and parsing automatically.
|
|
226
155
|
|
|
227
156
|
## Fork of `sveltekit-modal`
|
|
228
157
|
|
|
@@ -231,8 +160,8 @@ Check out the awesome [sveltekit-modal](https://github.com/semicognitive/sveltek
|
|
|
231
160
|
## Possible future plans
|
|
232
161
|
|
|
233
162
|
- [X] Add hot reloading in dev mode
|
|
234
|
-
- [
|
|
235
|
-
- [
|
|
163
|
+
- [X] Generate endpoints automatically during build (via Vercel Build Output API)
|
|
164
|
+
- [X] Auto-bundle requirements.txt / pyproject.toml / Pipfile at build time
|
|
236
165
|
- [ ] Add form actions
|
|
237
166
|
- [ ] Add load functions
|
|
238
|
-
- [ ] Add helper functions to automatically call API endpoints in project
|
|
167
|
+
- [ ] Add helper functions to automatically call API endpoints in project
|
package/esm/src/vite/mod.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { loadEnv } from "vite";
|
|
2
1
|
import { $ as run$, cd as cd$, which, path, chalk, } from "zx";
|
|
3
2
|
const get_pyServerEndpointAsString = (app_url, serve = false) => `
|
|
4
3
|
const handle = (method) => (async ({ request, fetch, url }) => {
|
|
@@ -11,7 +10,7 @@ const get_pyServerEndpointAsString = (app_url, serve = false) => `
|
|
|
11
10
|
if (${serve}) {
|
|
12
11
|
fullURL = new URL(url.pathname, new URL('${app_url}')) + url.search;
|
|
13
12
|
} else {
|
|
14
|
-
fullURL = new URL('/api' + url.pathname,
|
|
13
|
+
fullURL = new URL('/api' + url.pathname, url.origin) + url.search;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
console.log(\`PY: Reached python endpoint of \${method} \${fullURL}\`)
|
|
@@ -78,23 +77,7 @@ export async function sveltekit_python_vercel(opts = {}) {
|
|
|
78
77
|
name: "vite-plugin-sveltekit_python-build",
|
|
79
78
|
apply: "build",
|
|
80
79
|
async configResolved(config) {
|
|
81
|
-
console.log("PY: BUILD DEBUG");
|
|
82
80
|
console.log("PY: ROOT PATH: " + config.root);
|
|
83
|
-
console.log("PY: LOADED VERCEL URL: " + loadEnv("", config.root, "").VERCEL_URL);
|
|
84
|
-
const packagelocation = path.join(config.root, "node_modules", "sveltekit-python-vercel", "esm/src/vite");
|
|
85
|
-
console.log("PY: PACKAGE LOCATION: " + packagelocation);
|
|
86
|
-
const python_path = opts.python_path ?? (await which("python3"));
|
|
87
|
-
await run$ `cd ${packagelocation}`;
|
|
88
|
-
await run$ `${python_path} ${packagelocation}/sveltekit_python_vercel/build.py --root ${config.root}`;
|
|
89
|
-
// check if env var starts with http
|
|
90
|
-
let httpPrefix = "";
|
|
91
|
-
if (!loadEnv("", config.root, "").VERCEL_URL.startsWith("http")) {
|
|
92
|
-
httpPrefix = "https://";
|
|
93
|
-
}
|
|
94
|
-
const api_url = path.join(httpPrefix + loadEnv("", config.root, "").VERCEL_URL);
|
|
95
|
-
// get current Vercel deploy URL
|
|
96
|
-
sveltekit_url = new URL(api_url);
|
|
97
|
-
console.log("PY: Build API URL: " + sveltekit_url.toString());
|
|
98
81
|
},
|
|
99
82
|
};
|
|
100
83
|
const plugin_py_server_endpoint_serve = {
|
|
@@ -116,13 +99,10 @@ export async function sveltekit_python_vercel(opts = {}) {
|
|
|
116
99
|
name: "vite-plugin-sveltekit_python-server-endpoint",
|
|
117
100
|
apply: "build",
|
|
118
101
|
transform(src, id) {
|
|
119
|
-
// console.log("Transform function called for", id); // Add this line
|
|
120
102
|
if (/\.py$/.test(id)) {
|
|
121
|
-
if (sveltekit_url === undefined)
|
|
122
|
-
throw new Error(`${plugin_python_serve.name} failed to produce a sveltekit_url`);
|
|
123
103
|
return {
|
|
124
|
-
code: get_pyServerEndpointAsString(
|
|
125
|
-
map: null,
|
|
104
|
+
code: get_pyServerEndpointAsString(new URL("http://localhost"), false),
|
|
105
|
+
map: null,
|
|
126
106
|
};
|
|
127
107
|
}
|
|
128
108
|
},
|
|
@@ -1,45 +1,120 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import shutil
|
|
3
2
|
import glob
|
|
4
|
-
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
5
7
|
from pathlib import Path, PurePosixPath
|
|
6
8
|
|
|
7
|
-
parser = argparse.ArgumentParser(description="Run
|
|
8
|
-
parser.add_argument("--root", default=".", help="Root directory of
|
|
9
|
-
parser.add_argument("--packagedir", default=None, help="
|
|
9
|
+
parser = argparse.ArgumentParser(description="Run SvelteKit Python Deployment")
|
|
10
|
+
parser.add_argument("--root", default=".", help="Root directory of the SvelteKit project")
|
|
11
|
+
parser.add_argument("--packagedir", default=None, help="Directory of the sveltekit-python-vercel package")
|
|
10
12
|
args = parser.parse_args()
|
|
11
13
|
|
|
12
|
-
|
|
13
14
|
root_dir = Path(args.root).absolute()
|
|
14
|
-
api_dir = root_dir / "api"
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
func_dir = root_dir / ".vercel" / "output" / "functions" / "api" / "index.func"
|
|
17
|
+
func_dir.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
|
|
19
19
|
if args.packagedir:
|
|
20
|
-
shutil.copy(Path(args.packagedir).absolute() / "deploy.py",
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
shutil.copy(Path(args.packagedir).absolute() / "deploy.py", func_dir / "index.py")
|
|
21
|
+
|
|
22
|
+
# find all +server.py routes and copy them into the .func directory
|
|
23
|
+
manifest = []
|
|
24
|
+
|
|
25
|
+
for module_path in glob.glob(str(root_dir / "src/routes/**/+server.py"), recursive=True):
|
|
26
|
+
rel = Path(module_path).absolute().relative_to(root_dir / "src/routes")
|
|
27
|
+
|
|
29
28
|
# replace square brackets with curly brackets
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# remove any groups from the URL
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
29
|
+
rel = Path(str(rel).replace("[", "{").replace("]", "}"))
|
|
30
|
+
|
|
31
|
+
# remove any SvteleKit groups from the URL
|
|
32
|
+
parts = [
|
|
33
|
+
p for p in PurePosixPath(rel).parts
|
|
34
|
+
if not (p.startswith("(") and p.endswith(")"))
|
|
35
|
+
]
|
|
36
|
+
rel = Path(*parts)
|
|
37
|
+
|
|
38
|
+
target_dir = func_dir / rel.parent
|
|
39
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
shutil.copy(module_path, target_dir / rel.name)
|
|
42
|
+
|
|
43
|
+
if not (target_dir / "__init__.py").exists():
|
|
44
|
+
(target_dir / "__init__.py").touch()
|
|
45
|
+
|
|
46
|
+
# build the API route
|
|
47
|
+
parent = PurePosixPath(rel).parent
|
|
48
|
+
if str(parent) == ".":
|
|
49
|
+
api_route = "/api"
|
|
50
|
+
else:
|
|
51
|
+
api_route = "/api/" + str(parent)
|
|
52
|
+
|
|
53
|
+
manifest.append({"file": str(PurePosixPath(rel)), "route": api_route})
|
|
54
|
+
print(f"PYTHON ENDPOINT: {module_path} → {api_route}")
|
|
55
|
+
|
|
56
|
+
(func_dir / "_manifest.json").write_text(json.dumps(manifest, indent=2))
|
|
57
|
+
|
|
58
|
+
# bundle the dependency file here
|
|
59
|
+
dep_files = ["requirements.txt", "pyproject.toml", "uv.lock", "Pipfile", "Pipfile.lock"]
|
|
60
|
+
found_dep = False
|
|
61
|
+
for dep_file in dep_files:
|
|
62
|
+
src = root_dir / dep_file
|
|
63
|
+
if src.exists():
|
|
64
|
+
shutil.copy(src, func_dir / dep_file)
|
|
65
|
+
print(f"PYTHON ENDPOINT: Bundled {dep_file}")
|
|
66
|
+
found_dep = True
|
|
67
|
+
|
|
68
|
+
if not found_dep:
|
|
69
|
+
(func_dir / "requirements.txt").write_text("fastapi\nuvicorn\n")
|
|
70
|
+
print("PYTHON ENDPOINT: No dependency file found, created minimal requirements.txt")
|
|
71
|
+
|
|
72
|
+
# pre-install Python packages into _deps/ so they are available in the lambda env
|
|
73
|
+
dep_dir = func_dir / "_deps"
|
|
74
|
+
dep_dir.mkdir(exist_ok=True)
|
|
75
|
+
pip_cmd = [sys.executable, "-m", "pip", "install", "--target", str(dep_dir), "--quiet"]
|
|
76
|
+
if (func_dir / "requirements.txt").exists():
|
|
77
|
+
pip_cmd += ["-r", str(func_dir / "requirements.txt")]
|
|
78
|
+
subprocess.run(pip_cmd, check=True)
|
|
79
|
+
print("PYTHON ENDPOINT: Installed deps from requirements.txt")
|
|
80
|
+
elif (func_dir / "pyproject.toml").exists():
|
|
81
|
+
# extract dependencies from pyproject.toml
|
|
82
|
+
try:
|
|
83
|
+
import tomllib
|
|
84
|
+
except ImportError:
|
|
85
|
+
import tomli as tomllib # type: ignore
|
|
86
|
+
with open(func_dir / "pyproject.toml", "rb") as _f:
|
|
87
|
+
_pyproject = tomllib.load(_f)
|
|
88
|
+
_deps_list = _pyproject.get("project", {}).get("dependencies", [])
|
|
89
|
+
if _deps_list:
|
|
90
|
+
subprocess.run(pip_cmd + _deps_list, check=True)
|
|
91
|
+
print(f"PYTHON ENDPOINT: Installed {len(_deps_list)} deps from pyproject.toml")
|
|
92
|
+
|
|
93
|
+
# python3.12 is the AWS Lambda runtime string
|
|
94
|
+
vc_config = {"runtime": "python3.12", "handler": "index.handler"}
|
|
95
|
+
(func_dir / ".vc-config.json").write_text(json.dumps(vc_config, indent=2))
|
|
96
|
+
print("PYTHON ENDPOINT: Created .vc-config.json")
|
|
97
|
+
|
|
98
|
+
# patch the SvelteKit config.json that the endpoints are rerouted to python
|
|
99
|
+
config_path = root_dir / ".vercel" / "output" / "config.json"
|
|
100
|
+
if config_path.exists():
|
|
101
|
+
config = json.loads(config_path.read_text())
|
|
102
|
+
routes = config.get("routes", [])
|
|
103
|
+
|
|
104
|
+
python_route = {"src": "^/api(/.*)?$", "dest": "api/index"}
|
|
105
|
+
|
|
106
|
+
# routes /api/* to python before SvelteKit catches it
|
|
107
|
+
fs_idx = next(
|
|
108
|
+
(i for i, r in enumerate(routes) if r.get("handle") == "filesystem"),
|
|
109
|
+
0,
|
|
110
|
+
)
|
|
111
|
+
routes.insert(fs_idx, python_route)
|
|
112
|
+
config["routes"] = routes
|
|
113
|
+
|
|
114
|
+
config_path.write_text(json.dumps(config, indent=2))
|
|
115
|
+
print("PYTHON ENDPOINT: Patched .vercel/output/config.json")
|
|
116
|
+
else:
|
|
117
|
+
print(
|
|
118
|
+
"WARNING: .vercel/output/config.json not found. "
|
|
119
|
+
"Make sure to run `vite build` before this script."
|
|
120
|
+
)
|
|
@@ -1,55 +1,141 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import asyncio
|
|
2
|
+
import base64
|
|
3
3
|
import importlib.util
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
_base = Path(__file__).parent
|
|
10
|
+
_deps = _base / "_deps"
|
|
11
|
+
if _deps.exists() and str(_deps) not in sys.path:
|
|
12
|
+
sys.path.insert(0, str(_deps))
|
|
13
|
+
if str(_base) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(_base))
|
|
6
15
|
|
|
7
16
|
from fastapi import FastAPI, Request
|
|
8
17
|
from fastapi.responses import JSONResponse
|
|
9
18
|
|
|
10
19
|
app = FastAPI()
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
elif hasattr(mod, method):
|
|
44
|
-
app.add_api_route(api_route, getattr(mod, method), methods=[method])
|
|
45
|
-
print(f"PYTHON ENDPOINT: Added {module_path} [{method}] at {api_route}")
|
|
46
|
-
elif hasattr(mod, method.lower()):
|
|
47
|
-
app.add_api_route(api_route, getattr(mod, method.lower()), methods=[method])
|
|
48
|
-
print(f"PYTHON ENDPOINT: Added {module_path} [{method}] at {api_route}")
|
|
21
|
+
|
|
22
|
+
def _load_module(file_path: Path):
|
|
23
|
+
spec = importlib.util.spec_from_file_location("_route_module", file_path)
|
|
24
|
+
mod = importlib.util.module_from_spec(spec)
|
|
25
|
+
spec.loader.exec_module(mod)
|
|
26
|
+
return mod
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_manifest_path = _base / "_manifest.json"
|
|
30
|
+
if _manifest_path.exists():
|
|
31
|
+
for _entry in json.loads(_manifest_path.read_text()):
|
|
32
|
+
_mod = _load_module(_base / _entry["file"])
|
|
33
|
+
_route = _entry["route"]
|
|
34
|
+
|
|
35
|
+
for _method in ["GET", "POST", "PATCH", "PUT", "DELETE"]:
|
|
36
|
+
_has_upper = hasattr(_mod, _method)
|
|
37
|
+
_has_lower = hasattr(_mod, _method.lower())
|
|
38
|
+
|
|
39
|
+
if _has_upper and _has_lower:
|
|
40
|
+
raise Exception(
|
|
41
|
+
f"Duplicate method {_method} and {_method.lower()} in {_route}"
|
|
42
|
+
)
|
|
43
|
+
elif _has_upper:
|
|
44
|
+
app.add_api_route(_route, getattr(_mod, _method), methods=[_method])
|
|
45
|
+
print(f"PYTHON ENDPOINT: Registered {_method} {_route}")
|
|
46
|
+
elif _has_lower:
|
|
47
|
+
app.add_api_route(_route, getattr(_mod, _method.lower()), methods=[_method])
|
|
48
|
+
print(f"PYTHON ENDPOINT: Registered {_method} {_route}")
|
|
49
|
+
|
|
49
50
|
|
|
50
51
|
@app.exception_handler(Exception)
|
|
51
|
-
async def
|
|
52
|
-
return JSONResponse(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
async def _exception_handler(request: Request, exc: Exception):
|
|
53
|
+
return JSONResponse(status_code=500, content={"error": str(exc)})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _dispatch(scope: dict, body: bytes) -> tuple[int, dict, bytes]:
|
|
57
|
+
"""Run FastAPI for one HTTP request and collect the response."""
|
|
58
|
+
queue: asyncio.Queue = asyncio.Queue()
|
|
59
|
+
queue.put_nowait({"type": "http.request", "body": body, "more_body": False})
|
|
60
|
+
|
|
61
|
+
status = 500
|
|
62
|
+
resp_headers: dict = {}
|
|
63
|
+
resp_body = b""
|
|
64
|
+
|
|
65
|
+
async def receive():
|
|
66
|
+
return await queue.get()
|
|
67
|
+
|
|
68
|
+
async def send(message):
|
|
69
|
+
nonlocal status, resp_body
|
|
70
|
+
if message["type"] == "http.response.start":
|
|
71
|
+
status = message["status"]
|
|
72
|
+
for k, v in message.get("headers", []):
|
|
73
|
+
if isinstance(k, bytes):
|
|
74
|
+
k = k.decode()
|
|
75
|
+
if isinstance(v, bytes):
|
|
76
|
+
v = v.decode()
|
|
77
|
+
k = k.lower()
|
|
78
|
+
if k in resp_headers:
|
|
79
|
+
existing = resp_headers[k]
|
|
80
|
+
resp_headers[k] = (existing if isinstance(existing, list) else [existing]) + [v]
|
|
81
|
+
else:
|
|
82
|
+
resp_headers[k] = v
|
|
83
|
+
elif message["type"] == "http.response.body":
|
|
84
|
+
resp_body += message.get("body", b"")
|
|
85
|
+
|
|
86
|
+
await app(scope, receive, send)
|
|
87
|
+
return status, resp_headers, resp_body
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def handler(event, context):
|
|
91
|
+
payload = json.loads(event.get("body") or "{}")
|
|
92
|
+
|
|
93
|
+
# parse the path and query string
|
|
94
|
+
parsed = urlsplit(payload.get("path", "/"))
|
|
95
|
+
path = parsed.path or "/"
|
|
96
|
+
query = (parsed.query or "").encode()
|
|
97
|
+
|
|
98
|
+
# Build ASGI-style header list
|
|
99
|
+
raw_headers: dict = payload.get("headers") or {}
|
|
100
|
+
headers_list = []
|
|
101
|
+
host = payload.get("host", "localhost")
|
|
102
|
+
scheme = "https"
|
|
103
|
+
for k, v in raw_headers.items():
|
|
104
|
+
k_lower = k.lower()
|
|
105
|
+
if k_lower == "host":
|
|
106
|
+
host = v
|
|
107
|
+
if k_lower == "x-forwarded-proto":
|
|
108
|
+
scheme = v
|
|
109
|
+
headers_list.append((k_lower.encode(), str(v).encode()))
|
|
110
|
+
|
|
111
|
+
scope = {
|
|
112
|
+
"type": "http",
|
|
113
|
+
"http_version": "1.1",
|
|
114
|
+
"method": payload.get("method", "GET").upper(),
|
|
115
|
+
"path": path,
|
|
116
|
+
"raw_path": path.encode(),
|
|
117
|
+
"query_string": query,
|
|
118
|
+
"root_path": "",
|
|
119
|
+
"headers": headers_list,
|
|
120
|
+
"server": (host, 443 if scheme == "https" else 80),
|
|
121
|
+
"client": (raw_headers.get("x-real-ip", "127.0.0.1"), 0),
|
|
122
|
+
"scheme": scheme,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
body = payload.get("body") or b""
|
|
126
|
+
if payload.get("encoding") == "base64":
|
|
127
|
+
body = base64.b64decode(body)
|
|
128
|
+
elif isinstance(body, str):
|
|
129
|
+
body = body.encode()
|
|
130
|
+
|
|
131
|
+
status, resp_headers, resp_body = asyncio.run(_dispatch(scope, body))
|
|
132
|
+
|
|
133
|
+
result: dict = {"statusCode": status, "headers": resp_headers}
|
|
134
|
+
if resp_body:
|
|
135
|
+
try:
|
|
136
|
+
result["body"] = resp_body.decode("utf-8")
|
|
137
|
+
except UnicodeDecodeError:
|
|
138
|
+
result["body"] = base64.b64encode(resp_body).decode()
|
|
139
|
+
result["encoding"] = "base64"
|
|
140
|
+
|
|
141
|
+
return result
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"module": "./esm/mod.js",
|
|
3
3
|
"types": "./types/mod.d.ts",
|
|
4
4
|
"name": "sveltekit-python-vercel",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "1.0.3-beta.8b30fdb",
|
|
6
6
|
"description": "Write Sveltekit server endpoints in Python and seamlessly deploy to Vercel",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|