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.
- {coloco-0.3.5 → coloco-0.3.6}/PKG-INFO +18 -13
- {coloco-0.3.5 → coloco-0.3.6}/README.md +13 -8
- {coloco-0.3.5 → coloco-0.3.6}/pyproject.toml +9 -5
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/__main__.py +6 -6
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/api.py +23 -18
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/app.py +41 -16
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/api.py +8 -8
- coloco-0.3.6/src/coloco/cli/build.py +66 -0
- coloco-0.3.6/src/coloco/cli/db.py +144 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/dev.py +11 -7
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/node.py +6 -6
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/serve.py +5 -5
- coloco-0.3.6/src/coloco/db.py +143 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/static.py +3 -3
- coloco-0.3.5/src/coloco/cli/build.py +0 -22
- coloco-0.3.5/src/coloco/cli/db.py +0 -252
- coloco-0.3.5/src/coloco/db.py +0 -80
- {coloco-0.3.5 → coloco-0.3.6}/LICENSE +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/__init__.py +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/createapp.py +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/cli/package.py +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/codegen.py +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/config.py +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/dependencies.py +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/exceptions.py +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/lifespan.py +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/docker/.dockerignore-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/docker/Dockerfile-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/docker/docker-compose.yml-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/404.svelte-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/app.css-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/app.svelte-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/index.html-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/index.svelte-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+app/main.ts-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/openapi-ts.config.ts-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/package.json-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/svelte.config.js-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/tsconfig.json-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/vite-env.d.ts-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/+node/vite.config.ts-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/.gitignore-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/README.md-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/example/api.py-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/example/index.svelte-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/main.py-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/pyproject.toml-tpl +0 -0
- {coloco-0.3.5 → coloco-0.3.6}/src/coloco/templates/standard/tsconfig.json-tpl +0 -0
- {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.
|
|
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.
|
|
8
|
-
Requires-Dist:
|
|
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:
|
|
13
|
-
Requires-Dist:
|
|
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
|
-
#
|
|
72
|
+
# TBD Required features for launch
|
|
73
73
|
|
|
74
74
|
- Deploy tools
|
|
75
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
#
|
|
56
|
+
# TBD Required features for launch
|
|
57
57
|
|
|
58
58
|
- Deploy tools
|
|
59
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"tortoise-pathway==0.1.6",
|
|
14
|
+
"type-less>=0.1.16",
|
|
15
15
|
"uvicorn>=0.31.0",
|
|
16
16
|
]
|
|
17
|
-
requires-python = ">=3.
|
|
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 =
|
|
9
|
+
app = cyclopts.App()
|
|
10
10
|
|
|
11
|
-
app.command(
|
|
12
|
-
app.
|
|
13
|
-
app.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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,
|
|
1
|
+
from .api import create_api, global_routes
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from .db import
|
|
4
|
-
|
|
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(
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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,
|