coloco 0.3.4__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.4 → coloco-0.3.6}/PKG-INFO +25 -18
- {coloco-0.3.4 → coloco-0.3.6}/README.md +16 -9
- {coloco-0.3.4 → coloco-0.3.6}/pyproject.toml +13 -9
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/__init__.py +6 -2
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/__main__.py +6 -6
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/api.py +29 -19
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/app.py +49 -20
- {coloco-0.3.4 → 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.4 → coloco-0.3.6}/src/coloco/cli/dev.py +11 -7
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/cli/node.py +6 -6
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/cli/serve.py +5 -5
- coloco-0.3.6/src/coloco/db.py +143 -0
- coloco-0.3.6/src/coloco/dependencies.py +6 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/static.py +3 -3
- coloco-0.3.4/src/coloco/cli/build.py +0 -22
- coloco-0.3.4/src/coloco/cli/db.py +0 -100
- coloco-0.3.4/src/coloco/db.py +0 -39
- {coloco-0.3.4 → coloco-0.3.6}/LICENSE +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/cli/createapp.py +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/cli/package.py +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/codegen.py +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/config.py +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/exceptions.py +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/lifespan.py +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/docker/.dockerignore-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/docker/Dockerfile-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/docker/docker-compose.yml-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+app/404.svelte-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+app/app.css-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+app/app.svelte-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+app/index.html-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+app/index.svelte-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+app/main.ts-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+node/openapi-ts.config.ts-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+node/package.json-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+node/svelte.config.js-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+node/tsconfig.json-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+node/vite-env.d.ts-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/+node/vite.config.ts-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/.gitignore-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/README.md-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/example/api.py-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/example/index.svelte-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/main.py-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/pyproject.toml-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/src/coloco/templates/standard/tsconfig.json-tpl +0 -0
- {coloco-0.3.4 → coloco-0.3.6}/tests/__init__.py +0 -0
|
@@ -1,17 +1,17 @@
|
|
|
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:
|
|
9
|
-
Requires-Dist: fastapi
|
|
10
|
-
Requires-Dist: PyJWT
|
|
11
|
-
Requires-Dist: tortoise-orm[asyncpg]
|
|
12
|
-
Requires-Dist:
|
|
13
|
-
Requires-Dist:
|
|
14
|
-
Requires-Dist: uvicorn
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: cyclopts>=3.18.0
|
|
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: tortoise-pathway==0.1.6
|
|
13
|
+
Requires-Dist: type-less>=0.1.16
|
|
14
|
+
Requires-Dist: uvicorn>=0.31.0
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
# Coloco
|
|
@@ -56,8 +56,8 @@ Serves the page `myapp.com/hello`, which calls `myapp.com/hello/test?name=Coloco
|
|
|
56
56
|
# Running in Production
|
|
57
57
|
|
|
58
58
|
- `coloco build`
|
|
59
|
-
|
|
60
|
-
- `coloco serve`
|
|
59
|
+
- Artifacts will be saved to `dist`
|
|
60
|
+
- From dist, run `coloco serve`
|
|
61
61
|
|
|
62
62
|
# Opinions
|
|
63
63
|
|
|
@@ -69,21 +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
|
-
-
|
|
76
|
-
-
|
|
75
|
+
- Swap migrations to `tortoise-pathway` once cross-app migrations are supported
|
|
76
|
+
- Config with secrets encryption
|
|
77
77
|
- Package/share modules with git
|
|
78
|
+
- Docs
|
|
79
|
+
|
|
80
|
+
# Features in development
|
|
81
|
+
|
|
82
|
+
- CRUD
|
|
83
|
+
- Caching
|
|
84
|
+
- User module
|
|
85
|
+
- Dev tools module
|
|
86
|
+
- Admin panel module
|
|
78
87
|
|
|
79
88
|
# Dreams
|
|
80
89
|
|
|
81
90
|
- Move `node_modules` into `+node`
|
|
82
|
-
- Somehow clean up litter from `uv`
|
|
83
91
|
|
|
84
|
-
# TODO
|
|
92
|
+
# Other TODO
|
|
85
93
|
|
|
86
|
-
- Swap to `cyclopts` for CLI
|
|
87
94
|
- Swap to `copier` for template creation
|
|
88
95
|
- `Textual` CLI app / installer / dev tools?
|
|
89
|
-
-
|
|
96
|
+
- Resolve duplicate types in openapi spec (due to model serializers)
|
|
@@ -40,8 +40,8 @@ Serves the page `myapp.com/hello`, which calls `myapp.com/hello/test?name=Coloco
|
|
|
40
40
|
# Running in Production
|
|
41
41
|
|
|
42
42
|
- `coloco build`
|
|
43
|
-
|
|
44
|
-
- `coloco serve`
|
|
43
|
+
- Artifacts will be saved to `dist`
|
|
44
|
+
- From dist, run `coloco serve`
|
|
45
45
|
|
|
46
46
|
# Opinions
|
|
47
47
|
|
|
@@ -53,21 +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
|
-
-
|
|
60
|
-
-
|
|
59
|
+
- Swap migrations to `tortoise-pathway` once cross-app migrations are supported
|
|
60
|
+
- Config with secrets encryption
|
|
61
61
|
- Package/share modules with git
|
|
62
|
+
- Docs
|
|
63
|
+
|
|
64
|
+
# Features in development
|
|
65
|
+
|
|
66
|
+
- CRUD
|
|
67
|
+
- Caching
|
|
68
|
+
- User module
|
|
69
|
+
- Dev tools module
|
|
70
|
+
- Admin panel module
|
|
62
71
|
|
|
63
72
|
# Dreams
|
|
64
73
|
|
|
65
74
|
- Move `node_modules` into `+node`
|
|
66
|
-
- Somehow clean up litter from `uv`
|
|
67
75
|
|
|
68
|
-
# TODO
|
|
76
|
+
# Other TODO
|
|
69
77
|
|
|
70
|
-
- Swap to `cyclopts` for CLI
|
|
71
78
|
- Swap to `copier` for template creation
|
|
72
79
|
- `Textual` CLI app / installer / dev tools?
|
|
73
|
-
-
|
|
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
|
-
"
|
|
10
|
-
"fastapi
|
|
11
|
-
"PyJWT
|
|
12
|
-
"tortoise-orm[asyncpg]
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"uvicorn
|
|
9
|
+
"cyclopts>=3.18.0",
|
|
10
|
+
"fastapi>=0.115.0",
|
|
11
|
+
"PyJWT>=2.10.1",
|
|
12
|
+
"tortoise-orm[asyncpg]>=0.24.0",
|
|
13
|
+
"tortoise-pathway==0.1.6",
|
|
14
|
+
"type-less>=0.1.16",
|
|
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,12 +1,16 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .api import api
|
|
2
|
+
from .app import ColocoApp, create_app, get_current_app
|
|
2
3
|
from .codegen import generate_openapi_schema, generate_openapi_code
|
|
3
4
|
from .exceptions import UserError
|
|
4
|
-
from .
|
|
5
|
+
from . import dependencies
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
8
|
"api",
|
|
9
|
+
"ColocoApp",
|
|
8
10
|
"create_app",
|
|
11
|
+
"dependencies",
|
|
9
12
|
"generate_openapi_schema",
|
|
10
13
|
"generate_openapi_code",
|
|
14
|
+
"get_current_app",
|
|
11
15
|
"UserError",
|
|
12
16
|
]
|
|
@@ -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,20 +4,36 @@ 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):
|
|
35
|
+
# TODO: diff check the schema json
|
|
36
|
+
# TODO: diff check not working
|
|
21
37
|
logging.info("Generating OpenAPI schema in thread...")
|
|
22
38
|
generate_openapi_schema(app)
|
|
23
39
|
generate_openapi_code(host=f"http://localhost:5172", diff_files=True)
|
|
@@ -33,16 +49,17 @@ async def generate_openapi(app: FastAPI):
|
|
|
33
49
|
|
|
34
50
|
|
|
35
51
|
def create_api(is_dev: bool = False):
|
|
52
|
+
kwargs = {
|
|
53
|
+
"lifespan": execute_lifespan,
|
|
54
|
+
}
|
|
36
55
|
if not is_dev:
|
|
37
56
|
kwargs = {
|
|
57
|
+
**kwargs,
|
|
38
58
|
"openapi_url": None,
|
|
39
59
|
"docs_url": None,
|
|
40
60
|
"redoc_url": None,
|
|
41
61
|
}
|
|
42
62
|
else:
|
|
43
|
-
kwargs = {
|
|
44
|
-
"lifespan": execute_lifespan,
|
|
45
|
-
}
|
|
46
63
|
register_lifespan(generate_openapi)
|
|
47
64
|
|
|
48
65
|
api = FastAPI(
|
|
@@ -60,7 +77,6 @@ def create_api(is_dev: bool = False):
|
|
|
60
77
|
allow_headers=["*"],
|
|
61
78
|
)
|
|
62
79
|
|
|
63
|
-
# bind_static(api)
|
|
64
80
|
bind_exceptions(api, debug=is_dev)
|
|
65
81
|
|
|
66
82
|
return api
|
|
@@ -68,7 +84,7 @@ def create_api(is_dev: bool = False):
|
|
|
68
84
|
|
|
69
85
|
# ========================= Global routing =========================
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
global_routes: list[ColocoRoute] = []
|
|
72
88
|
|
|
73
89
|
|
|
74
90
|
def api(func):
|
|
@@ -77,8 +93,6 @@ def api(func):
|
|
|
77
93
|
|
|
78
94
|
|
|
79
95
|
def _add_global_route(args, kwargs, func, method: str):
|
|
80
|
-
fill_type_hints(func, use_literals=True)
|
|
81
|
-
|
|
82
96
|
module_name = func.__module__.lstrip("src.app")
|
|
83
97
|
|
|
84
98
|
# Prepend module name to path
|
|
@@ -87,7 +101,10 @@ def _add_global_route(args, kwargs, func, method: str):
|
|
|
87
101
|
# TODO: Make this configurable
|
|
88
102
|
"/api/"
|
|
89
103
|
# TODO: Make this read project configuration, probably need to add routes after running
|
|
90
|
-
+ module_name.rsplit(".", 1)[0]
|
|
104
|
+
+ module_name.rsplit(".", 1)[0]
|
|
105
|
+
.replace(".", "/")
|
|
106
|
+
.replace(".-", ".") # Strip - from folders (for dev only)
|
|
107
|
+
.lstrip("-")
|
|
91
108
|
+ ("" if path.startswith("/") else "/")
|
|
92
109
|
+ path
|
|
93
110
|
)
|
|
@@ -96,18 +113,11 @@ def _add_global_route(args, kwargs, func, method: str):
|
|
|
96
113
|
else:
|
|
97
114
|
kwargs["path"] = path
|
|
98
115
|
|
|
99
|
-
return
|
|
100
|
-
*args,
|
|
101
|
-
**{
|
|
102
|
-
**kwargs,
|
|
103
|
-
"summary": (kwargs.get("summary", "") + f" ({module_name})").strip(),
|
|
104
|
-
"methods": [method],
|
|
105
|
-
},
|
|
106
|
-
)(func)
|
|
116
|
+
return global_routes.append(ColocoRoute(args, kwargs, func, method, module_name))
|
|
107
117
|
|
|
108
118
|
|
|
109
|
-
def _make_route_decorator(method: str):
|
|
110
|
-
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]:
|
|
111
121
|
def handler_wrapper(func):
|
|
112
122
|
return _add_global_route(args, kwargs, func, method)
|
|
113
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
|
|
|
@@ -17,10 +21,10 @@ class ColocoApp:
|
|
|
17
21
|
name: str
|
|
18
22
|
database_url: str = None
|
|
19
23
|
orm_config: dict = None
|
|
20
|
-
migrations_dir: str = "
|
|
24
|
+
migrations_dir: str = "+migrations"
|
|
21
25
|
|
|
22
26
|
|
|
23
|
-
def discover_files(directory, name):
|
|
27
|
+
def discover_files(directory, name, is_dev=False):
|
|
24
28
|
api_files = []
|
|
25
29
|
try:
|
|
26
30
|
with os.scandir(directory) as entries:
|
|
@@ -29,12 +33,12 @@ def discover_files(directory, name):
|
|
|
29
33
|
# Skip directories starting with "+" and "node_modules"
|
|
30
34
|
if (
|
|
31
35
|
not entry.name.startswith("+")
|
|
32
|
-
and not entry.name.startswith("-")
|
|
36
|
+
and (not entry.name.startswith("-") or is_dev)
|
|
33
37
|
and not entry.name.startswith(".")
|
|
34
38
|
and not entry.name == "node_modules"
|
|
35
39
|
and not entry.name == "coloco"
|
|
36
40
|
):
|
|
37
|
-
api_files.extend(discover_files(entry.path, name))
|
|
41
|
+
api_files.extend(discover_files(entry.path, name, is_dev))
|
|
38
42
|
elif entry.is_file() and entry.name == name:
|
|
39
43
|
api_files.append(entry.path)
|
|
40
44
|
except (PermissionError, FileNotFoundError) as e:
|
|
@@ -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,24 +71,47 @@ 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
|
-
database_url,
|
|
83
|
+
database_url,
|
|
84
|
+
model_files=discover_files(src_path, name="models.py", is_dev=mode == "dev"),
|
|
78
85
|
)
|
|
79
|
-
|
|
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)
|
|
111
|
+
|
|
112
|
+
if database_url:
|
|
113
|
+
register_db_lifecycle(CURRENT_APP)
|
|
114
|
+
|
|
86
115
|
return CURRENT_APP
|
|
87
116
|
|
|
88
117
|
|
|
@@ -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,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import cyclopts
|
|
2
2
|
import os
|
|
3
3
|
from rich import print
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
6
|
|
|
7
|
-
app =
|
|
7
|
+
app = cyclopts.App()
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def _run_npm(command):
|
|
@@ -13,14 +13,14 @@ def _run_npm(command):
|
|
|
13
13
|
print(
|
|
14
14
|
"[red]Error: package.json not found. Please ensure you are in a coloco project directory.[/red]"
|
|
15
15
|
)
|
|
16
|
-
raise
|
|
16
|
+
raise SystemExit(1)
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
19
|
# run npm install
|
|
20
20
|
subprocess.run(command, cwd=".")
|
|
21
21
|
except Exception as e:
|
|
22
22
|
print(f"[red]Error: {e}[/red]")
|
|
23
|
-
raise
|
|
23
|
+
raise SystemExit(1)
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def _setup_dev_env():
|
|
@@ -63,8 +63,8 @@ def dev():
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
@app.command()
|
|
66
|
-
def build(dir: str = None):
|
|
66
|
+
def build(dir: str | None = None):
|
|
67
67
|
"""Runs the node dev server"""
|
|
68
68
|
print("[green]Building node app...[/green]")
|
|
69
69
|
|
|
70
|
-
subprocess.run(["npm", "run", "build"], cwd=".")
|
|
70
|
+
subprocess.run(["npm", "run", "build", *(["--", "--outDir", dir] if dir else [])], cwd=".")
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
from rich import print
|
|
2
1
|
from .api import _serve
|
|
3
|
-
import
|
|
4
|
-
import typer
|
|
5
|
-
import uvicorn
|
|
2
|
+
from ..config import get_coloco_config
|
|
6
3
|
|
|
7
4
|
|
|
8
5
|
def serve(
|
|
9
|
-
app: str =
|
|
6
|
+
app: str | None = None,
|
|
10
7
|
port: int = 80,
|
|
11
8
|
host: str = "0.0.0.0",
|
|
12
9
|
log_level: str = "info",
|
|
13
10
|
):
|
|
11
|
+
if not app:
|
|
12
|
+
app = get_coloco_config().get("app") or "src.main.app"
|
|
13
|
+
|
|
14
14
|
return _serve(app=app, host=host, port=port, log_level=log_level, mode="prod")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from .api import ColocoRoute
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from .lifespan import register_lifespan
|
|
6
|
+
from rich import print
|
|
7
|
+
from type_less import replace_type_hint_map_deep
|
|
8
|
+
from typing import Callable, Type, TypeVar, Optional, Any
|
|
9
|
+
from pydantic import ConfigDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
CLS = TypeVar("CLS")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
IMPORT_ERROR = (
|
|
16
|
+
"[red]Tortoise is not installed. "
|
|
17
|
+
"Please install it with `uv add tortoise-orm`. "
|
|
18
|
+
"If you intend to use anything other than sqlite, "
|
|
19
|
+
"you will need to install the appropriate database driver as well "
|
|
20
|
+
"(e.g. `uv add tortoise-orm[asyncpg]` for postgres).[/red]"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def app_class_to_table_name(cls):
|
|
25
|
+
try:
|
|
26
|
+
app_name = cls.__module__.split(".")[-2]
|
|
27
|
+
except IndexError:
|
|
28
|
+
raise ValueError(f"Could not determine app name for model {cls}")
|
|
29
|
+
return f"{app_name}_{cls.__name__.lower()}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_orm_config(database_url: str, model_files: list[str]):
|
|
33
|
+
model_modules = [
|
|
34
|
+
model_file.replace("./", "").replace("/", ".").replace(".py", "")
|
|
35
|
+
for model_file in model_files
|
|
36
|
+
]
|
|
37
|
+
app_to_models = defaultdict(list)
|
|
38
|
+
for model_module in model_modules:
|
|
39
|
+
app = model_module.lstrip("src.app.").split(".")[0]
|
|
40
|
+
app_to_models[app].append(model_module)
|
|
41
|
+
return {
|
|
42
|
+
"connections": {"default": database_url},
|
|
43
|
+
"table_name_generator": app_class_to_table_name,
|
|
44
|
+
"apps": {
|
|
45
|
+
app: {
|
|
46
|
+
"models": [
|
|
47
|
+
*models,
|
|
48
|
+
],
|
|
49
|
+
"default_connection": "default",
|
|
50
|
+
}
|
|
51
|
+
for app, models in app_to_models.items()
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def init_tortoise(app):
|
|
57
|
+
try:
|
|
58
|
+
from tortoise import Tortoise
|
|
59
|
+
except ImportError:
|
|
60
|
+
print(IMPORT_ERROR)
|
|
61
|
+
raise
|
|
62
|
+
await Tortoise.init(config=app.orm_config, table_name_generator=app_class_to_table_name)
|
|
63
|
+
return Tortoise.close_connections
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def register_db_lifecycle(app):
|
|
67
|
+
@asynccontextmanager
|
|
68
|
+
async def lifecycle_connect_database(api):
|
|
69
|
+
print("[green]Connecting to database...[/green]")
|
|
70
|
+
close_connections = await init_tortoise(app)
|
|
71
|
+
print("[green]Database ready[/green]")
|
|
72
|
+
yield
|
|
73
|
+
print("[yellow]Closing database connection...[/yellow]")
|
|
74
|
+
await close_connections()
|
|
75
|
+
|
|
76
|
+
# Register DB Connection
|
|
77
|
+
register_lifespan(lifecycle_connect_database)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def inject_model_serializers(orm_config: dict, routes: list[ColocoRoute]):
|
|
81
|
+
try:
|
|
82
|
+
from tortoise import Tortoise
|
|
83
|
+
from tortoise.contrib.pydantic import pydantic_model_creator
|
|
84
|
+
except ImportError:
|
|
85
|
+
print(IMPORT_ERROR)
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
# TODO: find out if we can init the config without initing the DB
|
|
89
|
+
Tortoise.table_name_generator = app_class_to_table_name
|
|
90
|
+
for label, _app in orm_config["apps"].items():
|
|
91
|
+
for model in _app["models"]:
|
|
92
|
+
Tortoise.init_models([model], label)
|
|
93
|
+
|
|
94
|
+
# Auto-create serializers
|
|
95
|
+
# TODO: configurable
|
|
96
|
+
model_classes = [cls for models in Tortoise.apps.values() for cls in models.values()]
|
|
97
|
+
model_to_serializer = {}
|
|
98
|
+
for model_class in model_classes:
|
|
99
|
+
# Wrapped with @serializable
|
|
100
|
+
if getattr(model_class, "__model_serializer_create", None):
|
|
101
|
+
model_class.__model_serializer = pydantic_model_creator(
|
|
102
|
+
model_class, **model_class.__model_serializer_create
|
|
103
|
+
)
|
|
104
|
+
# Create a general serializer
|
|
105
|
+
elif not getattr(model_class, "__model_serializer", None):
|
|
106
|
+
model_class.__model_serializer = pydantic_model_creator(model_class)
|
|
107
|
+
model_to_serializer[model_class] = model_class.__model_serializer
|
|
108
|
+
|
|
109
|
+
for route in routes:
|
|
110
|
+
route.func.__annotations__["return"], occurrences = replace_type_hint_map_deep(
|
|
111
|
+
route.func.__annotations__["return"],
|
|
112
|
+
model_to_serializer,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Magical model adapting (when models detected)
|
|
116
|
+
# TODO: find a better way to do this than wrapping every route
|
|
117
|
+
# We need to async eval the serialization
|
|
118
|
+
# - Middleware - can't catch pre-serialized values
|
|
119
|
+
# - Pydantic - sync serialization only
|
|
120
|
+
# - JSON Encoder - sync serialization only, + happens after serialization
|
|
121
|
+
if occurrences:
|
|
122
|
+
route.func = _wrap_model_serializer(route.func)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def _serialize_models(obj):
|
|
126
|
+
"""
|
|
127
|
+
Auto-runs the serializer for tortoise models
|
|
128
|
+
"""
|
|
129
|
+
if hasattr(obj, "__model_serializer"):
|
|
130
|
+
return await obj.__model_serializer.from_tortoise_orm(obj)
|
|
131
|
+
elif isinstance(obj, list):
|
|
132
|
+
return [await _serialize_models(item) for item in obj]
|
|
133
|
+
elif isinstance(obj, dict):
|
|
134
|
+
return {k: await _serialize_models(v) for k, v in obj.items()}
|
|
135
|
+
return obj
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _wrap_model_serializer(func):
|
|
139
|
+
@wraps(func)
|
|
140
|
+
async def wrapper(*args, **kwargs):
|
|
141
|
+
return await _serialize_models(await func(*args, **kwargs))
|
|
142
|
+
|
|
143
|
+
return wrapper
|
|
@@ -4,9 +4,9 @@ from fastapi.staticfiles import StaticFiles
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
# Static Files
|
|
7
|
-
def bind_static(api: FastAPI, dist_dir: str = "
|
|
8
|
-
api.mount("/assets", StaticFiles(directory=f"{dist_dir}/
|
|
7
|
+
def bind_static(api: FastAPI, dist_dir: str = "."):
|
|
8
|
+
api.mount("/assets", StaticFiles(directory=f"{dist_dir}/static/assets"), name="static")
|
|
9
9
|
|
|
10
10
|
@api.get("/{all_paths:path}")
|
|
11
11
|
async def serve_app(all_paths: str):
|
|
12
|
-
return FileResponse(f"{dist_dir}/
|
|
12
|
+
return FileResponse(f"{dist_dir}/static/index.html")
|
|
@@ -1,22 +0,0 @@
|
|
|
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
|
-
)
|
|
@@ -1,100 +0,0 @@
|
|
|
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
|
-
|
coloco-0.3.4/src/coloco/db.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|