coloco 0.1.1__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.
Files changed (40) hide show
  1. coloco-0.1.1/PKG-INFO +58 -0
  2. coloco-0.1.1/README.md +43 -0
  3. coloco-0.1.1/pyproject.toml +32 -0
  4. coloco-0.1.1/src/coloco/__init__.py +12 -0
  5. coloco-0.1.1/src/coloco/__main__.py +15 -0
  6. coloco-0.1.1/src/coloco/api.py +110 -0
  7. coloco-0.1.1/src/coloco/app.py +58 -0
  8. coloco-0.1.1/src/coloco/cli/api.py +48 -0
  9. coloco-0.1.1/src/coloco/cli/createapp.py +36 -0
  10. coloco-0.1.1/src/coloco/cli/dev.py +16 -0
  11. coloco-0.1.1/src/coloco/cli/node.py +49 -0
  12. coloco-0.1.1/src/coloco/cli/package.py +0 -0
  13. coloco-0.1.1/src/coloco/codegen.py +61 -0
  14. coloco-0.1.1/src/coloco/exceptions.py +32 -0
  15. coloco-0.1.1/src/coloco/lifespan.py +30 -0
  16. coloco-0.1.1/src/coloco/requirements.txt +6 -0
  17. coloco-0.1.1/src/coloco/static.py +12 -0
  18. coloco-0.1.1/src/coloco/templates/docker/.dockerignore-tpl +16 -0
  19. coloco-0.1.1/src/coloco/templates/docker/Dockerfile-tpl +59 -0
  20. coloco-0.1.1/src/coloco/templates/docker/docker-compose.yml-tpl +29 -0
  21. coloco-0.1.1/src/coloco/templates/standard/+app/404.svelte-tpl +1 -0
  22. coloco-0.1.1/src/coloco/templates/standard/+app/app.css-tpl +79 -0
  23. coloco-0.1.1/src/coloco/templates/standard/+app/app.svelte-tpl +25 -0
  24. coloco-0.1.1/src/coloco/templates/standard/+app/index.html-tpl +15 -0
  25. coloco-0.1.1/src/coloco/templates/standard/+app/index.svelte-tpl +1 -0
  26. coloco-0.1.1/src/coloco/templates/standard/+app/main.ts +9 -0
  27. coloco-0.1.1/src/coloco/templates/standard/+node/openapi-ts.config.ts-tpl +10 -0
  28. coloco-0.1.1/src/coloco/templates/standard/+node/package.json-tpl +24 -0
  29. coloco-0.1.1/src/coloco/templates/standard/+node/svelte.config.js-tpl +7 -0
  30. coloco-0.1.1/src/coloco/templates/standard/+node/tsconfig.json-tpl +43 -0
  31. coloco-0.1.1/src/coloco/templates/standard/+node/vite-env.d.ts-tpl +2 -0
  32. coloco-0.1.1/src/coloco/templates/standard/+node/vite.config.ts-tpl +34 -0
  33. coloco-0.1.1/src/coloco/templates/standard/.gitignore-tpl +6 -0
  34. coloco-0.1.1/src/coloco/templates/standard/README.md-tpl +3 -0
  35. coloco-0.1.1/src/coloco/templates/standard/example/api.py-tpl +6 -0
  36. coloco-0.1.1/src/coloco/templates/standard/example/index.svelte-tpl +7 -0
  37. coloco-0.1.1/src/coloco/templates/standard/main.py-tpl +3 -0
  38. coloco-0.1.1/src/coloco/templates/standard/requirements.txt-tpl +1 -0
  39. coloco-0.1.1/src/coloco/templates/standard/tsconfig.json-tpl +3 -0
  40. coloco-0.1.1/tests/__init__.py +0 -0
coloco-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.1
2
+ Name: coloco
3
+ Version: 0.1.1
4
+ Summary: A kit for creating FastAPI + Svelte applications
5
+ Author-Email: Channel Cat <channelcat@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: aerich[toml]==0.8.1
9
+ Requires-Dist: fastapi==0.115.0
10
+ Requires-Dist: PyJWT==2.10.1
11
+ Requires-Dist: tortoise-orm[asyncpg]==0.24.0
12
+ Requires-Dist: typer==0.15.2
13
+ Requires-Dist: uvicorn==0.31.0
14
+ Description-Content-Type: text/markdown
15
+
16
+ # Coloco
17
+
18
+ A kit for creating FastAPI + Svelte applications focusing on locality of code and decreased boilerplate. Create simple full-stack apps with built-in codegen. Deploy with a package that can be hosted with python or a docker container.
19
+
20
+ File-based routing for your front-end and back-end. Expose API endpoints with docs via `fastapi`. Generate a front-end with `svelte`.
21
+
22
+ Example:
23
+
24
+ `hello/api.py`
25
+ ```python
26
+ from coloco import api
27
+
28
+ @api
29
+ def test(name: str) -> str:
30
+ return f"Hello {name}!"
31
+
32
+ ```
33
+
34
+ `hello/index.svelte`
35
+ ```svelte
36
+ <script lang="ts">
37
+ import { test } from "./api";
38
+
39
+ const results = test({ query: { name: "DoItLive" } });
40
+ </script>
41
+
42
+ {#if $results.loading}
43
+ Loading...
44
+ {:else}
45
+ The server says {$results.data}
46
+ {/if}
47
+ ```
48
+
49
+ Serves the page `myapp.com/hello`, which calls `myapp.com/hello/test?name=DoItLive` and prints the message `Hello DoItLive!`
50
+
51
+ # Opinions
52
+
53
+ This framework is opinionated and combines the following tools/concepts:
54
+ * FastAPI
55
+ * Svelte
56
+ * openapi-ts (codegen)
57
+ * file-based routing (using svelte5-router)
58
+ * tortoise-orm (optional)
coloco-0.1.1/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # Coloco
2
+
3
+ A kit for creating FastAPI + Svelte applications focusing on locality of code and decreased boilerplate. Create simple full-stack apps with built-in codegen. Deploy with a package that can be hosted with python or a docker container.
4
+
5
+ File-based routing for your front-end and back-end. Expose API endpoints with docs via `fastapi`. Generate a front-end with `svelte`.
6
+
7
+ Example:
8
+
9
+ `hello/api.py`
10
+ ```python
11
+ from coloco import api
12
+
13
+ @api
14
+ def test(name: str) -> str:
15
+ return f"Hello {name}!"
16
+
17
+ ```
18
+
19
+ `hello/index.svelte`
20
+ ```svelte
21
+ <script lang="ts">
22
+ import { test } from "./api";
23
+
24
+ const results = test({ query: { name: "DoItLive" } });
25
+ </script>
26
+
27
+ {#if $results.loading}
28
+ Loading...
29
+ {:else}
30
+ The server says {$results.data}
31
+ {/if}
32
+ ```
33
+
34
+ Serves the page `myapp.com/hello`, which calls `myapp.com/hello/test?name=DoItLive` and prints the message `Hello DoItLive!`
35
+
36
+ # Opinions
37
+
38
+ This framework is opinionated and combines the following tools/concepts:
39
+ * FastAPI
40
+ * Svelte
41
+ * openapi-ts (codegen)
42
+ * file-based routing (using svelte5-router)
43
+ * tortoise-orm (optional)
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "coloco"
3
+ version = "0.1.1"
4
+ description = "A kit for creating FastAPI + Svelte applications"
5
+ authors = [
6
+ { name = "Channel Cat", email = "channelcat@gmail.com" },
7
+ ]
8
+ dependencies = [
9
+ "aerich[toml]==0.8.1",
10
+ "fastapi==0.115.0",
11
+ "PyJWT==2.10.1",
12
+ "tortoise-orm[asyncpg]==0.24.0",
13
+ "typer==0.15.2",
14
+ "uvicorn==0.31.0",
15
+ ]
16
+ requires-python = ">=3.11"
17
+ readme = "README.md"
18
+
19
+ [project.license]
20
+ text = "MIT"
21
+
22
+ [project.scripts]
23
+ coloco = "coloco:main"
24
+
25
+ [build-system]
26
+ requires = [
27
+ "pdm-backend",
28
+ ]
29
+ build-backend = "pdm.backend"
30
+
31
+ [tool.pdm]
32
+ distribution = true
@@ -0,0 +1,12 @@
1
+ from .app import create_app
2
+ from .codegen import generate_openapi_schema, generate_openapi_code
3
+ from .exceptions import UserError
4
+ from .api import api
5
+
6
+ __all__ = [
7
+ "api",
8
+ "create_app",
9
+ "generate_openapi_schema",
10
+ "generate_openapi_code",
11
+ "UserError",
12
+ ]
@@ -0,0 +1,15 @@
1
+ import typer
2
+ from .cli.dev import dev
3
+ from .cli.api import app as api_app
4
+ from .cli.node import app as node_app
5
+ from .cli.createapp import createapp
6
+
7
+ app = typer.Typer()
8
+
9
+ app.command()(dev)
10
+ app.add_typer(node_app, name="node")
11
+ app.add_typer(api_app, name="api")
12
+ app.command()(createapp)
13
+
14
+ if __name__ == "__main__":
15
+ app()
@@ -0,0 +1,110 @@
1
+ from contextlib import asynccontextmanager
2
+ from fastapi import APIRouter, FastAPI
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ import logging
5
+ from os import environ
6
+ from .codegen import (
7
+ custom_generate_unique_id,
8
+ generate_openapi_code,
9
+ generate_openapi_schema,
10
+ )
11
+ from .lifespan import execute_lifespan, register_lifespan
12
+ from .static import bind_static
13
+ from .exceptions import bind_exceptions
14
+
15
+
16
+ logging.basicConfig(level=logging.INFO)
17
+
18
+
19
+ @asynccontextmanager
20
+ async def generate_openapi(app: FastAPI):
21
+ logging.info("Generating OpenAPI schema...")
22
+ generate_openapi_schema(app)
23
+ generate_openapi_code(host=f"http://localhost:5172", diff_files=True)
24
+
25
+ yield
26
+
27
+
28
+ def create_api(is_dev: bool = False):
29
+ if not is_dev:
30
+ kwargs = {
31
+ "openapi_url": None,
32
+ "docs_url": None,
33
+ "redoc_url": None,
34
+ }
35
+ else:
36
+ kwargs = {
37
+ "lifespan": execute_lifespan,
38
+ }
39
+ register_lifespan(generate_openapi)
40
+
41
+ api = FastAPI(
42
+ generate_unique_id_function=custom_generate_unique_id,
43
+ **kwargs,
44
+ )
45
+ api.service = CORSMiddleware(
46
+ app=api,
47
+ allow_origins=[
48
+ f"http://localhost:5173",
49
+ "https://mysite.app",
50
+ ],
51
+ allow_credentials=True,
52
+ allow_methods=["*"],
53
+ allow_headers=["*"],
54
+ )
55
+
56
+ # bind_static(api)
57
+ bind_exceptions(api)
58
+
59
+ return api
60
+
61
+
62
+ # ========================= Global routing =========================
63
+
64
+ global_router = APIRouter()
65
+
66
+
67
+ def api(func):
68
+ # Auto-route
69
+ return _add_global_route([f"/{func.__name__}"], {}, func, "GET")
70
+
71
+
72
+ def _add_global_route(args, kwargs, func, method: str):
73
+ # Prepend module name to path
74
+ path = args[0] if args else kwargs.get("path", "")
75
+ path = (
76
+ # TODO: Make this configurable
77
+ "/api/"
78
+ + func.__module__.rsplit(".", 1)[0].replace(".", "/")
79
+ + ("" if path.startswith("/") else "/")
80
+ + path
81
+ )
82
+ if args:
83
+ args = (path, *args[1:])
84
+ else:
85
+ kwargs["path"] = path
86
+
87
+ return global_router.api_route(
88
+ *args,
89
+ **{
90
+ **kwargs,
91
+ "summary": (kwargs.get("summary", "") + f" ({func.__module__})").strip(),
92
+ "methods": [method],
93
+ },
94
+ )(func)
95
+
96
+
97
+ def _make_route_decorator(method: str):
98
+ def route_wrapper(*args, **kwargs):
99
+ def handler_wrapper(func):
100
+ return _add_global_route(args, kwargs, func, method)
101
+
102
+ return handler_wrapper
103
+
104
+ return route_wrapper
105
+
106
+
107
+ api.get = _make_route_decorator("GET")
108
+ api.post = _make_route_decorator("POST")
109
+ api.put = _make_route_decorator("PUT")
110
+ api.delete = _make_route_decorator("DELETE")
@@ -0,0 +1,58 @@
1
+ from .api import create_api, global_router
2
+ from dataclasses import dataclass
3
+ from fastapi import FastAPI
4
+ from importlib import import_module
5
+ import os
6
+ from rich import print
7
+
8
+
9
+ @dataclass
10
+ class ColocoApp:
11
+ api: FastAPI
12
+ name: str
13
+
14
+
15
+ import os
16
+
17
+
18
+ def find_api_files(directory):
19
+ api_files = []
20
+ try:
21
+ with os.scandir(directory) as entries:
22
+ for entry in entries:
23
+ if entry.is_dir():
24
+ # Skip directories starting with "+" and "node_modules"
25
+ if (
26
+ not entry.name.startswith("+")
27
+ and not entry.name == "node_modules"
28
+ and not entry.name == "coloco"
29
+ ):
30
+ api_files.extend(find_api_files(entry.path))
31
+ elif entry.is_file() and entry.name == "api.py":
32
+ api_files.append(entry.path)
33
+ except (PermissionError, FileNotFoundError) as e:
34
+ print(f"Error accessing {directory}: {e}")
35
+ return api_files
36
+
37
+
38
+ def create_app(name: str):
39
+ api = create_api(is_dev=True)
40
+
41
+ # Discover all api.py files from root, excluding node_modules and +app
42
+ api_files = find_api_files(".")
43
+ for api_file in api_files:
44
+ # convert python file path to module path
45
+ module_name = api_file.replace("./", "").replace(".py", "").replace("/", ".")
46
+ try:
47
+ module = import_module(module_name)
48
+ except Exception as e:
49
+ print(f"[red]Error importing '{api_file}': {e}[/red]")
50
+ continue
51
+ # if not hasattr(module, "router"):
52
+ # print(f"[red]Module '{api_file}' has no router[/red]")
53
+ # continue
54
+ # api.include_router(module.router)
55
+
56
+ api.include_router(global_router)
57
+
58
+ return ColocoApp(api=api, name=name)
@@ -0,0 +1,48 @@
1
+ from importlib import import_module
2
+ import typer
3
+ from rich import print
4
+ import uvicorn
5
+ from ..app import ColocoApp
6
+
7
+
8
+ app = typer.Typer()
9
+
10
+
11
+ @app.command()
12
+ def serve(
13
+ app: str,
14
+ host: str = "127.0.0.1",
15
+ port: int = 80,
16
+ log_level: str = "info",
17
+ reload=False,
18
+ ):
19
+ if not "." in app:
20
+ print(
21
+ "[red]App should be the name of a variable in a python file, example: main.py -> api = main.api[/red]"
22
+ )
23
+ raise typer.Abort()
24
+
25
+ module_name, var_name = app.rsplit(".", 1)
26
+ try:
27
+ module = import_module(module_name)
28
+ except ImportError:
29
+ print(f"[red]Module or python file {module_name} not found[/red]")
30
+ raise typer.Abort()
31
+
32
+ if not hasattr(module, var_name):
33
+ print(f"[red]Variable {var_name} not found in module {module_name}[/red]")
34
+ raise typer.Abort()
35
+
36
+ var = getattr(module, var_name)
37
+
38
+ if not isinstance(var, ColocoApp):
39
+ print(f"[red]{var_name} is not a ColocoApp. Please use create_app[/red]")
40
+ raise typer.Abort()
41
+
42
+ uvicorn.run(
43
+ f"{module_name}:{var_name}.api.service",
44
+ host=host,
45
+ port=port,
46
+ reload=reload,
47
+ log_level=log_level,
48
+ )
@@ -0,0 +1,36 @@
1
+ import typer
2
+ from rich import print
3
+ import os
4
+ import shutil
5
+
6
+
7
+ def createapp(name: str):
8
+ current_dir = os.path.dirname(os.path.abspath(__file__))
9
+ template_dir = f"{current_dir}/../templates/standard"
10
+ install_dir = f"{os.getcwd()}/{name}"
11
+ print(f"Creating app {name}...")
12
+ print(f"App created in {install_dir}")
13
+
14
+ template_vars = {
15
+ "project_name": name,
16
+ }
17
+
18
+ # Create directory
19
+ os.makedirs(install_dir, exist_ok=True)
20
+
21
+ # Copy all tpl files in folders and subfolders to install_dir under their relative paths
22
+ for root, dirs, files in os.walk(template_dir):
23
+ relative_path = os.path.relpath(root, template_dir)
24
+ install_subdir = os.path.join(install_dir, relative_path)
25
+ if not os.path.exists(install_subdir):
26
+ os.makedirs(install_subdir)
27
+ for file in files:
28
+ if file.endswith("-tpl"):
29
+ with open(os.path.join(root, file), "r") as f:
30
+ content = f.read()
31
+ for key, value in template_vars.items():
32
+ content = content.replace(f"{{{{ {key} }}}}", value)
33
+ with open(
34
+ os.path.join(install_dir, relative_path, file[:-4]), "w"
35
+ ) as f:
36
+ f.write(content)
@@ -0,0 +1,16 @@
1
+ from rich import print
2
+ from .api import serve
3
+ from subprocess import Popen
4
+
5
+
6
+ def dev(app: str, host: str = "127.0.0.1"):
7
+ node = Popen([f"npm run dev"], cwd="+node", shell=True)
8
+ serve(
9
+ app=app,
10
+ host=host,
11
+ port=5172,
12
+ reload=True,
13
+ log_level="debug",
14
+ )
15
+ node.terminate()
16
+ node.wait()
@@ -0,0 +1,49 @@
1
+ import typer
2
+ import os
3
+ from rich import print
4
+ import shutil
5
+ import subprocess
6
+
7
+ app = typer.Typer()
8
+
9
+
10
+ @app.command()
11
+ def install():
12
+ """Installs node dependencies for the project"""
13
+
14
+ # if not exists +node/package.json, raise error
15
+ if not os.path.exists("+node/package.json"):
16
+ print(
17
+ "[red]Error: +node/package.json not found. Please ensure you are in a coloco project directory.[/red]"
18
+ )
19
+ raise typer.Abort()
20
+
21
+ # copy +node/package.json to /package.json
22
+ shutil.copyfile("+node/package.json", "package.json")
23
+ if os.path.exists("+node/package-lock.json"):
24
+ shutil.copyfile("+node/package-lock.json", "package-lock.json")
25
+
26
+ try:
27
+ # run npm install
28
+ subprocess.run(["npm", "install"], cwd=".")
29
+
30
+ # move package.json and package-lock.json back to +node
31
+ shutil.move("package.json", "+node/package.json")
32
+ shutil.move("package-lock.json", "+node/package-lock.json")
33
+ except Exception as e:
34
+ print(f"[red]Error installing packages: {e}[/red]")
35
+ try:
36
+ os.remove("package.json")
37
+ os.remove("package-lock.json")
38
+ except Exception:
39
+ pass
40
+ raise typer.Abort()
41
+
42
+ print("[green]Packages installed successfully.[/green]")
43
+
44
+
45
+ @app.command()
46
+ def dev():
47
+ """Runs the node dev server"""
48
+ print("[green]Running node dev server...[/green]")
49
+ subprocess.run(["npm", "run", "dev"], cwd="+node")
File without changes
@@ -0,0 +1,61 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.routing import APIRoute
3
+ import json
4
+ from subprocess import run
5
+
6
+ import os
7
+ import shutil
8
+ import filecmp
9
+
10
+
11
+ def compare_and_copy(source_dir, target_dir):
12
+ """
13
+ Compares files in two directories and copies files from source to target if they differ, creating target directories if needed.
14
+
15
+ Args:
16
+ source_dir (str): Path to the source directory.
17
+ target_dir (str): Path to the target directory.
18
+ """
19
+ for root, _, files in os.walk(source_dir):
20
+ rel_path = os.path.relpath(root, source_dir) # Get relative path from source
21
+ target_path = os.path.join(target_dir, rel_path)
22
+ os.makedirs(
23
+ target_path, exist_ok=True
24
+ ) # Create target directory if it doesn't exist
25
+
26
+ for file in files:
27
+ source_file = os.path.join(root, file)
28
+ target_file = os.path.join(target_path, file)
29
+
30
+ if not os.path.exists(target_file) or not filecmp.cmp(
31
+ source_file, target_file
32
+ ):
33
+ shutil.copy2(source_file, target_file)
34
+
35
+
36
+ def custom_generate_unique_id(route: APIRoute) -> str:
37
+ return f"{route.name}"
38
+
39
+
40
+ def generate_openapi_schema(app: FastAPI, path="/tmp/openapi.json"):
41
+ with open(path, "w") as f:
42
+ json.dump(app.openapi(), f)
43
+
44
+
45
+ def generate_openapi_code(
46
+ host,
47
+ spec_path="/tmp/openapi.json",
48
+ output_dir="/app/+app/.generated/client",
49
+ diff_files=False,
50
+ ):
51
+ temp_dir = "/tmp/backend_api"
52
+
53
+ run(
54
+ f"npx openapi-ts "
55
+ f"--base {host} "
56
+ f"--input {spec_path} "
57
+ f"--output {temp_dir if diff_files else output_dir} ".split(),
58
+ cwd="/app/+node",
59
+ )
60
+ if diff_files:
61
+ compare_and_copy(temp_dir, output_dir)
@@ -0,0 +1,32 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.responses import JSONResponse
3
+ from fastapi.requests import Request
4
+
5
+
6
+ class UserError(Exception):
7
+ status_code: int
8
+ code: str
9
+
10
+ def __init__(self, *args: object, status_code=400, code="bad_request") -> None:
11
+ super().__init__(*args)
12
+ self.code = code
13
+
14
+
15
+ class ServerError(Exception):
16
+ pass
17
+
18
+
19
+ def bind_exceptions(api: FastAPI):
20
+ @api.exception_handler(UserError)
21
+ async def user_error_handler(request: Request, exc: UserError):
22
+ return JSONResponse(
23
+ status_code=exc.status_code,
24
+ content={"error": {"code": exc.code, "message": f"{exc}"}},
25
+ )
26
+
27
+ @api.exception_handler(Exception)
28
+ async def exception_handler(request: Request, exc: Exception):
29
+ return JSONResponse(
30
+ status_code=500,
31
+ content={"error": {"code": "api_error", "message": f"{exc}"}},
32
+ )
@@ -0,0 +1,30 @@
1
+ from contextlib import asynccontextmanager
2
+ from asyncio import iscoroutinefunction
3
+ from fastapi import FastAPI
4
+
5
+ lifespan_wrappers = []
6
+
7
+
8
+ @asynccontextmanager
9
+ async def execute_lifespan(app: FastAPI):
10
+ contexts = []
11
+
12
+ for lifespan_handler in lifespan_wrappers:
13
+ generator = lifespan_handler(app)
14
+ contexts.append(generator)
15
+ if hasattr(generator, "__aenter__"):
16
+ await generator.__aenter__()
17
+ else:
18
+ next(generator)
19
+
20
+ yield
21
+
22
+ while contexts and (generator := contexts.pop()):
23
+ if hasattr(generator, "__aexit__"):
24
+ await generator.__aexit__(None, None, None)
25
+ else:
26
+ next(generator)
27
+
28
+
29
+ def register_lifespan(handler):
30
+ lifespan_wrappers.append(handler)
@@ -0,0 +1,6 @@
1
+ aerich[toml]==0.8.1
2
+ fastapi==0.115.0
3
+ PyJWT==2.10.1
4
+ tortoise-orm[asyncpg]==0.24.0
5
+ typer==0.15.2
6
+ uvicorn==0.31.0
@@ -0,0 +1,12 @@
1
+ from fastapi import FastAPI
2
+ from fastapi.responses import FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+
5
+
6
+ # Static Files
7
+ def bind_static(api: FastAPI):
8
+ api.mount("/static", StaticFiles(directory="static"), name="static")
9
+
10
+ @api.get("/")
11
+ def index():
12
+ return FileResponse("static/index.html")
@@ -0,0 +1,16 @@
1
+ node_modules
2
+ dist
3
+
4
+ # OS
5
+ .DS_Store
6
+ Thumbs.db
7
+
8
+ # Env
9
+ .env
10
+ .env.*
11
+ !.env.example
12
+ !.env.test
13
+
14
+ # Vite
15
+ vite.config.js.timestamp-*
16
+ vite.config.ts.timestamp-*
@@ -0,0 +1,59 @@
1
+ # Dev Container
2
+
3
+ FROM python:3.12-alpine
4
+
5
+ RUN apk add --update nodejs npm
6
+
7
+ # Python Install
8
+ WORKDIR /app
9
+ ADD ./requirements.txt /app/requirements.txt
10
+ RUN python -m pip install -r /app/requirements.txt
11
+
12
+ # TODO: remove this when coloco is a package
13
+ ADD ./coloco/requirements.txt /app/coloco/requirements.txt
14
+ RUN python -m pip install -r /app/coloco/requirements.txt
15
+
16
+ # Node Install
17
+ RUN mkdir /.npm && chown -R 1000:1000 "/.npm"
18
+ ADD ./+node/package.json /app/package.json
19
+ ADD ./+node/package-lock.json /app/package-lock.json
20
+ RUN npm install
21
+
22
+ # Codegen
23
+ # RUN npm install -g @hey-api/client-fetch @hey-api/openapi-ts
24
+
25
+ ADD . /app
26
+
27
+ # ADD backend /api
28
+ # RUN python -c "from main import app; from coloco.codegen import generate_openapi_schema; generate_openapi_schema(app.api, '/tmp/openapi.json')"
29
+
30
+ # # Frontend Build
31
+
32
+ # FROM node:20-alpine as frontend
33
+
34
+ # WORKDIR /app
35
+
36
+ # ADD frontend/package.json /app/package.json
37
+ # ADD frontend/package-lock.json /app/package-lock.json
38
+
39
+ # RUN npm install
40
+
41
+ # COPY --from=backend /tmp/openapi.json /tmp/openapi.json
42
+
43
+ # RUN npx @hey-api/openapi-ts \
44
+ # --base https://mysite.app \
45
+ # --input /tmp/openapi.json \
46
+ # --output /app/src/backend \
47
+ # --client legacy/fetch
48
+
49
+ # ADD frontend /app
50
+ # RUN npm run build
51
+
52
+ # # Combined Image
53
+
54
+ # FROM backend
55
+
56
+ # COPY --from=frontend /app/dist /api/static
57
+ # ENV MODE=production
58
+
59
+ CMD ["python", "-m", "coloco", "start"]
@@ -0,0 +1,29 @@
1
+ services:
2
+ app:
3
+ build: .
4
+ volumes:
5
+ - ./:/app
6
+ - /app/node_modules
7
+ user: 1000:1000
8
+ ports:
9
+ - 5172:5172
10
+ - 5173:5173
11
+ entrypoint: python -m coloco
12
+ command: dev --host 0.0.0.0 main.app
13
+ tty: true
14
+
15
+ # Helper Scripts
16
+ install-local-node:
17
+ profiles: [ script ]
18
+ build: .
19
+ user: 1000:1000
20
+ volumes:
21
+ - ./:/app
22
+ command: python -m coloco node install
23
+
24
+ shell:
25
+ profiles: [ script ]
26
+ build: .
27
+ volumes:
28
+ - ./:/app
29
+ command: sh
@@ -0,0 +1,79 @@
1
+ :root {
2
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3
+ line-height: 1.5;
4
+ font-weight: 400;
5
+
6
+ color-scheme: light dark;
7
+ color: rgba(255, 255, 255, 0.87);
8
+ background-color: #242424;
9
+
10
+ font-synthesis: none;
11
+ text-rendering: optimizeLegibility;
12
+ -webkit-font-smoothing: antialiased;
13
+ -moz-osx-font-smoothing: grayscale;
14
+ }
15
+
16
+ a {
17
+ font-weight: 500;
18
+ color: #646cff;
19
+ text-decoration: inherit;
20
+ }
21
+ a:hover {
22
+ color: #535bf2;
23
+ }
24
+
25
+ body {
26
+ margin: 0;
27
+ display: flex;
28
+ place-items: center;
29
+ min-width: 320px;
30
+ min-height: 100vh;
31
+ }
32
+
33
+ h1 {
34
+ font-size: 3.2em;
35
+ line-height: 1.1;
36
+ }
37
+
38
+ .card {
39
+ padding: 2em;
40
+ }
41
+
42
+ #app {
43
+ max-width: 1280px;
44
+ margin: 0 auto;
45
+ padding: 2rem;
46
+ text-align: center;
47
+ }
48
+
49
+ button {
50
+ border-radius: 8px;
51
+ border: 1px solid transparent;
52
+ padding: 0.6em 1.2em;
53
+ font-size: 1em;
54
+ font-weight: 500;
55
+ font-family: inherit;
56
+ background-color: #1a1a1a;
57
+ cursor: pointer;
58
+ transition: border-color 0.25s;
59
+ }
60
+ button:hover {
61
+ border-color: #646cff;
62
+ }
63
+ button:focus,
64
+ button:focus-visible {
65
+ outline: 4px auto -webkit-focus-ring-color;
66
+ }
67
+
68
+ @media (prefers-color-scheme: light) {
69
+ :root {
70
+ color: #213547;
71
+ background-color: #ffffff;
72
+ }
73
+ a:hover {
74
+ color: #747bff;
75
+ }
76
+ button {
77
+ background-color: #f9f9f9;
78
+ }
79
+ }
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import { setupClient } from "@coloco/client";
3
+ import { route, Router, getRoutes } from "@coloco/router";
4
+
5
+ import Index from "./index.svelte";
6
+ import NotFound from "./404.svelte";
7
+
8
+ setupClient({
9
+ baseUrl: "http://localhost:5172",
10
+ });
11
+ const routes = getRoutes({
12
+ index: Index,
13
+ notFound: NotFound,
14
+ });
15
+ </script>
16
+
17
+ <main>
18
+ Main Page<br /><br />
19
+
20
+ <a use:route href="/">Home</a><br />
21
+ <a use:route href="/example">Example</a><br />
22
+ <a use:route href="/missing">Missing</a>
23
+
24
+ <Router basePath="/" {routes} />
25
+ </main>
@@ -0,0 +1,15 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>{{ project_name }}</title>
8
+ </head>
9
+
10
+ <body>
11
+ <div id="app"></div>
12
+ <script type="module" src="/main.ts"></script>
13
+ </body>
14
+
15
+ </html>
@@ -0,0 +1 @@
1
+ I am the home page!
@@ -0,0 +1,9 @@
1
+ import { mount } from 'svelte'
2
+ import './app.css'
3
+ import App from './app.svelte'
4
+
5
+ const app = mount(App, {
6
+ target: document.getElementById('app')!,
7
+ })
8
+
9
+ export default app
@@ -0,0 +1,10 @@
1
+ import { defaultPlugins } from '@hey-api/openapi-ts';
2
+ import { defineConfig } from '@coloco/codegen-svelte';
3
+
4
+ export default {
5
+ plugins: [
6
+ ...defaultPlugins,
7
+ '@hey-api/client-fetch',
8
+ defineConfig({ name: 'coloco-codegen', outputPath: './api' }),
9
+ ],
10
+ };
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{ project_name }}",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
11
+ },
12
+ "devDependencies": {
13
+ "@hey-api/client-fetch": "^0.8.2",
14
+ "@hey-api/openapi-ts": "^0.64.7",
15
+ "@mateothegreat/svelte5-router": "^0.1.38",
16
+ "@sveltejs/vite-plugin-svelte": "^5.0.3",
17
+ "@tsconfig/svelte": "^5.0.4",
18
+ "@types/node": "^22.13.4",
19
+ "svelte": "^5.19.6",
20
+ "svelte-check": "^4.1.4",
21
+ "typescript": "~5.7.2",
22
+ "vite": "^6.1.0"
23
+ }
24
+ }
@@ -0,0 +1,7 @@
1
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
2
+
3
+ export default {
4
+ // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5
+ // for more information about preprocessors
6
+ preprocess: vitePreprocess(),
7
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "compilerOptions": {
3
+ "rootDirs": [
4
+ "..",
5
+ "../+app/.generated/client/api"
6
+ ],
7
+ "verbatimModuleSyntax": true,
8
+ "isolatedModules": true,
9
+ "lib": [
10
+ "esnext",
11
+ "DOM",
12
+ "DOM.Iterable"
13
+ ],
14
+ "moduleResolution": "bundler",
15
+ "module": "esnext",
16
+ "noEmit": true,
17
+ "target": "esnext",
18
+ // Project
19
+ "allowJs": true,
20
+ "checkJs": true,
21
+ "esModuleInterop": true,
22
+ "forceConsistentCasingInFileNames": true,
23
+ "resolveJsonModule": true,
24
+ "skipLibCheck": true,
25
+ "sourceMap": true,
26
+ "strict": true,
27
+ "paths": {
28
+ "@api/*": [
29
+ "../+app/.generated/client/api/*"
30
+ ]
31
+ },
32
+ },
33
+ "include": [
34
+ "../**/*.ts",
35
+ "../**/*.js",
36
+ "../**/*.svelte",
37
+ "./vite.config.ts"
38
+ ],
39
+ "exclude": [
40
+ "../node_modules/**",
41
+ "../dist/**"
42
+ ]
43
+ }
@@ -0,0 +1,2 @@
1
+ /// <reference types="svelte" />
2
+ /// <reference types="vite/client" />
@@ -0,0 +1,34 @@
1
+ import path, { dirname, resolve, relative } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { defineConfig } from 'vite'
4
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const root = resolve(__dirname, '../');
8
+
9
+ // https://vite.dev/config/
10
+ export default defineConfig({
11
+ plugins: [svelte()],
12
+ root: resolve(root, '+app'),
13
+ server: {
14
+ host: "0.0.0.0",
15
+ port: 5172,
16
+ },
17
+ clearScreen: false,
18
+ resolve: {
19
+ // @ts-ignore
20
+ alias: [{
21
+ find: './api',
22
+ customResolver: (_, filePath: string) => {
23
+ const subFolder = path.relative(root, dirname(filePath));
24
+ const apiFile = resolve(root, `+app/.generated/client/api/${subFolder}/api.ts`);
25
+ return apiFile;
26
+ }
27
+ },
28
+ // @ts-ignore
29
+ {
30
+ find: '@api',
31
+ replacement: '/.generated/client/api'
32
+ }]
33
+ }
34
+ })
@@ -0,0 +1,6 @@
1
+ .env
2
+ node_modules
3
+ dist
4
+ +app/.generated
5
+ .vite
6
+ __pycache__
@@ -0,0 +1,3 @@
1
+ # {{ project_name }}
2
+
3
+ A kit for creating FastAPI + Svelte applications
@@ -0,0 +1,6 @@
1
+ from coloco import api
2
+
3
+
4
+ @api
5
+ def test(name: str) -> str:
6
+ return f"Hello {name}"
@@ -0,0 +1,7 @@
1
+ <script lang="ts">
2
+ import { test } from "./api";
3
+
4
+ const results = test({ query: { name: "Testy McTesterson" } });
5
+ </script>
6
+
7
+ Calling server... {$results.loading} - {$results.data} - {$results.error}
@@ -0,0 +1,3 @@
1
+ from coloco import create_app
2
+
3
+ app = create_app(name="{{ project_name }}")
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "./+node/tsconfig.json"
3
+ }
File without changes