sveltekit-python-vercel 0.4.1 → 1.0.3

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 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://camo.githubusercontent.com/f1ac9955f30176e6183aeeeac1b77354c7a132696fdc77c06ef0f0bec30f258c/68747470733a2f2f6861636b616461792e636f6d2f77702d636f6e74656e742f75706c6f6164732f323031392f30392f707974686f6e2d6c6f676f2e706e67"/>
4
- <img width="100" alt="image" src="https://camo.githubusercontent.com/add2c9721e333f0043ac938f3dadbc26a282776e01b95b308fcaba5afaf74ae3/68747470733a2f2f6173736574732e76657263656c2e636f6d2f696d6167652f75706c6f61642f76313538383830353835382f7265706f7369746f726965732f76657263656c2f6c6f676f2e706e67"/>
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 (quasi) automatically to Vercel Serverless
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(({ command, mode }) => {
40
- return {
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"; // Use the vercel adapter
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 prepares all your endpoints and copies them to the `/api` directory where Vercel looks for functions
67
- - Functions and Routes tell Vercel how to run and redirect function calls
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; vite build",
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
- Using [Poetry](https://python-poetry.org/) to manage your virtual environments with this package is recommended.
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
- - Enter your virtual env with `poetry shell`
119
- - Run `pnpm dev` or `npm dev`
120
- - You should see both the usual SvelteKit server start as well as the unvicorn server (by default on `http://0.0.0.0:8000`) in the console.
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
- - At the moment this requires a tiny bit of extra labor besides just pushing to your repository. I believe this is because of the way Vercel looks for serverless functions, but I hope to make this a bit easier in the future.
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
- Note:
132
-
133
- - To make this a bit smoother, you can add a script to you `package.json`:
134
- ```json
135
- "scripts": {
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 lang="ts">
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 response = await fetch("/py", {
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
- let res = await response.json();
161
- total = res.sum;
112
+ total = (await res.json()).sum;
162
113
  }
163
114
 
164
115
  async function pyAddGet() {
165
- const response = await fetch(`/py?a=${a}&b=${b}`, {
166
- method: "GET",
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>This is a SvelteKit page with a python backend.</h1>
178
-
179
- <h3>POST Example</h3>
180
- <form>
181
- <input type="number" name="a" placeholder="Number 1" bind:value="{a}" />
182
- <input type="number" name="b" placeholder="Number 2" bind:value="{b}" />
183
- <button on:click|preventDefault="{pyAddPost}">Add</button>
184
- </form>
185
- <h4>Total: {total}</h4>
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
- There are currently a few things that have to be worked around.
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
- - [ ] Generate endpoints (/api folder) automatically during build
235
- - [ ] Auto create requirements.txt from pyproject.toml (both related to vercel functions being checked/handled before build)
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
@@ -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, new URL('${app_url}')) + url.search;
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(sveltekit_url, false),
125
- map: null, // provide source map if available
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 Sveltekit Python Deployment")
8
- parser.add_argument("--root", default=".", help="Root directory of Vercel project")
9
- parser.add_argument("--packagedir", default=None, help="Root directory of Vercel project")
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
- if not api_dir.exists():
17
- api_dir.mkdir()
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", api_dir / "index.py")
21
-
22
-
23
- # Add all +server.py routes to web_app
24
- for module_path in glob.glob(str(root_dir / 'src/routes/**/+server.py'), recursive=True):
25
-
26
- # replace the root_dir with api_dir
27
- api_route = api_dir / Path(module_path).absolute().relative_to(root_dir / "src/routes")
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
- api_route = Path(str(api_route).replace('[', '{').replace(']', '}'))
31
-
32
- # remove any groups from the URL
33
- api_route = Path(str(PurePosixPath(*[part for part in PurePosixPath(api_route).parts if not part.startswith("(") and not part.endswith(")")])))
34
-
35
- if not api_route.parent.exists():
36
- api_route.parent.mkdir(parents=True)
37
-
38
- # copy module path to api_route
39
- shutil.copy(module_path, api_route.parent)
40
-
41
- # create __init__.py if it doesn't exist
42
- if not (api_route.parent / "__init__.py").exists():
43
- (api_route.parent / "__init__.py").touch()
44
-
45
- print(f"PYTHON ENDPOINT: Copied {module_path} to {api_route.parent}")
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 glob
2
- import importlib
1
+ import asyncio
2
+ import base64
3
3
  import importlib.util
4
- import os
5
- from pathlib import Path, PurePosixPath
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
- async def hello_world():
13
- return {"message": "Hello World!"}
14
-
15
- app.add_api_route("/api", hello_world, methods=["GET"])
16
-
17
- # Add all +server.py routes to web_app
18
- for module_path in glob.glob('./**/+server.py', recursive=True):
19
-
20
- module_name_joined = module_path[2:].replace(os.path.sep, '.')
21
- module_name, module_package = module_name_joined.rsplit('.', maxsplit=1)
22
-
23
- api_route = module_path[1:] if module_path.startswith('./') else module_path
24
- api_route = str(Path(api_route).parent)
25
-
26
- # Replace square brackets with curly brackets
27
- api_route = api_route.replace('[', '{').replace(']', '}')
28
-
29
- # remove any groups from the URL
30
- api_route = str(PurePosixPath(*[part for part in PurePosixPath(api_route).parts if not part.startswith("(") and not part.endswith(")")]))
31
-
32
- mod = importlib.import_module(module_name, module_package)
33
-
34
- # Add endpoints
35
- for method in ["GET", "POST", "PATCH", "PUT", "DELETE"]:
36
-
37
- # Check for duplicate methods
38
- if hasattr(mod, method) and hasattr(mod, method.lower()):
39
- raise Exception(
40
- f"Duplicate method {method} and {method.lower()} in {api_route}"
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 unicorn_exception_handler(request: Request, exc: Exception):
52
- return JSONResponse(
53
- status_code=500,
54
- content={"error": f"{exc}"},
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": "v0.4.1",
5
+ "version": "1.0.3",
6
6
  "description": "Write Sveltekit server endpoints in Python and seamlessly deploy to Vercel",
7
7
  "repository": {
8
8
  "type": "git",