coloco 0.3.5__tar.gz → 0.3.6__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.3.5 → coloco-0.3.6}/PKG-INFO +18 -13
  2. {coloco-0.3.5 → coloco-0.3.6}/README.md +13 -8
  3. {coloco-0.3.5 → coloco-0.3.6}/pyproject.toml +9 -5
  4. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/__main__.py +6 -6
  5. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/api.py +23 -18
  6. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/app.py +41 -16
  7. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/api.py +8 -8
  8. coloco-0.3.6/src/coloco/cli/build.py +66 -0
  9. coloco-0.3.6/src/coloco/cli/db.py +144 -0
  10. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/dev.py +11 -7
  11. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/node.py +6 -6
  12. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/serve.py +5 -5
  13. coloco-0.3.6/src/coloco/db.py +143 -0
  14. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/static.py +3 -3
  15. coloco-0.3.5/src/coloco/cli/build.py +0 -22
  16. coloco-0.3.5/src/coloco/cli/db.py +0 -252
  17. coloco-0.3.5/src/coloco/db.py +0 -80
  18. {coloco-0.3.5 → coloco-0.3.6}/LICENSE +0 -0
  19. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/__init__.py +0 -0
  20. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/createapp.py +0 -0
  21. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/package.py +0 -0
  22. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/codegen.py +0 -0
  23. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/config.py +0 -0
  24. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/dependencies.py +0 -0
  25. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/exceptions.py +0 -0
  26. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/lifespan.py +0 -0
  27. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/docker/.dockerignore-tpl +0 -0
  28. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/docker/Dockerfile-tpl +0 -0
  29. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/docker/docker-compose.yml-tpl +0 -0
  30. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/404.svelte-tpl +0 -0
  31. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/app.css-tpl +0 -0
  32. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/app.svelte-tpl +0 -0
  33. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/index.html-tpl +0 -0
  34. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/index.svelte-tpl +0 -0
  35. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/main.ts-tpl +0 -0
  36. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/openapi-ts.config.ts-tpl +0 -0
  37. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/package.json-tpl +0 -0
  38. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/svelte.config.js-tpl +0 -0
  39. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/tsconfig.json-tpl +0 -0
  40. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/vite-env.d.ts-tpl +0 -0
  41. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/vite.config.ts-tpl +0 -0
  42. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/.gitignore-tpl +0 -0
  43. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/README.md-tpl +0 -0
  44. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/example/api.py-tpl +0 -0
  45. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/example/index.svelte-tpl +0 -0
  46. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/main.py-tpl +0 -0
  47. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/pyproject.toml-tpl +0 -0
  48. {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/tsconfig.json-tpl +0 -0
  49. {coloco-0.3.5 → coloco-0.3.6}/tests/__init__.py +0 -0
@@ -1,16 +1,16 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: coloco
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: A kit for creating FastAPI + Svelte applications
5
5
  Author-Email: Channel Cat <channelcat@gmail.com>
6
6
  License: MIT
7
- Requires-Python: >=3.11
8
- Requires-Dist: aerich[toml]>=0.8.1
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: cyclopts>=3.18.0
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.8
13
- Requires-Dist: typer>=0.15.2
12
+ Requires-Dist: tortoise-pathway==0.1.6
13
+ Requires-Dist: type-less>=0.1.16
14
14
  Requires-Dist: uvicorn>=0.31.0
15
15
  Description-Content-Type: text/markdown
16
16
 
@@ -69,23 +69,28 @@ This framework is opinionated and combines the following excellent tools:
69
69
  - svelte5-router (file-based routing)
70
70
  - tortoise-orm (optional)
71
71
 
72
- # Plans
72
+ # TBD Required features for launch
73
73
 
74
74
  - Deploy tools
75
- - Migrations for ORM - `aerich` (Improvements needed, watching `tortoise-pathway`)
75
+ - Swap migrations to `tortoise-pathway` once cross-app migrations are supported
76
+ - Config with secrets encryption
77
+ - Package/share modules with git
78
+ - Docs
79
+
80
+ # Features in development
81
+
76
82
  - CRUD
77
83
  - Caching
78
- - Package/share modules with git
79
- -
84
+ - User module
85
+ - Dev tools module
86
+ - Admin panel module
80
87
 
81
88
  # Dreams
82
89
 
83
90
  - Move `node_modules` into `+node`
84
- - Somehow clean up litter from `uv`
85
91
 
86
- # TODO
92
+ # Other TODO
87
93
 
88
- - Swap to `cyclopts` for CLI
89
94
  - Swap to `copier` for template creation
90
95
  - `Textual` CLI app / installer / dev tools?
91
- - Common dev `admin` module
96
+ - Resolve duplicate types in openapi spec (due to model serializers)
@@ -53,23 +53,28 @@ This framework is opinionated and combines the following excellent tools:
53
53
  - svelte5-router (file-based routing)
54
54
  - tortoise-orm (optional)
55
55
 
56
- # Plans
56
+ # TBD Required features for launch
57
57
 
58
58
  - Deploy tools
59
- - Migrations for ORM - `aerich` (Improvements needed, watching `tortoise-pathway`)
59
+ - Swap migrations to `tortoise-pathway` once cross-app migrations are supported
60
+ - Config with secrets encryption
61
+ - Package/share modules with git
62
+ - Docs
63
+
64
+ # Features in development
65
+
60
66
  - CRUD
61
67
  - Caching
62
- - Package/share modules with git
63
- -
68
+ - User module
69
+ - Dev tools module
70
+ - Admin panel module
64
71
 
65
72
  # Dreams
66
73
 
67
74
  - Move `node_modules` into `+node`
68
- - Somehow clean up litter from `uv`
69
75
 
70
- # TODO
76
+ # Other TODO
71
77
 
72
- - Swap to `cyclopts` for CLI
73
78
  - Swap to `copier` for template creation
74
79
  - `Textual` CLI app / installer / dev tools?
75
- - Common dev `admin` module
80
+ - Resolve duplicate types in openapi spec (due to model serializers)
@@ -1,20 +1,20 @@
1
1
  [project]
2
2
  name = "coloco"
3
- version = "0.3.5"
3
+ version = "0.3.6"
4
4
  description = "A kit for creating FastAPI + Svelte applications"
5
5
  authors = [
6
6
  { name = "Channel Cat", email = "channelcat@gmail.com" },
7
7
  ]
8
8
  dependencies = [
9
- "aerich[toml]>=0.8.1",
9
+ "cyclopts>=3.18.0",
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.8",
14
- "typer>=0.15.2",
13
+ "tortoise-pathway==0.1.6",
14
+ "type-less>=0.1.16",
15
15
  "uvicorn>=0.31.0",
16
16
  ]
17
- requires-python = ">=3.11"
17
+ requires-python = ">=3.12"
18
18
  readme = "README.md"
19
19
 
20
20
  [project.license]
@@ -31,3 +31,7 @@ build-backend = "pdm.backend"
31
31
 
32
32
  [tool.pdm]
33
33
  distribution = true
34
+
35
+ [tool.uv.sources.tortoise-pathway]
36
+ git = "https://github.com/channelcat/tortoise-pathway"
37
+ rev = "multi-app-support"
@@ -1,16 +1,16 @@
1
- import typer
2
1
  from .cli.dev import dev
3
2
  from .cli.api import app as api_app
4
3
  from .cli.node import app as node_app
5
4
  from .cli.createapp import createapp
6
5
  from .cli.build import build
7
6
  from .cli.serve import serve
7
+ import cyclopts
8
8
 
9
- app = typer.Typer()
9
+ app = cyclopts.App()
10
10
 
11
- app.command()(dev)
12
- app.add_typer(node_app, name="node")
13
- app.add_typer(api_app, name="api")
11
+ app.command(dev, name="dev")
12
+ app.command(node_app, name="node")
13
+ app.command(api_app, name="api")
14
14
  app.command()(build)
15
15
  app.command()(createapp)
16
16
  app.command()(serve)
@@ -19,7 +19,7 @@ app.command()(serve)
19
19
  try:
20
20
  from .cli.db import app as db_app
21
21
 
22
- app.add_typer(db_app, name="db")
22
+ app.command(db_app, name="db")
23
23
  except ImportError:
24
24
  pass
25
25
 
@@ -4,19 +4,33 @@ from .codegen import (
4
4
  generate_openapi_schema,
5
5
  )
6
6
  from contextlib import asynccontextmanager
7
+ from dataclasses import dataclass
7
8
  from .exceptions import bind_exceptions
8
9
  from fastapi import APIRouter, FastAPI
9
10
  from fastapi.middleware.cors import CORSMiddleware
10
11
  from .lifespan import execute_lifespan, register_lifespan
11
12
  import logging
12
13
  from os import environ
13
- from type_less import fill_type_hints
14
14
  from threading import Thread
15
+ from type_less import fill_type_hints
16
+ from typing import Callable, TypeVar
15
17
 
16
18
 
17
19
  logging.basicConfig(level=logging.INFO)
18
20
 
19
21
 
22
+ T = TypeVar("T")
23
+
24
+
25
+ @dataclass
26
+ class ColocoRoute:
27
+ args: tuple
28
+ kwargs: dict
29
+ func: Callable
30
+ method: str
31
+ module_name: str
32
+
33
+
20
34
  def _generate_openapi_thread(app: FastAPI):
21
35
  # TODO: diff check the schema json
22
36
  # TODO: diff check not working
@@ -35,16 +49,17 @@ async def generate_openapi(app: FastAPI):
35
49
 
36
50
 
37
51
  def create_api(is_dev: bool = False):
52
+ kwargs = {
53
+ "lifespan": execute_lifespan,
54
+ }
38
55
  if not is_dev:
39
56
  kwargs = {
57
+ **kwargs,
40
58
  "openapi_url": None,
41
59
  "docs_url": None,
42
60
  "redoc_url": None,
43
61
  }
44
62
  else:
45
- kwargs = {
46
- "lifespan": execute_lifespan,
47
- }
48
63
  register_lifespan(generate_openapi)
49
64
 
50
65
  api = FastAPI(
@@ -62,7 +77,6 @@ def create_api(is_dev: bool = False):
62
77
  allow_headers=["*"],
63
78
  )
64
79
 
65
- # bind_static(api)
66
80
  bind_exceptions(api, debug=is_dev)
67
81
 
68
82
  return api
@@ -70,7 +84,7 @@ def create_api(is_dev: bool = False):
70
84
 
71
85
  # ========================= Global routing =========================
72
86
 
73
- global_router = APIRouter()
87
+ global_routes: list[ColocoRoute] = []
74
88
 
75
89
 
76
90
  def api(func):
@@ -79,8 +93,6 @@ def api(func):
79
93
 
80
94
 
81
95
  def _add_global_route(args, kwargs, func, method: str):
82
- fill_type_hints(func, use_literals=True)
83
-
84
96
  module_name = func.__module__.lstrip("src.app")
85
97
 
86
98
  # Prepend module name to path
@@ -101,18 +113,11 @@ def _add_global_route(args, kwargs, func, method: str):
101
113
  else:
102
114
  kwargs["path"] = path
103
115
 
104
- return global_router.api_route(
105
- *args,
106
- **{
107
- **kwargs,
108
- "summary": (kwargs.get("summary", "") + f" ({module_name})").strip(),
109
- "methods": [method],
110
- },
111
- )(func)
116
+ return global_routes.append(ColocoRoute(args, kwargs, func, method, module_name))
112
117
 
113
118
 
114
- def _make_route_decorator(method: str):
115
- def route_wrapper(*args, **kwargs):
119
+ def _make_route_decorator(method: str) -> Callable[..., Callable[[T], T]]:
120
+ def route_wrapper(*args, **kwargs) -> Callable[[T], T]:
116
121
  def handler_wrapper(func):
117
122
  return _add_global_route(args, kwargs, func, method)
118
123
 
@@ -1,13 +1,17 @@
1
- from .api import create_api, global_router
1
+ from .api import create_api, global_routes
2
2
  from dataclasses import dataclass
3
- from .db import get_orm_config, create_lifecycle_connect_database
4
- from fastapi import FastAPI
3
+ from .db import (
4
+ get_orm_config,
5
+ inject_model_serializers,
6
+ register_db_lifecycle,
7
+ )
8
+ from fastapi import APIRouter, FastAPI
5
9
  from importlib import import_module
6
- from .lifespan import register_lifespan
7
10
  import os
8
11
  from rich import print
9
12
  from .static import bind_static
10
13
  import traceback
14
+ from type_less import fill_type_hints
11
15
  from typing import Literal
12
16
 
13
17
 
@@ -53,8 +57,10 @@ def create_app(name: str, database_url: str = None) -> ColocoApp:
53
57
  mode: Literal["dev", "prod"] = os.environ.get("COLOCO_MODE", "dev")
54
58
  api = create_api(is_dev=mode == "dev")
55
59
 
60
+ src_path = "src"
61
+
56
62
  # Discover all api.py files from root, excluding node_modules and +app
57
- api_files = discover_files(".", name="api.py", is_dev=mode == "dev")
63
+ api_files = discover_files(src_path, name="api.py", is_dev=mode == "dev")
58
64
  for api_file in api_files:
59
65
  # convert python file path to module path
60
66
  module_name = api_file.replace("./", "").replace(".py", "").replace("/", ".")
@@ -65,27 +71,46 @@ def create_app(name: str, database_url: str = None) -> ColocoApp:
65
71
  print(traceback.format_exc())
66
72
  continue
67
73
 
68
- api.include_router(global_router)
69
-
70
- # Production mode serves dist
71
- if mode == "prod":
72
- bind_static(api)
74
+ # Inject type hints
75
+ for route in global_routes:
76
+ fill_type_hints(route.func, use_literals=True)
73
77
 
74
78
  # Setup Database
75
- if database_url:
79
+ # We need this first to grab the models for route type hints
80
+ has_database = bool(database_url)
81
+ if has_database:
76
82
  orm_config = get_orm_config(
77
83
  database_url,
78
- model_files=discover_files(".", name="models.py", is_dev=mode == "dev"),
84
+ model_files=discover_files(src_path, name="models.py", is_dev=mode == "dev"),
79
85
  )
86
+ inject_model_serializers(orm_config, global_routes)
80
87
  else:
81
88
  orm_config = None
82
89
 
83
- CURRENT_APP = ColocoApp(
84
- api=api, name=name, database_url=database_url, orm_config=orm_config
85
- )
90
+ router = APIRouter()
91
+ for route in global_routes:
92
+ router.api_route(
93
+ *route.args,
94
+ **{
95
+ **route.kwargs,
96
+ **{
97
+ "summary": (
98
+ route.kwargs.get("summary", "") + f" ({route.module_name})"
99
+ ).strip(),
100
+ "methods": [route.method],
101
+ },
102
+ },
103
+ )(route.func)
104
+ api.include_router(router)
105
+
106
+ # Production mode serves dist
107
+ if mode == "prod":
108
+ bind_static(api)
109
+
110
+ CURRENT_APP = ColocoApp(api=api, name=name, database_url=database_url, orm_config=orm_config)
86
111
 
87
112
  if database_url:
88
- register_lifespan(create_lifecycle_connect_database(CURRENT_APP))
113
+ register_db_lifecycle(CURRENT_APP)
89
114
 
90
115
  return CURRENT_APP
91
116
 
@@ -1,15 +1,15 @@
1
1
  from ..app import ColocoApp
2
2
  from ..codegen import generate_openapi_schema, generate_openapi_code
3
+ import cyclopts
3
4
  from importlib import import_module
4
5
  import os
5
6
  from rich import print
6
7
  import sys
7
- import typer
8
8
  from typing import Literal
9
9
  import uvicorn
10
10
 
11
11
 
12
- app = typer.Typer()
12
+ app = cyclopts.App()
13
13
 
14
14
 
15
15
  def _verify_app(app: str = "src.main.app") -> ColocoApp:
@@ -17,7 +17,7 @@ def _verify_app(app: str = "src.main.app") -> ColocoApp:
17
17
  print(
18
18
  "[red]App should be the name of a variable in a python file, example: main.py -> api = main.api[/red]"
19
19
  )
20
- raise typer.Abort()
20
+ raise SystemExit(1)
21
21
 
22
22
  module_name, var_name = app.rsplit(".", 1)
23
23
  try:
@@ -26,17 +26,17 @@ def _verify_app(app: str = "src.main.app") -> ColocoApp:
26
26
  module = import_module(module_name)
27
27
  except ModuleNotFoundError:
28
28
  print(f"[red]Module or python file {module_name} not found[/red]")
29
- raise typer.Abort()
29
+ raise SystemExit(1)
30
30
 
31
31
  if not hasattr(module, var_name):
32
32
  print(f"[red]Variable {var_name} not found in module {module_name}[/red]")
33
- raise typer.Abort()
33
+ raise SystemExit(1)
34
34
 
35
35
  var = getattr(module, var_name)
36
36
 
37
37
  if not isinstance(var, ColocoApp):
38
38
  print(f"[red]{var_name} is not a ColocoApp. Please use create_app[/red]")
39
- raise typer.Abort()
39
+ raise SystemExit(1)
40
40
 
41
41
  return var
42
42
 
@@ -50,12 +50,12 @@ def _verify_is_packaged():
50
50
  print(
51
51
  f"[red]Dist dir {dist_dir} does not exist. Run [green]coloco build[/green] to package the app.[/red]"
52
52
  )
53
- raise typer.Abort()
53
+ raise SystemExit(1)
54
54
  if not os.path.exists(app_dir):
55
55
  print(
56
56
  f"[red]App is missing from package directory {app_dir}. Run [green]coloco build[/green] to package the app.[/red]"
57
57
  )
58
- raise typer.Abort()
58
+ raise SystemExit(1)
59
59
 
60
60
 
61
61
  def _serve(
@@ -0,0 +1,66 @@
1
+ from .api import _verify_app, codegen
2
+ from ..config import get_coloco_config
3
+ from .node import build as build_node
4
+ import os
5
+ from pathlib import Path
6
+ from rich import print
7
+ import shutil
8
+
9
+
10
+ def build(
11
+ app: str | None = None,
12
+ ):
13
+ if not app:
14
+ app = get_coloco_config().get("app") or "src.main.app"
15
+
16
+ _verify_app(app)
17
+
18
+ cwd = Path(os.getcwd())
19
+ source_dir = cwd / "src"
20
+ dist_dir = cwd / "dist"
21
+ api_dir = dist_dir / "src"
22
+ frontend_dir = dist_dir / "static"
23
+
24
+ print(f"Clearing {dist_dir}...")
25
+ shutil.rmtree(dist_dir, ignore_errors=True)
26
+
27
+ # # Codegen API
28
+ codegen(app)
29
+
30
+ # # Build node app
31
+ print(f"Packaging app...")
32
+ build_node(dir=frontend_dir)
33
+
34
+ print(f"Adding source files...")
35
+
36
+ api_dir.mkdir(parents=True, exist_ok=True)
37
+
38
+ # Collect backend files
39
+ package_api_files = get_coloco_config().get("package_api_files") or ["*.py"]
40
+ for glob_pattern in package_api_files:
41
+ # If glob_pattern starts with /, assume non-recursive
42
+ files = (
43
+ source_dir.glob(glob_pattern[1:])
44
+ if glob_pattern.startswith("/")
45
+ else source_dir.rglob(glob_pattern)
46
+ )
47
+ for file in files:
48
+ # Skip dev files
49
+ if "/-" in str(file):
50
+ continue
51
+
52
+ destination = api_dir / file.relative_to(source_dir)
53
+ destination.parent.mkdir(parents=True, exist_ok=True)
54
+ shutil.copy(file, destination)
55
+ print(f" |- {file.relative_to(cwd)}")
56
+
57
+ print(f"Adding project files...")
58
+ shutil.copy(cwd / "pyproject.toml", dist_dir / "pyproject.toml")
59
+ shutil.copy(cwd / "uv.lock", dist_dir / "uv.lock")
60
+
61
+ print(
62
+ f"App packaged into {dist_dir}.\n"
63
+ f"From the [yellow]{dist_dir}[/yellow] directory:\n"
64
+ f" Run [green]uv sync --frozen[/green] to install dependencies.\n"
65
+ f" Run [green]coloco serve[/green] to start the app in production mode.\n"
66
+ )
@@ -0,0 +1,144 @@
1
+ from .api import _verify_app
2
+ from ..app import ColocoApp, get_current_app
3
+ from asyncio import run
4
+ import cyclopts
5
+ import functools
6
+ import logging
7
+ import os
8
+ from pathlib import Path
9
+ from rich import print
10
+ from tortoise import Tortoise
11
+ from tortoise.log import logger as tortoise_logger
12
+ from tortoise_pathway.migration_manager import MigrationManager
13
+ from typing import Callable, TypeVar
14
+
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ app = cyclopts.App()
20
+
21
+
22
+ def _get_model_apps(coloco_app: ColocoApp):
23
+ return [app for app in coloco_app.orm_config["apps"]]
24
+
25
+
26
+ def _get_coloco_app():
27
+ _verify_app()
28
+ return get_current_app()
29
+
30
+
31
+ def _get_app_migrations_path_func(migrations_dir: str) -> Callable[[str], Path]:
32
+ def _get_app_migrations_path(self, app) -> Path:
33
+ return Path(os.path.join("src", "app", app, migrations_dir))
34
+
35
+ return _get_app_migrations_path
36
+
37
+
38
+ def db_command(func):
39
+ @functools.wraps(func)
40
+ def wrapper(*args, **kwargs):
41
+ tortoise_logger.setLevel(logging.WARNING)
42
+ run(func(*args, **kwargs))
43
+ run(Tortoise.close_connections())
44
+
45
+ return app.command(name=func.__name__)(wrapper)
46
+
47
+
48
+ async def get_migration_manager():
49
+ coloco_app = _get_coloco_app()
50
+ apps = _get_model_apps(coloco_app)
51
+ await Tortoise.init(config=coloco_app.orm_config)
52
+
53
+ # TODO: propose a patch for this
54
+ MigrationManager.get_migrations_dir = _get_app_migrations_path_func(coloco_app.migrations_dir)
55
+
56
+ manager = MigrationManager(apps, coloco_app.migrations_dir)
57
+ await manager.initialize()
58
+
59
+ return manager
60
+
61
+
62
+ # ----------------------------- Commands -----------------------------
63
+
64
+
65
+ @db_command
66
+ async def makemigrations(
67
+ app: str | None = None, name: str | None = None, empty: bool = False
68
+ ) -> None:
69
+ """Create new migration(s) based on model changes."""
70
+ manager = await get_migration_manager()
71
+
72
+ print(f"Making migrations for {app or 'all apps'}...", end="", flush=True)
73
+
74
+ migrations = await manager.create_migration(name, app=app, auto=True)
75
+
76
+ if not migrations:
77
+ print("[gray]no changes[/gray]")
78
+ return
79
+
80
+ print()
81
+ for migration in migrations:
82
+ print(f" |- [yellow]{migration.display_name()}[/yellow] @ {migration.path()}")
83
+
84
+
85
+ @db_command
86
+ async def migrate(app: str | None = None) -> None:
87
+ """Apply migrations to the database."""
88
+ manager = await get_migration_manager()
89
+
90
+ pending = manager.get_pending_migrations(app=app)
91
+
92
+ if not pending:
93
+ print("[gray]No pending migrations.[/gray]")
94
+ return
95
+
96
+ s = "s" if len(pending) > 1 else ""
97
+ print(f"Applying [cyan]{len(pending)}[/cyan] migration{s}:")
98
+
99
+ applied = []
100
+ async for migration in manager.apply_migrations_async(app=app):
101
+ print(f" |- [cyan]{migration.display_name()}[/cyan]")
102
+ applied.append(migration)
103
+
104
+ if applied:
105
+ print(f"[green]Successfully applied {len(applied)} migration{s}[/green]")
106
+ else:
107
+ print("[gray]No migrations were applied.[/gray]")
108
+
109
+
110
+ @db_command
111
+ async def rollback(app: str | None = None, migration: str | None = None) -> None:
112
+ """Revert the most recent migration."""
113
+ manager = await get_migration_manager()
114
+
115
+ reverted = await manager.revert_migration(migration=migration, app=app)
116
+
117
+ if reverted:
118
+ print(f"Successfully reverted migration: [yellow]{reverted.display_name()}[/yellow]")
119
+ else:
120
+ print("[gray]No migration was reverted.[/gray]")
121
+
122
+
123
+ @db_command
124
+ async def showmigrations(app: str | None = None) -> None:
125
+ """Show migration status."""
126
+ manager = await get_migration_manager()
127
+
128
+ applied = manager.get_applied_migrations(app=app)
129
+ pending = manager.get_pending_migrations(app=app)
130
+
131
+ print(f"Migrations for {app}:" if app else "All Migrations:")
132
+ print("\nApplied migrations:")
133
+ if applied:
134
+ for migration in applied:
135
+ print(f" [X] {migration.display_name()}")
136
+ else:
137
+ print(" (none)")
138
+
139
+ print("\nPending migrations:")
140
+ if pending:
141
+ for migration in pending:
142
+ print(f" [ ] {migration.display_name()}")
143
+ else:
144
+ print(" (none)")
@@ -1,24 +1,28 @@
1
1
  from .api import _verify_app, _serve
2
+ import cyclopts
2
3
  from ..config import get_coloco_config
3
4
  from .node import install, _setup_dev_env
4
5
  import os
5
6
  from rich import print
6
7
  from subprocess import Popen
7
8
 
9
+ app = cyclopts.App()
8
10
 
9
- def dev(app: str = None, host: str = "127.0.0.1"):
11
+
12
+ @app.default()
13
+ def dev(app: str | None = None, host: str = "127.0.0.1"):
10
14
  if not app:
11
15
  app = get_coloco_config().get("app") or "src.main.app"
12
16
 
13
17
  _verify_app(app)
14
18
 
15
- # Check Node Modules
16
- if not os.path.exists(os.path.join(os.getcwd(), "node_modules")):
17
- print("[yellow]Node modules not found, installing...[/yellow]")
18
- install()
19
+ # # Check Node Modules
20
+ # if not os.path.exists(os.path.join(os.getcwd(), "node_modules")):
21
+ # print("[yellow]Node modules not found, installing...[/yellow]")
22
+ # install()
19
23
 
20
- _setup_dev_env()
21
- node = Popen([f"npm run dev"], shell=True)
24
+ # _setup_dev_env()
25
+ # node = Popen([f"npm run dev"], shell=True)
22
26
  _serve(
23
27
  app=app,
24
28
  host=host,