sveltekit-python-vercel 0.2.0 → 0.4.0

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
@@ -17,13 +17,12 @@ Write Python endpoints in [SvelteKit](https://kit.svelte.dev/) and seamlessly de
17
17
  - [Fork of `sveltekit-modal`](#fork-of-sveltekit-modal)
18
18
  - [Possible future plans](#possible-future-plans)
19
19
 
20
-
21
20
  **This is very much in beta.**
22
21
 
23
22
  ## Current Features
24
23
 
25
24
  - Write `+server.py` files nearly the same way you would write `+server.js` files
26
- - Deploy (quadi) automatically to Vercel Serverless
25
+ - Deploy (quasi) automatically to Vercel Serverless
27
26
 
28
27
  ## Installing
29
28
 
@@ -47,7 +46,7 @@ Write Python endpoints in [SvelteKit](https://kit.svelte.dev/) and seamlessly de
47
46
  - Update your `svelte.config.js`:
48
47
 
49
48
  ```javascript
50
- import adapter from "@sveltejs/adapter-vercel"; // Use the vercel adapter
49
+ import adapter from "@sveltejs/adapter-vercel"; // Use the vercel adapter
51
50
  import { vitePreprocess } from "@sveltejs/kit/vite";
52
51
 
53
52
  /** @type {import('@sveltejs/kit').Config} */
@@ -63,8 +62,10 @@ Write Python endpoints in [SvelteKit](https://kit.svelte.dev/) and seamlessly de
63
62
  ```
64
63
 
65
64
  - Update your `vercel.json`
65
+
66
66
  - The build command prepares all your endpoints and copies them to the `/api` directory where Vercel looks for functions
67
67
  - Functions and Routes tell Vercel how to run and redirect function calls
68
+
68
69
  ```json
69
70
  {
70
71
  "buildCommand": "node ./node_modules/sveltekit-python-vercel/esm/src/vite/sveltekit_python_vercel/bin.mjs; vite build",
@@ -85,7 +86,8 @@ Write Python endpoints in [SvelteKit](https://kit.svelte.dev/) and seamlessly de
85
86
  - Write some `+server.py` endpoints. See the example section below.
86
87
 
87
88
  ## Testing Locally
88
- Using [Poetry](https://python-poetry.org/) to manage your virtual environments with this package is recommended.
89
+
90
+ Using [Poetry](https://python-poetry.org/) to manage your virtual environments with this package is recommended.
89
91
 
90
92
  - Run `poetry init` to create a new virtual environment, and follow the steps. Or simply create a `pyproject.toml` like the one below.
91
93
 
@@ -99,14 +101,17 @@ Using [Poetry](https://python-poetry.org/) to manage your virtual environments w
99
101
 
100
102
  [tool.poetry.dependencies]
101
103
  python = "^3.9"
102
- fastapi = "^0.95.1"
104
+ fastapi = "^0.95.2"
103
105
  uvicorn = "^0.22.0"
104
-
106
+
107
+ [tool.poetry.group.dev.dependencies]
108
+ watchfiles = "^0.19.0"
105
109
 
106
110
  [build-system]
107
111
  requires = ["poetry-core"]
108
112
  build-backend = "poetry.core.masonry.api"
109
113
  ```
114
+
110
115
  - Required packages are python3.9 (that is what Vercel's runtime uses), `fastapi`, and `uvicorn`.
111
116
  - Install whatever other dependencies you need from pypi using `poetry add package-name`
112
117
 
@@ -115,6 +120,7 @@ Using [Poetry](https://python-poetry.org/) to manage your virtual environments w
115
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.
116
121
 
117
122
  ## Deploying to Vercel
123
+
118
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.
119
125
 
120
126
  - When you make changes to your python endpoints, you have to manually regenerate the `/api` folder by running:
@@ -122,8 +128,8 @@ Using [Poetry](https://python-poetry.org/) to manage your virtual environments w
122
128
  2. `node ./node_modules/sveltekit-python-vercel/esm/src/vite/sveltekit_python_vercel/bin.mjs`
123
129
  - Then commit `requirements.txt` and the changes in `/api` and push.
124
130
 
125
-
126
131
  Note:
132
+
127
133
  - To make this a bit smoother, you can add a script to you `package.json`:
128
134
  ```json
129
135
  "scripts": {
@@ -133,12 +139,10 @@ Note:
133
139
  ```
134
140
  - and then just run `pnpm py-update`
135
141
 
136
-
137
-
138
-
139
142
  ## Example
140
143
 
141
144
  - Frontend: `/src/routes/py/+page.svelte`
145
+
142
146
  ```html
143
147
  <script lang="ts">
144
148
  let a = 0;
@@ -146,12 +150,12 @@ Note:
146
150
  let total = 0;
147
151
 
148
152
  async function pyAddPost() {
149
- const response = await fetch('/py', {
150
- method: 'POST',
153
+ const response = await fetch("/py", {
154
+ method: "POST",
151
155
  body: JSON.stringify({ a, b }),
152
156
  headers: {
153
- 'content-type': 'application/json'
154
- }
157
+ "content-type": "application/json",
158
+ },
155
159
  });
156
160
  let res = await response.json();
157
161
  total = res.sum;
@@ -159,10 +163,10 @@ Note:
159
163
 
160
164
  async function pyAddGet() {
161
165
  const response = await fetch(`/py?a=${a}&b=${b}`, {
162
- method: 'GET',
166
+ method: "GET",
163
167
  headers: {
164
- 'content-type': 'application/json'
165
- }
168
+ "content-type": "application/json",
169
+ },
166
170
  });
167
171
 
168
172
  let res = await response.json();
@@ -174,9 +178,9 @@ Note:
174
178
 
175
179
  <h3>POST Example</h3>
176
180
  <form>
177
- <input type="number" name="a" placeholder="Number 1" bind:value={a} />
178
- <input type="number" name="b" placeholder="Number 2" bind:value={b} />
179
- <button on:click|preventDefault={pyAddPost}>Add</button>
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>
180
184
  </form>
181
185
  <h4>Total: {total}</h4>
182
186
 
@@ -184,14 +188,15 @@ Note:
184
188
 
185
189
  <h3>GET Example</h3>
186
190
  <form>
187
- <input type="number" name="a" placeholder="Number 1" bind:value={a} />
188
- <input type="number" name="b" placeholder="Number 2" bind:value={b} />
189
- <button on:click|preventDefault={pyAddGet}>Add</button>
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>
190
194
  </form>
191
195
  <h4>Total: {total}</h4>
192
196
  ```
193
197
 
194
198
  - Backend: `/src/routes/py/+server.py`
199
+
195
200
  ```python
196
201
  from pydantic import BaseModel
197
202
 
@@ -201,18 +206,19 @@ Note:
201
206
  b: float
202
207
 
203
208
 
204
- async def POST(numberSet: NumberSet):
205
- return {"sum": float(numberSet.a) + float(numberSet.b)}
209
+ async def POST(data: NumberSet):
210
+ return {"sum": data.a + data.b}
206
211
 
207
212
 
208
- async def GET(a, b):
209
- return {"sum": float(a) + float(b)}
213
+ async def GET(a: float, b: float):
214
+ return {"sum": a + b}
210
215
 
211
216
  ```
212
217
 
213
218
  ### Backend Caveats
214
219
 
215
220
  There are currently a few things that have to be worked around.
221
+
216
222
  - `GET` endpoints are directly fed the parameters from the url, so when you define an endpoint
217
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.
218
224
 
@@ -224,8 +230,9 @@ Check out the awesome [sveltekit-modal](https://github.com/semicognitive/sveltek
224
230
 
225
231
  ## Possible future plans
226
232
 
233
+ - [X] Add hot reloading in dev mode
227
234
  - [ ] Generate endpoints (/api folder) automatically during build
228
235
  - [ ] Auto create requirements.txt from pyproject.toml (both related to vercel functions being checked/handled before build)
229
236
  - [ ] Add form actions
230
237
  - [ ] Add load functions
231
- - [ ] Add helper functions to automatically call API endpoints in project
238
+ - [ ] Add helper functions to automatically call API endpoints in project\
@@ -14,10 +14,9 @@ const get_pyServerEndpointAsString = (app_url, serve = false) => `
14
14
  fullURL = new URL('/api' + url.pathname, new URL('${app_url}')) + url.search;
15
15
  }
16
16
 
17
- console.log(\`Reached python endpoint of \${method} \${fullURL}\`)
17
+ console.log(\`PY: Reached python endpoint of \${method} \${fullURL}\`)
18
18
  let requestBody = await request.clone().text();
19
- console.log(\`Body: \${requestBody}\`);
20
- console.log(\`Content-Type: \${request.headers.get('content-type')}\`);
19
+ console.log(\`PY: Body: \${requestBody}\`);
21
20
 
22
21
  if (method === 'GET') {
23
22
  requestBody = null;
@@ -79,11 +78,11 @@ export async function sveltekit_python_vercel(opts = {}) {
79
78
  name: "vite-plugin-sveltekit_python-build",
80
79
  apply: "build",
81
80
  async configResolved(config) {
82
- console.log("BUILD DEBUG");
83
- console.log("ROOT PATH: " + config.root);
84
- console.log("LOADED VERCEL URL: " + loadEnv("", config.root, "").VERCEL_URL);
81
+ console.log("PY: BUILD DEBUG");
82
+ console.log("PY: ROOT PATH: " + config.root);
83
+ console.log("PY: LOADED VERCEL URL: " + loadEnv("", config.root, "").VERCEL_URL);
85
84
  const packagelocation = path.join(config.root, "node_modules", "sveltekit-python-vercel", "esm/src/vite");
86
- console.log("PACKAGE LOCATION: " + packagelocation);
85
+ console.log("PY: PACKAGE LOCATION: " + packagelocation);
87
86
  const python_path = opts.python_path ?? (await which("python3"));
88
87
  await run$ `cd ${packagelocation}`;
89
88
  await run$ `${python_path} ${packagelocation}/sveltekit_python_vercel/build.py --root ${config.root}`;
@@ -95,7 +94,7 @@ export async function sveltekit_python_vercel(opts = {}) {
95
94
  const api_url = path.join(httpPrefix + loadEnv("", config.root, "").VERCEL_URL);
96
95
  // get current Vercel deploy URL
97
96
  sveltekit_url = new URL(api_url);
98
- console.log("Build API URL: " + sveltekit_url.toString());
97
+ console.log("PY: Build API URL: " + sveltekit_url.toString());
99
98
  },
100
99
  };
101
100
  const plugin_py_server_endpoint_serve = {
@@ -25,7 +25,8 @@ for module_path in glob.glob(str(root_dir / 'src/routes/**/+server.py'), recursi
25
25
 
26
26
  # replace the root_dir with api_dir
27
27
  api_route = api_dir / Path(module_path).absolute().relative_to(root_dir / "src/routes")
28
-
28
+ api_route = Path(str(api_route).replace('[', '{').replace(']', '}'))
29
+
29
30
  if not api_route.parent.exists():
30
31
  api_route.parent.mkdir(parents=True)
31
32
 
@@ -1,7 +1,7 @@
1
- import os
2
1
  import glob
3
2
  import importlib
4
3
  import importlib.util
4
+ import os
5
5
  from pathlib import Path
6
6
 
7
7
  from fastapi import FastAPI, Request
@@ -22,29 +22,27 @@ for module_path in glob.glob('./**/+server.py', recursive=True):
22
22
 
23
23
  api_route = module_path[1:] if module_path.startswith('./') else module_path
24
24
  api_route = str(Path(api_route).parent)
25
+
26
+ # Replace square brackets with curly brackets
27
+ api_route = api_route.replace('[', '{').replace(']', '}')
25
28
 
26
29
  mod = importlib.import_module(module_name, module_package)
27
-
28
- if hasattr(mod, 'GET'):
29
- app.add_api_route(api_route, mod.GET, methods=["GET"])
30
- print(f"PYTHON ENDPOINT: Added {module_path} [GET] at {api_route}")
31
-
32
- if hasattr(mod, 'POST'):
33
- app.add_api_route(api_route, mod.POST, methods=["POST"])
34
- print(f"PYTHON ENDPOINT: Added {module_path} [POST] at {api_route}")
35
-
36
- if hasattr(mod, 'PATCH'):
37
- app.add_api_route(api_route, mod.PATCH, methods=["PATCH"])
38
- print(f"PYTHON ENDPOINT: Added {module_path} [PATCH] at {api_route}")
39
-
40
- if hasattr(mod, 'PUT'):
41
- app.add_api_route(api_route, mod.PUT, methods=["PUT"])
42
- print(f"PYTHON ENDPOINT: Added {module_path} [PUT] at {api_route}")
43
-
44
- if hasattr(mod, 'DELETE'):
45
- app.add_api_route(api_route, mod.DELETE, methods=["DELETE"])
46
- print(f"PYTHON ENDPOINT: Added {module_path} [DELETE] at {api_route}")
47
-
30
+
31
+ # Add endpoints
32
+ for method in ["GET", "POST", "PATCH", "PUT", "DELETE"]:
33
+
34
+ # Check for duplicate methods
35
+ if hasattr(mod, method) and hasattr(mod, method.lower()):
36
+ raise Exception(
37
+ f"Duplicate method {method} and {method.lower()} in {api_route}"
38
+ )
39
+
40
+ elif hasattr(mod, method):
41
+ app.add_api_route(api_route, getattr(mod, method), methods=[method])
42
+ print(f"PYTHON ENDPOINT: Added {module_path} [{method}] at {api_route}")
43
+ elif hasattr(mod, method.lower()):
44
+ app.add_api_route(api_route, getattr(mod, method.lower()), methods=[method])
45
+ print(f"PYTHON ENDPOINT: Added {module_path} [{method}] at {api_route}")
48
46
 
49
47
  @app.exception_handler(Exception)
50
48
  async def unicorn_exception_handler(request: Request, exc: Exception):
@@ -1,14 +1,12 @@
1
- import uvicorn
2
1
  import argparse
3
- import os
2
+ import glob
4
3
  import importlib
5
4
  import importlib.util
6
- import glob
7
5
  import shutil
8
6
  from pathlib import Path
9
7
 
10
- from fastapi import FastAPI, Request
11
- from fastapi.responses import JSONResponse
8
+ import uvicorn
9
+ from fastapi import FastAPI
12
10
 
13
11
  parser = argparse.ArgumentParser(description="Run Sveltekit Python Server")
14
12
  parser.add_argument("--host", default="0.0.0.0", help="Server hostname")
@@ -18,59 +16,64 @@ args = parser.parse_args()
18
16
 
19
17
  app = FastAPI()
20
18
 
21
- def app_factory():
22
- return app # return the app object
19
+ root_dir = Path(args.root).absolute()
20
+
21
+ api_dir = Path("./sveltekit_python_vercel").absolute()
22
+
23
+ route_dir = root_dir.joinpath("src/routes")
24
+
25
+ watch_modules = [] # list of modules to watch for changes
26
+
27
+ for module_path in glob.glob(
28
+ route_dir.joinpath("**/+server.py").as_posix(), recursive=True
29
+ ):
30
+ abs_module_path = Path(module_path).absolute()
31
+
32
+ watch_modules.append(abs_module_path.parent.as_posix())
33
+
34
+ api_route = api_dir.joinpath(abs_module_path.relative_to(root_dir / "src/routes"))
35
+
36
+ if not api_route.parent.exists():
37
+ api_route.parent.mkdir(parents=True)
38
+
39
+ # copy module path to api_route
40
+ shutil.copy(module_path, api_route.parent)
41
+
42
+ module_name = api_route.stem
43
+
44
+ spec = importlib.util.spec_from_file_location(module_name, api_route)
45
+ mod = importlib.util.module_from_spec(spec)
46
+ spec.loader.exec_module(mod)
47
+
48
+ # Get the relative path of the module from the API directory
49
+ rel_path = api_route.relative_to(api_dir)
50
+
51
+ # Convert the relative path to a string and remove the file extension
52
+ api_path = f"/{rel_path.parent}"
53
+
54
+ # Replace square brackets with curly brackets
55
+ api_path = api_path.replace("[", "{").replace("]", "}")
56
+
57
+ # Add endpoints
58
+ for method in ["GET", "POST", "PATCH", "PUT", "DELETE"]:
59
+ # Check for duplicate methods
60
+ if hasattr(mod, method) and hasattr(mod, method.lower()):
61
+ raise Exception(
62
+ f"Duplicate method {method} and {method.lower()} in {api_route}"
63
+ )
64
+ elif hasattr(mod, method):
65
+ app.add_api_route(api_path, getattr(mod, method), methods=[method])
66
+ elif hasattr(mod, method.lower()):
67
+ app.add_api_route(api_path, getattr(mod, method.lower()), methods=[method])
68
+
23
69
 
24
70
  if __name__ == "__main__":
25
-
26
- root_dir = Path(args.root).absolute()
27
- api_dir = Path("./sveltekit_python_vercel").absolute()
28
-
29
- # Add all +server.py routes to web_app
30
- for module_path in glob.glob(str(root_dir / 'src/routes/**/+server.py'), recursive=True):
31
-
32
- # replace the root_dir with api_dir
33
- api_route = api_dir / Path(module_path).absolute().relative_to(root_dir/ "src/routes")
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
- print(f"PYTHON ENDPOINT: Copied {module_path} to {api_route.parent}")
41
-
42
- # Get the module name from the module path
43
- module_name = api_route.stem
44
-
45
- # Import the module dynamically
46
- spec = importlib.util.spec_from_file_location(module_name, api_route)
47
- mod = importlib.util.module_from_spec(spec)
48
- spec.loader.exec_module(mod)
49
-
50
- # Get the relative path of the module from the API directory
51
- rel_path = api_route.relative_to(api_dir)
52
- # Convert the relative path to a string and remove the file extension
53
- api_path = "/" + str(rel_path.parent)
54
-
55
- print("ADDING API PATH:", rel_path, api_path)
56
-
57
- if hasattr(mod, 'GET'):
58
- app.add_api_route(api_path, mod.GET, methods=["GET"])
59
-
60
- if hasattr(mod, 'POST'):
61
- app.add_api_route(api_path, mod.POST, methods=["POST"])
62
-
63
- if hasattr(mod, 'PATCH'):
64
- app.add_api_route(api_path, mod.PATCH, methods=["PATCH"])
65
-
66
- if hasattr(mod, 'PUT'):
67
- app.add_api_route(api_path, mod.PUT, methods=["PUT"])
68
-
69
- if hasattr(mod, 'DELETE'):
70
- app.add_api_route(api_path, mod.DELETE, methods=["DELETE"])
71
-
72
-
73
- config = uvicorn.Config(app_factory, host=args.host, port=args.port, log_level="info", factory=True)
74
- server = uvicorn.Server(config)
75
- print(f"Hosting on http://{args.host}:{args.port}")
76
- server.run()
71
+ uvicorn.run(
72
+ "sveltekit_python_vercel.serve:app",
73
+ host=args.host,
74
+ port=args.port,
75
+ log_level="info",
76
+ reload=True,
77
+ reload_includes=[*set(watch_modules)],
78
+ reload_excludes=[api_dir.as_posix()],
79
+ )
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.2.0",
5
+ "version": "v0.4.0",
6
6
  "description": "Write Sveltekit server endpoints in Python and seamlessly deploy to Vercel",
7
7
  "repository": {
8
8
  "type": "git",