coloco 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {coloco-0.2.0 → coloco-0.3.0}/PKG-INFO +43 -19
  2. coloco-0.3.0/README.md +73 -0
  3. {coloco-0.2.0 → coloco-0.3.0}/pyproject.toml +2 -1
  4. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/__main__.py +12 -0
  5. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/api.py +10 -8
  6. coloco-0.3.0/src/coloco/app.py +95 -0
  7. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/cli/api.py +43 -6
  8. coloco-0.3.0/src/coloco/cli/build.py +22 -0
  9. coloco-0.3.0/src/coloco/cli/db.py +100 -0
  10. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/cli/dev.py +5 -3
  11. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/cli/node.py +41 -6
  12. coloco-0.3.0/src/coloco/cli/serve.py +14 -0
  13. coloco-0.3.0/src/coloco/db.py +39 -0
  14. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/exceptions.py +11 -3
  15. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/lifespan.py +3 -1
  16. coloco-0.3.0/src/coloco/static.py +12 -0
  17. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+app/app.svelte-tpl +4 -4
  18. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+node/package.json-tpl +2 -2
  19. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+node/vite.config.ts-tpl +14 -0
  20. coloco-0.3.0/src/coloco/templates/standard/pyproject.toml-tpl +13 -0
  21. coloco-0.2.0/README.md +0 -50
  22. coloco-0.2.0/src/coloco/app.py +0 -58
  23. coloco-0.2.0/src/coloco/requirements.txt +0 -6
  24. coloco-0.2.0/src/coloco/static.py +0 -12
  25. coloco-0.2.0/src/coloco/templates/standard/requirements.txt-tpl +0 -1
  26. {coloco-0.2.0 → coloco-0.3.0}/LICENSE +0 -0
  27. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/__init__.py +0 -0
  28. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/cli/createapp.py +0 -0
  29. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/cli/package.py +0 -0
  30. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/codegen.py +0 -0
  31. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/docker/.dockerignore-tpl +0 -0
  32. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/docker/Dockerfile-tpl +0 -0
  33. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/docker/docker-compose.yml-tpl +0 -0
  34. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+app/404.svelte-tpl +0 -0
  35. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+app/app.css-tpl +0 -0
  36. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+app/index.html-tpl +0 -0
  37. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+app/index.svelte-tpl +0 -0
  38. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+app/main.ts-tpl +0 -0
  39. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+node/openapi-ts.config.ts-tpl +0 -0
  40. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+node/svelte.config.js-tpl +0 -0
  41. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+node/tsconfig.json-tpl +0 -0
  42. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/+node/vite-env.d.ts-tpl +0 -0
  43. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/.gitignore-tpl +0 -0
  44. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/README.md-tpl +0 -0
  45. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/example/api.py-tpl +0 -0
  46. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/example/index.svelte-tpl +0 -0
  47. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/main.py-tpl +0 -0
  48. {coloco-0.2.0 → coloco-0.3.0}/src/coloco/templates/standard/tsconfig.json-tpl +0 -0
  49. {coloco-0.2.0 → coloco-0.3.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: coloco
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A kit for creating FastAPI + Svelte applications
5
5
  Author-Email: Channel Cat <channelcat@gmail.com>
6
6
  License: MIT
@@ -9,17 +9,19 @@ Requires-Dist: aerich[toml]==0.8.1
9
9
  Requires-Dist: fastapi==0.115.0
10
10
  Requires-Dist: PyJWT==2.10.1
11
11
  Requires-Dist: tortoise-orm[asyncpg]==0.24.0
12
+ Requires-Dist: type-less==0.1.4
12
13
  Requires-Dist: typer==0.15.2
13
14
  Requires-Dist: uvicorn==0.31.0
14
15
  Description-Content-Type: text/markdown
15
16
 
16
17
  # Coloco
17
18
 
18
- A kit for creating full-stack apps with co-located code, built on FastAPI and Svelte. Bundle your front-end and back-end code and easily tie them together with codegen.
19
+ A kit for creating full-stack apps with co-located code, built on FastAPI and Svelte. Bundle your front-end and back-end code and easily tie them together with codegen.
19
20
 
20
21
  Example:
21
22
 
22
23
  `hello/api.py`
24
+
23
25
  ```python
24
26
  from coloco import api
25
27
 
@@ -30,36 +32,58 @@ def test(name: str) -> str:
30
32
  ```
31
33
 
32
34
  `hello/index.svelte`
35
+
33
36
  ```svelte
34
37
  <script lang="ts">
35
38
  import { test } from "./api";
36
-
37
- const response = test({ query: { name: "Coloco" } });
38
39
  </script>
39
40
 
40
- {#if $response.loading}
41
- Loading...
42
- {:else}
43
- The server says {$response.data}
44
- {/if}
41
+ {#await test({ name: "Coloco" })}
42
+ Loading...
43
+ {:then result}
44
+ The server says {result}
45
+ {/await}
45
46
  ```
46
47
 
47
48
  Serves the page `myapp.com/hello`, which calls `myapp.com/hello/test?name=Coloco` and prints the message `Hello Coloco!`
48
49
 
50
+ # Getting Started
51
+
52
+ - `pip install coloco`
53
+ - `coloco createapp myapp`
54
+ - From `myapp` - `coloco dev`
55
+
56
+ # Running in Production
57
+
58
+ - `coloco build`
59
+
60
+ - `coloco serve`
61
+
49
62
  # Opinions
50
63
 
51
64
  This framework is opinionated and combines the following excellent tools:
52
- * FastAPI
53
- * Svelte
54
- * openapi-ts (codegen)
55
- * svelte5-router (file-based routing)
56
- * tortoise-orm (optional)
65
+
66
+ - FastAPI
67
+ - Svelte
68
+ - openapi-ts (codegen)
69
+ - svelte5-router (file-based routing)
70
+ - tortoise-orm (optional)
57
71
 
58
72
  # Plans
59
- * Deploy tools
60
- * ORM Support
61
- * SQLite/Postgres support
73
+
74
+ - Deploy tools
75
+ - ORM Support
76
+ - SQLite/Postgres support
77
+ - Package/share modules with git
62
78
 
63
79
  # Dreams
64
- * Move `node_modules` into `+node`
65
- * Remove the need for `query`, `params`, `body` for simple REST requests (maybe use legacy client but support the new one?)
80
+
81
+ - Move `node_modules` into `+node`
82
+ - Somehow clean up litter from `uv`
83
+
84
+ # TODO
85
+
86
+ - Swap to `cyclopts` for CLI
87
+ - Swap to `copier` for template creation
88
+ - `Textual` CLI app / installer / dev tools?
89
+ - Common dev `admin` module
coloco-0.3.0/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Coloco
2
+
3
+ A kit for creating full-stack apps with co-located code, built on FastAPI and Svelte. Bundle your front-end and back-end code and easily tie them together with codegen.
4
+
5
+ Example:
6
+
7
+ `hello/api.py`
8
+
9
+ ```python
10
+ from coloco import api
11
+
12
+ @api
13
+ def test(name: str) -> str:
14
+ return f"Hello {name}!"
15
+
16
+ ```
17
+
18
+ `hello/index.svelte`
19
+
20
+ ```svelte
21
+ <script lang="ts">
22
+ import { test } from "./api";
23
+ </script>
24
+
25
+ {#await test({ name: "Coloco" })}
26
+ Loading...
27
+ {:then result}
28
+ The server says {result}
29
+ {/await}
30
+ ```
31
+
32
+ Serves the page `myapp.com/hello`, which calls `myapp.com/hello/test?name=Coloco` and prints the message `Hello Coloco!`
33
+
34
+ # Getting Started
35
+
36
+ - `pip install coloco`
37
+ - `coloco createapp myapp`
38
+ - From `myapp` - `coloco dev`
39
+
40
+ # Running in Production
41
+
42
+ - `coloco build`
43
+
44
+ - `coloco serve`
45
+
46
+ # Opinions
47
+
48
+ This framework is opinionated and combines the following excellent tools:
49
+
50
+ - FastAPI
51
+ - Svelte
52
+ - openapi-ts (codegen)
53
+ - svelte5-router (file-based routing)
54
+ - tortoise-orm (optional)
55
+
56
+ # Plans
57
+
58
+ - Deploy tools
59
+ - ORM Support
60
+ - SQLite/Postgres support
61
+ - Package/share modules with git
62
+
63
+ # Dreams
64
+
65
+ - Move `node_modules` into `+node`
66
+ - Somehow clean up litter from `uv`
67
+
68
+ # TODO
69
+
70
+ - Swap to `cyclopts` for CLI
71
+ - Swap to `copier` for template creation
72
+ - `Textual` CLI app / installer / dev tools?
73
+ - Common dev `admin` module
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "coloco"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "A kit for creating FastAPI + Svelte applications"
5
5
  authors = [
6
6
  { name = "Channel Cat", email = "channelcat@gmail.com" },
@@ -10,6 +10,7 @@ dependencies = [
10
10
  "fastapi==0.115.0",
11
11
  "PyJWT==2.10.1",
12
12
  "tortoise-orm[asyncpg]==0.24.0",
13
+ "type-less==0.1.4",
13
14
  "typer==0.15.2",
14
15
  "uvicorn==0.31.0",
15
16
  ]
@@ -3,13 +3,25 @@ from .cli.dev import dev
3
3
  from .cli.api import app as api_app
4
4
  from .cli.node import app as node_app
5
5
  from .cli.createapp import createapp
6
+ from .cli.build import build
7
+ from .cli.serve import serve
6
8
 
7
9
  app = typer.Typer()
8
10
 
9
11
  app.command()(dev)
10
12
  app.add_typer(node_app, name="node")
11
13
  app.add_typer(api_app, name="api")
14
+ app.command()(build)
12
15
  app.command()(createapp)
16
+ app.command()(serve)
17
+
18
+ # Add DB if tortoise/aerich are installed
19
+ try:
20
+ from .cli.db import app as db_app
21
+
22
+ app.add_typer(db_app, name="db")
23
+ except ImportError:
24
+ pass
13
25
 
14
26
  if __name__ == "__main__":
15
27
  app()
@@ -1,16 +1,16 @@
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
1
  from .codegen import (
7
2
  custom_generate_unique_id,
8
3
  generate_openapi_code,
9
4
  generate_openapi_schema,
10
5
  )
11
- from .lifespan import execute_lifespan, register_lifespan
12
- from .static import bind_static
6
+ from contextlib import asynccontextmanager
13
7
  from .exceptions import bind_exceptions
8
+ from fastapi import APIRouter, FastAPI
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from .lifespan import execute_lifespan, register_lifespan
11
+ import logging
12
+ from os import environ
13
+ from type_less import fill_type_hints
14
14
 
15
15
 
16
16
  logging.basicConfig(level=logging.INFO)
@@ -54,7 +54,7 @@ def create_api(is_dev: bool = False):
54
54
  )
55
55
 
56
56
  # bind_static(api)
57
- bind_exceptions(api)
57
+ bind_exceptions(api, debug=is_dev)
58
58
 
59
59
  return api
60
60
 
@@ -70,6 +70,8 @@ def api(func):
70
70
 
71
71
 
72
72
  def _add_global_route(args, kwargs, func, method: str):
73
+ fill_type_hints(func, use_literals=True)
74
+
73
75
  # Prepend module name to path
74
76
  path = args[0] if args else kwargs.get("path", "")
75
77
  path = (
@@ -0,0 +1,95 @@
1
+ from .api import create_api, global_router
2
+ from dataclasses import dataclass
3
+ from .db import get_orm_config, lifecycle_connect_database
4
+ from fastapi import FastAPI
5
+ from importlib import import_module
6
+ from .lifespan import register_lifespan
7
+ import os
8
+ from rich import print
9
+ from .static import bind_static
10
+ import traceback
11
+ from typing import Literal
12
+
13
+
14
+ @dataclass
15
+ class ColocoApp:
16
+ api: FastAPI
17
+ name: str
18
+ database_url: str = None
19
+ orm_config: dict = None
20
+ migrations_dir: str = "./+migrations"
21
+
22
+
23
+ def discover_files(directory, name):
24
+ api_files = []
25
+ try:
26
+ with os.scandir(directory) as entries:
27
+ for entry in entries:
28
+ if entry.is_dir():
29
+ # Skip directories starting with "+" and "node_modules"
30
+ if (
31
+ not entry.name.startswith("+")
32
+ and not entry.name.startswith("-")
33
+ and not entry.name.startswith(".")
34
+ and not entry.name == "node_modules"
35
+ and not entry.name == "coloco"
36
+ ):
37
+ api_files.extend(discover_files(entry.path, name))
38
+ elif entry.is_file() and entry.name == name:
39
+ api_files.append(entry.path)
40
+ except (PermissionError, FileNotFoundError) as e:
41
+ print(f"Error accessing {directory}: {e}")
42
+ return api_files
43
+
44
+
45
+ CURRENT_APP = None
46
+
47
+
48
+ def create_app(name: str, database_url: str = None) -> ColocoApp:
49
+ global CURRENT_APP
50
+ if CURRENT_APP:
51
+ raise ValueError("Coloco app already created")
52
+
53
+ mode: Literal["dev", "prod"] = os.environ.get("COLOCO_MODE", "dev")
54
+ api = create_api(is_dev=mode == "dev")
55
+
56
+ # Discover all api.py files from root, excluding node_modules and +app
57
+ api_files = discover_files(".", name="api.py")
58
+ for api_file in api_files:
59
+ # convert python file path to module path
60
+ module_name = api_file.replace("./", "").replace(".py", "").replace("/", ".")
61
+ try:
62
+ module = import_module(module_name)
63
+ except Exception as e:
64
+ print(f"[red]Error importing '{api_file}': {e}[/red]")
65
+ print(traceback.format_exc())
66
+ continue
67
+
68
+ api.include_router(global_router)
69
+ from fastapi.staticfiles import StaticFiles
70
+
71
+ api.mount("/assets", StaticFiles(directory="dist/app/assets"), name="assets")
72
+
73
+ # Production mode serves dist
74
+ if mode == "prod":
75
+ bind_static(api)
76
+
77
+ # Setup Database
78
+ if database_url:
79
+ orm_config = get_orm_config(
80
+ database_url, model_files=discover_files(".", name="models.py")
81
+ )
82
+ register_lifespan(lifecycle_connect_database)
83
+ else:
84
+ orm_config = None
85
+
86
+ CURRENT_APP = ColocoApp(
87
+ api=api, name=name, database_url=database_url, orm_config=orm_config
88
+ )
89
+ return CURRENT_APP
90
+
91
+
92
+ def get_current_app() -> ColocoApp:
93
+ if not CURRENT_APP:
94
+ raise ValueError("Coloco app not created")
95
+ return CURRENT_APP
@@ -1,22 +1,24 @@
1
1
  from ..app import ColocoApp
2
+ from ..codegen import generate_openapi_schema, generate_openapi_code
2
3
  from importlib import import_module
3
4
  import os
4
5
  from rich import print
5
6
  import sys
6
7
  import typer
8
+ from typing import Literal
7
9
  import uvicorn
8
10
 
9
11
 
10
12
  app = typer.Typer()
11
13
 
12
14
 
13
- def _verify_app(app: str):
15
+ def _verify_app(app: str = "main.app") -> ColocoApp:
14
16
  if not "." in app:
15
17
  print(
16
18
  "[red]App should be the name of a variable in a python file, example: main.py -> api = main.api[/red]"
17
19
  )
18
20
  raise typer.Abort()
19
-
21
+
20
22
  module_name, var_name = app.rsplit(".", 1)
21
23
  try:
22
24
  # Needed for when running the binary
@@ -35,17 +37,37 @@ def _verify_app(app: str):
35
37
  if not isinstance(var, ColocoApp):
36
38
  print(f"[red]{var_name} is not a ColocoApp. Please use create_app[/red]")
37
39
  raise typer.Abort()
38
-
39
- return True
40
+
41
+ return var
42
+
43
+
44
+ def _verify_is_packaged():
45
+ dist_dir = os.path.join(os.getcwd(), "dist")
46
+ app_dir = os.path.join(dist_dir, "app")
47
+
48
+ # Verify dist dir exists
49
+ if not os.path.exists(dist_dir):
50
+ print(
51
+ f"[red]Dist dir {dist_dir} does not exist. Run [green]coloco build[/green] to package the app.[/red]"
52
+ )
53
+ raise typer.Abort()
54
+ if not os.path.exists(app_dir):
55
+ print(
56
+ f"[red]App is missing from package directory {app_dir}. Run [green]coloco build[/green] to package the app.[/red]"
57
+ )
58
+ raise typer.Abort()
59
+
40
60
 
41
61
  def _serve(
42
62
  app: str = "main.app",
43
63
  host: str = "127.0.0.1",
44
64
  port: int = 80,
45
65
  log_level: str = "info",
66
+ mode: Literal["dev", "prod"] = "prod",
46
67
  reload=False,
47
68
  ):
48
69
  module_name, var_name = app.rsplit(".", 1)
70
+ os.environ["COLOCO_MODE"] = mode
49
71
  uvicorn.run(
50
72
  f"{module_name}:{var_name}.api.service",
51
73
  host=host,
@@ -54,13 +76,28 @@ def _serve(
54
76
  log_level=log_level,
55
77
  )
56
78
 
79
+
57
80
  @app.command()
58
81
  def serve(
59
82
  app: str = "main.app",
60
83
  host: str = "127.0.0.1",
61
84
  port: int = 80,
62
85
  log_level: str = "info",
63
- reload=False,
86
+ mode="prod",
87
+ reload: bool = False,
64
88
  ):
65
89
  _verify_app(app)
66
- _serve(app=app, host=host, port=port, log_level=log_level, reload=reload)
90
+ if mode == "prod":
91
+ _verify_is_packaged()
92
+ _serve(app=app, host=host, port=port, log_level=log_level, mode=mode, reload=reload)
93
+
94
+
95
+ @app.command()
96
+ def codegen(
97
+ app: str = "main.app",
98
+ ):
99
+ coloco_app = _verify_app(app)
100
+ print("Generating OpenAPI code...")
101
+ generate_openapi_schema(coloco_app.api)
102
+ generate_openapi_code(host="http://localhost:5172")
103
+ print("[green]OpenAPI code generated successfully.[/green]")
@@ -0,0 +1,22 @@
1
+ from rich import print
2
+ from .api import _verify_app, codegen
3
+ from .node import build as build_node
4
+ import os
5
+
6
+
7
+ def build(
8
+ app: str = "main.app",
9
+ ):
10
+ _verify_app(app)
11
+ dist_dir = f"{os.getcwd()}/dist"
12
+ print(f"Packaging {app}...")
13
+
14
+ # Codegen API
15
+ codegen(app)
16
+ # Build node app
17
+ build_node(dir=dist_dir)
18
+
19
+ print(
20
+ f"App packaged into {dist_dir}.\n"
21
+ f"Run [green]coloco serve[/green] to start the app in production mode."
22
+ )
@@ -0,0 +1,100 @@
1
+ from aerich import Command
2
+ from .api import _verify_app
3
+ from ..app import ColocoApp, get_current_app
4
+ from asyncio import run
5
+ import functools
6
+ import os
7
+ from rich import print
8
+ from tortoise import Tortoise
9
+ import typer
10
+
11
+ app = typer.Typer()
12
+
13
+ def get_app():
14
+ _verify_app()
15
+ return get_current_app()
16
+
17
+ async def get_command(app: ColocoApp = None, init: bool = True):
18
+ app = app or get_app()
19
+ command = Command(
20
+ tortoise_config=app.orm_config,
21
+ app="models",
22
+ location=app.migrations_dir,
23
+ )
24
+ if init:
25
+ await command.init()
26
+ return command
27
+
28
+ def migrations_dir_exists(migrations_dir: str):
29
+ migrations_dir = os.path.join(migrations_dir, "models")
30
+ return os.path.exists(migrations_dir)
31
+
32
+ def ensure_migrations_dir(migrations_dir: str):
33
+ if not migrations_dir_exists(migrations_dir):
34
+ print("[red]Please run `coloco db init` first to initialize the database.[/red]")
35
+ raise typer.Exit(code=1)
36
+
37
+ def db_command(func):
38
+ @functools.wraps(func)
39
+ def wrapper(*args, **kwargs):
40
+ run(func(*args, **kwargs))
41
+ run(Tortoise.close_connections())
42
+ return wrapper
43
+
44
+ @app.command()
45
+ @db_command
46
+ async def makemigrations():
47
+ app = get_app()
48
+ ensure_migrations_dir(app.migrations_dir)
49
+ command = await get_command(app)
50
+ migrations = await command.migrate(name="update", empty=False)
51
+ if migrations:
52
+ print("[green]Migrations created successfully.[/green]")
53
+ else:
54
+ print("[gray]No database changes detected.[/gray]")
55
+
56
+ @app.command()
57
+ @db_command
58
+ async def migrate():
59
+ app = get_app()
60
+ ensure_migrations_dir(app.migrations_dir)
61
+ command = await get_command(app)
62
+ migrations = await command.upgrade(run_in_transaction=True)
63
+ if migrations:
64
+ print("[green]Database migrated successfully.[/green]")
65
+ else:
66
+ print("[gray]No database changes detected.[/gray]")
67
+
68
+ @app.command()
69
+ @db_command
70
+ async def init():
71
+ app = get_app()
72
+ if migrations_dir_exists(app.migrations_dir):
73
+ print("[red]Migrations directory already exists. You may want to run `coloco db migrate` instead.[/red]")
74
+ raise typer.Exit(code=1)
75
+ command = await get_command(app, init=False)
76
+ await command.init_db(safe=True)
77
+ print("[green]Database initialized successfully.[/green]")
78
+
79
+ @app.command()
80
+ @db_command
81
+ async def revert(version: str, fake: bool = False):
82
+ app = get_app()
83
+ ensure_migrations_dir(app.migrations_dir)
84
+ command = await get_command(app)
85
+ await command.downgrade(version=version, delete=False, fake=fake)
86
+
87
+
88
+ @app.command()
89
+ @db_command
90
+ async def heads():
91
+ command = await get_command()
92
+ await command.heads()
93
+
94
+ @app.command()
95
+ @db_command
96
+ async def history():
97
+ command = await get_command()
98
+ await command.history()
99
+
100
+
@@ -1,5 +1,5 @@
1
1
  from .api import _verify_app, _serve
2
- from .node import install
2
+ from .node import install, _setup_dev_env
3
3
  import os
4
4
  from rich import print
5
5
  from subprocess import Popen
@@ -12,14 +12,16 @@ def dev(app: str = "main.app", host: str = "127.0.0.1"):
12
12
  if not os.path.exists(os.path.join(os.getcwd(), "node_modules")):
13
13
  print("[yellow]Node modules not found, installing...[/yellow]")
14
14
  install()
15
-
15
+
16
+ _setup_dev_env()
16
17
  node = Popen([f"npm run dev"], cwd="+node", shell=True)
17
18
  _serve(
18
19
  app=app,
19
20
  host=host,
20
21
  port=5172,
21
- reload=True,
22
22
  log_level="debug",
23
+ mode="dev",
24
+ reload=True,
23
25
  )
24
26
  node.terminate()
25
27
  node.wait()
@@ -7,10 +7,7 @@ import subprocess
7
7
  app = typer.Typer()
8
8
 
9
9
 
10
- @app.command()
11
- def install():
12
- """Installs node dependencies for the project"""
13
-
10
+ def _run_npm(command):
14
11
  # if not exists +node/package.json, raise error
15
12
  if not os.path.exists("+node/package.json"):
16
13
  print(
@@ -25,13 +22,13 @@ def install():
25
22
 
26
23
  try:
27
24
  # run npm install
28
- subprocess.run(["npm", "install"], cwd=".")
25
+ subprocess.run(command, cwd=".")
29
26
 
30
27
  # move package.json and package-lock.json back to +node
31
28
  shutil.move("package.json", "+node/package.json")
32
29
  shutil.move("package-lock.json", "+node/package-lock.json")
33
30
  except Exception as e:
34
- print(f"[red]Error installing packages: {e}[/red]")
31
+ print(f"[red]Error: {e}[/red]")
35
32
  try:
36
33
  os.remove("package.json")
37
34
  os.remove("package-lock.json")
@@ -39,11 +36,49 @@ def install():
39
36
  pass
40
37
  raise typer.Abort()
41
38
 
39
+
40
+ def _setup_dev_env():
41
+ os.environ["API_HOST"] = "http://localhost:5172"
42
+
43
+
44
+ @app.command()
45
+ def install():
46
+ """Installs node dependencies for the project"""
47
+
48
+ _run_npm(["npm", "install"])
49
+
42
50
  print("[green]Packages installed successfully.[/green]")
43
51
 
44
52
 
53
+ @app.command()
54
+ def add(package: str):
55
+ """Adds a node dependency to the project"""
56
+
57
+ _run_npm(["npm", "add", "-D", package])
58
+
59
+ print("[green]Package added successfully.[/green]")
60
+
61
+
62
+ @app.command()
63
+ def link(package: str):
64
+ """Links a node dependency to the project"""
65
+
66
+ _run_npm(["npm", "link", package])
67
+
68
+ print("[green]Package linked successfully.[/green]")
69
+
70
+
45
71
  @app.command()
46
72
  def dev():
47
73
  """Runs the node dev server"""
48
74
  print("[green]Running node dev server...[/green]")
75
+ _setup_dev_env()
49
76
  subprocess.run(["npm", "run", "dev"], cwd="+node")
77
+
78
+
79
+ @app.command()
80
+ def build(dir: str = None):
81
+ """Runs the node dev server"""
82
+ print("[green]Building node app...[/green]")
83
+
84
+ subprocess.run(["npm", "run", "build"], cwd="+node")
@@ -0,0 +1,14 @@
1
+ from rich import print
2
+ from .api import _serve
3
+ import os
4
+ import typer
5
+ import uvicorn
6
+
7
+
8
+ def serve(
9
+ app: str = "main.app",
10
+ port: int = 80,
11
+ host: str = "0.0.0.0",
12
+ log_level: str = "info",
13
+ ):
14
+ return _serve(app=app, host=host, port=port, log_level=log_level, mode="prod")
@@ -0,0 +1,39 @@
1
+ from contextlib import asynccontextmanager
2
+ from rich import print
3
+
4
+ def get_orm_config(database_url: str, model_files: list[str]):
5
+ return {
6
+ "connections": {
7
+ "default": database_url
8
+ },
9
+ "apps": {
10
+ "models": {
11
+ "models": [
12
+ *[model_file.replace("./", "").replace("/", ".").replace(".py", "") for model_file in model_files],
13
+ "aerich.models",
14
+ ],
15
+ "default_connection": "default",
16
+ },
17
+ },
18
+ }
19
+
20
+ @asynccontextmanager
21
+ async def lifecycle_connect_database(api):
22
+ from .app import get_current_app
23
+ app = get_current_app()
24
+
25
+ try:
26
+ from tortoise import Tortoise
27
+ except ImportError:
28
+ print("[red]Tortoise is not installed. "
29
+ "Please install it with `pip install tortoise-orm`. "
30
+ "If you intend to use anything other than sqlite, "
31
+ "you will need to install the appropriate database driver as well "
32
+ "(e.g. `pip install tortoise-orm[asyncpg]` for postgres).[/red]")
33
+ raise
34
+ print("[green]Connecting to database...[/green]")
35
+ await Tortoise.init(config=app.orm_config)
36
+ print("[green]Database ready[/green]")
37
+ yield
38
+ print("[yellow]Closing database connection...[/yellow]")
39
+ await Tortoise.close_connections()
@@ -1,6 +1,7 @@
1
1
  from fastapi import FastAPI
2
2
  from fastapi.responses import JSONResponse
3
3
  from fastapi.requests import Request
4
+ from traceback import format_list, extract_tb
4
5
 
5
6
 
6
7
  class UserError(Exception):
@@ -16,17 +17,24 @@ class ServerError(Exception):
16
17
  pass
17
18
 
18
19
 
19
- def bind_exceptions(api: FastAPI):
20
+ def _get_error_json(exception: Exception, debug: bool = False):
21
+ error = {"name": getattr(exception, "code", "api_error"), "message": f"{exception}"}
22
+ if debug:
23
+ error["stack"] = format_list(extract_tb(exception.__traceback__))
24
+ return error
25
+
26
+
27
+ def bind_exceptions(api: FastAPI, debug: bool = False):
20
28
  @api.exception_handler(UserError)
21
29
  async def user_error_handler(request: Request, exc: UserError):
22
30
  return JSONResponse(
23
31
  status_code=exc.status_code,
24
- content={"error": {"code": exc.code, "message": f"{exc}"}},
32
+ content=_get_error_json(exc, debug=debug),
25
33
  )
26
34
 
27
35
  @api.exception_handler(Exception)
28
36
  async def exception_handler(request: Request, exc: Exception):
29
37
  return JSONResponse(
30
38
  status_code=500,
31
- content={"error": {"code": "api_error", "message": f"{exc}"}},
39
+ content=_get_error_json(exc, debug=debug),
32
40
  )
@@ -14,8 +14,10 @@ async def execute_lifespan(app: FastAPI):
14
14
  contexts.append(generator)
15
15
  if hasattr(generator, "__aenter__"):
16
16
  await generator.__aenter__()
17
- else:
17
+ elif hasattr(generator, "__enter__"):
18
18
  next(generator)
19
+ else:
20
+ raise ValueError(f"Invalid lifespan handler: {lifespan_handler}. Must be an sync/async context manager.")
19
21
 
20
22
  yield
21
23
 
@@ -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, dist_dir: str = "dist"):
8
+ api.mount("/assets", StaticFiles(directory=f"{dist_dir}/app/assets"), name="static")
9
+
10
+ @api.get("/{all_paths:path}")
11
+ async def serve_app(all_paths: str):
12
+ return FileResponse(f"{dist_dir}/app/index.html")
@@ -1,17 +1,17 @@
1
1
  <script lang="ts">
2
- import { setupClient } from "@coloco/api-client-svelte";
2
+ import { makeClient, setDefaultClient } from "@coloco/api-client-svelte";
3
3
  import { route, Router, getRoutes } from "@coloco/router";
4
4
 
5
5
  import Index from "./index.svelte";
6
6
  import NotFound from "./404.svelte";
7
7
 
8
- setupClient({
9
- baseUrl: "http://localhost:5172",
10
- });
8
+ const client = makeClient({ baseUrl: import.meta.env.API_HOST });
11
9
  const routes = getRoutes({
12
10
  index: Index,
13
11
  notFound: NotFound,
14
12
  });
13
+
14
+ setDefaultClient(client);
15
15
  </script>
16
16
 
17
17
  <main>
@@ -10,8 +10,8 @@
10
10
  "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
11
11
  },
12
12
  "devDependencies": {
13
- "@coloco/api-client-svelte": "^0.1.1",
14
- "@coloco/router": "^0.1.2",
13
+ "@coloco/api-client-svelte": "^0.3.0",
14
+ "@coloco/router": "^0.1.5",
15
15
  "@hey-api/client-fetch": "^0.8.2",
16
16
  "@hey-api/openapi-ts": "^0.64.7",
17
17
  "@sveltejs/vite-plugin-svelte": "^5.0.3",
@@ -14,6 +14,10 @@ export default defineConfig({
14
14
  host: "0.0.0.0",
15
15
  port: 5173,
16
16
  },
17
+ build: {
18
+ outDir: '../dist/app',
19
+ emptyOutDir: true, // also necessary
20
+ },
17
21
  clearScreen: false,
18
22
  resolve: {
19
23
  // @ts-ignore
@@ -29,6 +33,16 @@ export default defineConfig({
29
33
  {
30
34
  find: '@api',
31
35
  replacement: '/.generated/client/api'
36
+ },
37
+ // @ts-ignore
38
+ {
39
+ find: '@app/',
40
+ replacement: '/'
41
+ },
42
+ // @ts-ignore
43
+ {
44
+ find: '@root',
45
+ replacement: '/..'
32
46
  }]
33
47
  }
34
48
  })
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "{{ project_name }}"
3
+ version = "0.1.0"
4
+ description = "Your project description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "coloco>=0.2.0",
9
+ ]
10
+
11
+ [tool.coloco]
12
+ app = "main.app"
13
+ migrations_dir = "./+migrations"
coloco-0.2.0/README.md DELETED
@@ -1,50 +0,0 @@
1
- # Coloco
2
-
3
- A kit for creating full-stack apps with co-located code, built on FastAPI and Svelte. Bundle your front-end and back-end code and easily tie them together with codegen.
4
-
5
- Example:
6
-
7
- `hello/api.py`
8
- ```python
9
- from coloco import api
10
-
11
- @api
12
- def test(name: str) -> str:
13
- return f"Hello {name}!"
14
-
15
- ```
16
-
17
- `hello/index.svelte`
18
- ```svelte
19
- <script lang="ts">
20
- import { test } from "./api";
21
-
22
- const response = test({ query: { name: "Coloco" } });
23
- </script>
24
-
25
- {#if $response.loading}
26
- Loading...
27
- {:else}
28
- The server says {$response.data}
29
- {/if}
30
- ```
31
-
32
- Serves the page `myapp.com/hello`, which calls `myapp.com/hello/test?name=Coloco` and prints the message `Hello Coloco!`
33
-
34
- # Opinions
35
-
36
- This framework is opinionated and combines the following excellent tools:
37
- * FastAPI
38
- * Svelte
39
- * openapi-ts (codegen)
40
- * svelte5-router (file-based routing)
41
- * tortoise-orm (optional)
42
-
43
- # Plans
44
- * Deploy tools
45
- * ORM Support
46
- * SQLite/Postgres support
47
-
48
- # Dreams
49
- * Move `node_modules` into `+node`
50
- * Remove the need for `query`, `params`, `body` for simple REST requests (maybe use legacy client but support the new one?)
@@ -1,58 +0,0 @@
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)
@@ -1,6 +0,0 @@
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
@@ -1,12 +0,0 @@
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")
File without changes
File without changes
File without changes
File without changes
File without changes