coasti-planner 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coasti_planner-0.1.0/.gitignore +31 -0
- coasti_planner-0.1.0/.python-version +1 -0
- coasti_planner-0.1.0/PKG-INFO +129 -0
- coasti_planner-0.1.0/README.md +115 -0
- coasti_planner-0.1.0/pyproject.toml +128 -0
- coasti_planner-0.1.0/src/coasti_planner/__init__.py +0 -0
- coasti_planner-0.1.0/src/coasti_planner/__main__.py +54 -0
- coasti_planner-0.1.0/src/coasti_planner/config.py +80 -0
- coasti_planner-0.1.0/src/coasti_planner/database.py +23 -0
- coasti_planner-0.1.0/src/coasti_planner/dependencies.py +27 -0
- coasti_planner-0.1.0/src/coasti_planner/exceptions.py +25 -0
- coasti_planner-0.1.0/src/coasti_planner/models.py +102 -0
- coasti_planner-0.1.0/src/coasti_planner/routes/__init__.py +1 -0
- coasti_planner-0.1.0/src/coasti_planner/routes/api_v1/__init__.py +1 -0
- coasti_planner-0.1.0/src/coasti_planner/routes/api_v1/dummy.py +33 -0
- coasti_planner-0.1.0/src/coasti_planner/routes/api_v1/extension.py +29 -0
- coasti_planner-0.1.0/src/coasti_planner/routes/api_v1/filter.py +21 -0
- coasti_planner-0.1.0/src/coasti_planner/routes/api_v1/log.py +36 -0
- coasti_planner-0.1.0/src/coasti_planner/routes/api_v1/router.py +18 -0
- coasti_planner-0.1.0/src/coasti_planner/routes/api_v1/table.py +40 -0
- coasti_planner-0.1.0/src/coasti_planner/routes/framework.py +66 -0
- coasti_planner-0.1.0/src/coasti_planner/server.py +35 -0
- coasti_planner-0.1.0/src/coasti_planner/sql_io.py +201 -0
- coasti_planner-0.1.0/tests/conftest.py +43 -0
- coasti_planner-0.1.0/tests/test_main.py +52 -0
- coasti_planner-0.1.0/tests/test_server.py +214 -0
- coasti_planner-0.1.0/tests/test_sql_io.py +103 -0
- coasti_planner-0.1.0/uv.lock +1182 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.mypy_cache/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.coverage
|
|
10
|
+
htmlcov/
|
|
11
|
+
coverage.xml
|
|
12
|
+
dist/
|
|
13
|
+
build/
|
|
14
|
+
|
|
15
|
+
# Environment / secrets
|
|
16
|
+
.env
|
|
17
|
+
.env.*
|
|
18
|
+
!.env.example
|
|
19
|
+
|
|
20
|
+
# Node (frontend)
|
|
21
|
+
node_modules/
|
|
22
|
+
|
|
23
|
+
# Nix / direnv
|
|
24
|
+
.direnv/
|
|
25
|
+
|
|
26
|
+
# OS / editor
|
|
27
|
+
.DS_Store
|
|
28
|
+
|
|
29
|
+
# Module Federation
|
|
30
|
+
@mf-types
|
|
31
|
+
.mf
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: coasti-planner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Backend service for the Coasti Planning Framework.
|
|
5
|
+
Author-email: "Sebastian B. Mohr" <sebastian@mohrenclan.de>, "F. Paul Spitzner" <paul.spitzner@linkfish.eu>
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Requires-Dist: eyconf[pydantic]>=0.8.0
|
|
8
|
+
Requires-Dist: fastapi>=0.115.0
|
|
9
|
+
Requires-Dist: ruamel-yaml>=0.19.1
|
|
10
|
+
Requires-Dist: sqlalchemy>=2.0.0
|
|
11
|
+
Requires-Dist: typer>=0.27.3
|
|
12
|
+
Requires-Dist: uvicorn[standard]>=0.34.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Coasti Planning Framework — Backend
|
|
16
|
+
|
|
17
|
+
The backend is a Python 3.13 FastAPI application managed with
|
|
18
|
+
[uv](https://docs.astral.sh/uv/). It serves the framework and content-package
|
|
19
|
+
assets from local Vite build directories.
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
Run this command from the repository root:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv sync --directory backend --locked
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The repository root also provides a Nix/direnv development shell that installs
|
|
30
|
+
the required tools automatically. See the root [README](../README.md).
|
|
31
|
+
|
|
32
|
+
## Build the frontend distributions
|
|
33
|
+
|
|
34
|
+
The backend does not build frontend assets itself. Build the framework and the
|
|
35
|
+
example extension before running the backend in local distribution mode.
|
|
36
|
+
|
|
37
|
+
### Framework distribution
|
|
38
|
+
|
|
39
|
+
Install the frontend workspace dependencies and build the planner framework:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm --dir frontend install --frozen-lockfile
|
|
43
|
+
pnpm --dir frontend build
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This creates the framework distribution in `frontend/planner/dist`.
|
|
47
|
+
|
|
48
|
+
### Example extension distribution
|
|
49
|
+
|
|
50
|
+
Install the extension dependencies and build the example content package:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pnpm --dir cp_example_extension install --frozen-lockfile
|
|
54
|
+
pnpm --dir cp_example_extension build
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This creates the extension distribution in `cp_example_extension/dist`. The
|
|
58
|
+
build includes the generated `manifest.json` and Module Federation entry point.
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
The server accepts a `server` command and an optional YAML configuration path.
|
|
63
|
+
The legacy form with only the configuration path remains supported.
|
|
64
|
+
|
|
65
|
+
The example configuration is available at
|
|
66
|
+
[config.example.yml](../cp_example_extension/config.example.yml):
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
backend:
|
|
70
|
+
host: 127.0.0.1
|
|
71
|
+
port: 8000
|
|
72
|
+
framework:
|
|
73
|
+
dist_folder: ../frontend/planner/dist
|
|
74
|
+
extension:
|
|
75
|
+
dist_folder: ./dist
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Configuration is loaded by [EYConf](https://eyconf.readthedocs.io/) using its
|
|
79
|
+
Pydantic validation backend. The configuration is represented by three pairs of
|
|
80
|
+
schema/runtime types in `coasti_planner.config`:
|
|
81
|
+
|
|
82
|
+
- `BackendConfigSchema` / `BackendConfig` define the listening host and port.
|
|
83
|
+
- `AssetSourceSchema` / `AssetSource` define a local distribution or remote
|
|
84
|
+
development server.
|
|
85
|
+
- `ServerConfigSchema` / `ServerConfig` combine the backend, framework, and
|
|
86
|
+
extension settings.
|
|
87
|
+
|
|
88
|
+
Each asset source defines a local `dist_folder`. Relative paths are resolved
|
|
89
|
+
relative to the configuration file, not relative to the current working
|
|
90
|
+
directory.
|
|
91
|
+
|
|
92
|
+
## Run the backend
|
|
93
|
+
|
|
94
|
+
From the repository root, after building both distributions:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
uv run --directory backend coasti-planner ../cp_example_extension/config.example.yml
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Run the full stack in development mode
|
|
101
|
+
|
|
102
|
+
Development mode does not require either frontend distribution directory. Run
|
|
103
|
+
the backend from the backend project directory:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cd backend
|
|
107
|
+
uv run backend server --dev
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The backend uses Uvicorn autoreload and the example configuration by default.
|
|
111
|
+
Run the Vite development servers in separate terminals:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pnpm --dir frontend dev
|
|
115
|
+
pnpm --dir cp_example_extension dev
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The frontend development server proxies API requests to the autoreloading
|
|
119
|
+
backend, while the frontend and extension servers reload their own source files.
|
|
120
|
+
|
|
121
|
+
## HTTP endpoints
|
|
122
|
+
|
|
123
|
+
- `/` serves the framework `index.html` and its root-relative assets.
|
|
124
|
+
- `/api_v1/framework/{path}` serves framework distribution files.
|
|
125
|
+
- `/api_v1/extension/{path}` serves extension distribution files.
|
|
126
|
+
- `/api_v1/extension/manifest.json` serves the extension's generated manifest.
|
|
127
|
+
|
|
128
|
+
The manifest is read from the extension distribution. It is not duplicated in
|
|
129
|
+
the backend.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Coasti Planning Framework — Backend
|
|
2
|
+
|
|
3
|
+
The backend is a Python 3.13 FastAPI application managed with
|
|
4
|
+
[uv](https://docs.astral.sh/uv/). It serves the framework and content-package
|
|
5
|
+
assets from local Vite build directories.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
Run this command from the repository root:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv sync --directory backend --locked
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The repository root also provides a Nix/direnv development shell that installs
|
|
16
|
+
the required tools automatically. See the root [README](../README.md).
|
|
17
|
+
|
|
18
|
+
## Build the frontend distributions
|
|
19
|
+
|
|
20
|
+
The backend does not build frontend assets itself. Build the framework and the
|
|
21
|
+
example extension before running the backend in local distribution mode.
|
|
22
|
+
|
|
23
|
+
### Framework distribution
|
|
24
|
+
|
|
25
|
+
Install the frontend workspace dependencies and build the planner framework:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm --dir frontend install --frozen-lockfile
|
|
29
|
+
pnpm --dir frontend build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This creates the framework distribution in `frontend/planner/dist`.
|
|
33
|
+
|
|
34
|
+
### Example extension distribution
|
|
35
|
+
|
|
36
|
+
Install the extension dependencies and build the example content package:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm --dir cp_example_extension install --frozen-lockfile
|
|
40
|
+
pnpm --dir cp_example_extension build
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This creates the extension distribution in `cp_example_extension/dist`. The
|
|
44
|
+
build includes the generated `manifest.json` and Module Federation entry point.
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
The server accepts a `server` command and an optional YAML configuration path.
|
|
49
|
+
The legacy form with only the configuration path remains supported.
|
|
50
|
+
|
|
51
|
+
The example configuration is available at
|
|
52
|
+
[config.example.yml](../cp_example_extension/config.example.yml):
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
backend:
|
|
56
|
+
host: 127.0.0.1
|
|
57
|
+
port: 8000
|
|
58
|
+
framework:
|
|
59
|
+
dist_folder: ../frontend/planner/dist
|
|
60
|
+
extension:
|
|
61
|
+
dist_folder: ./dist
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Configuration is loaded by [EYConf](https://eyconf.readthedocs.io/) using its
|
|
65
|
+
Pydantic validation backend. The configuration is represented by three pairs of
|
|
66
|
+
schema/runtime types in `coasti_planner.config`:
|
|
67
|
+
|
|
68
|
+
- `BackendConfigSchema` / `BackendConfig` define the listening host and port.
|
|
69
|
+
- `AssetSourceSchema` / `AssetSource` define a local distribution or remote
|
|
70
|
+
development server.
|
|
71
|
+
- `ServerConfigSchema` / `ServerConfig` combine the backend, framework, and
|
|
72
|
+
extension settings.
|
|
73
|
+
|
|
74
|
+
Each asset source defines a local `dist_folder`. Relative paths are resolved
|
|
75
|
+
relative to the configuration file, not relative to the current working
|
|
76
|
+
directory.
|
|
77
|
+
|
|
78
|
+
## Run the backend
|
|
79
|
+
|
|
80
|
+
From the repository root, after building both distributions:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
uv run --directory backend coasti-planner ../cp_example_extension/config.example.yml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Run the full stack in development mode
|
|
87
|
+
|
|
88
|
+
Development mode does not require either frontend distribution directory. Run
|
|
89
|
+
the backend from the backend project directory:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cd backend
|
|
93
|
+
uv run backend server --dev
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The backend uses Uvicorn autoreload and the example configuration by default.
|
|
97
|
+
Run the Vite development servers in separate terminals:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pnpm --dir frontend dev
|
|
101
|
+
pnpm --dir cp_example_extension dev
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The frontend development server proxies API requests to the autoreloading
|
|
105
|
+
backend, while the frontend and extension servers reload their own source files.
|
|
106
|
+
|
|
107
|
+
## HTTP endpoints
|
|
108
|
+
|
|
109
|
+
- `/` serves the framework `index.html` and its root-relative assets.
|
|
110
|
+
- `/api_v1/framework/{path}` serves framework distribution files.
|
|
111
|
+
- `/api_v1/extension/{path}` serves extension distribution files.
|
|
112
|
+
- `/api_v1/extension/manifest.json` serves the extension's generated manifest.
|
|
113
|
+
|
|
114
|
+
The manifest is read from the extension distribution. It is not duplicated in
|
|
115
|
+
the backend.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "coasti-planner"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Backend service for the Coasti Planning Framework."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Sebastian B. Mohr", email = "sebastian@mohrenclan.de" },
|
|
8
|
+
{ name = "F. Paul Spitzner", email = "paul.spitzner@linkfish.eu" },
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"eyconf[pydantic]>=0.8.0",
|
|
13
|
+
"fastapi>=0.115.0",
|
|
14
|
+
"ruamel-yaml>=0.19.1",
|
|
15
|
+
"sqlalchemy>=2.0.0",
|
|
16
|
+
"typer>=0.27.3",
|
|
17
|
+
"uvicorn[standard]>=0.34.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
coasti-planner = "coasti_planner.__main__:app"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/coasti_planner"]
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
{ include-group = "lint" },
|
|
33
|
+
{ include-group = "test" },
|
|
34
|
+
{ include-group = "typed" },
|
|
35
|
+
"pre-commit>=4.5.1",
|
|
36
|
+
]
|
|
37
|
+
lint = [
|
|
38
|
+
"ruff"
|
|
39
|
+
]
|
|
40
|
+
typed = [
|
|
41
|
+
"mypy>=1.19.1",
|
|
42
|
+
]
|
|
43
|
+
test = [
|
|
44
|
+
"pytest>=9.0.2",
|
|
45
|
+
"pytest-cov>=7.1.0",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
target-version = "py313"
|
|
50
|
+
src = ["src", "tests"]
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
future-annotations = true
|
|
54
|
+
select = [
|
|
55
|
+
"A", # flake8-builtins
|
|
56
|
+
"ANN", # flake8-annotations
|
|
57
|
+
"B", # flake8-bugbear
|
|
58
|
+
"C4", # flake8-comprehensions
|
|
59
|
+
"E", # pycodestyle
|
|
60
|
+
"F", # pyflakes
|
|
61
|
+
"I", # isort
|
|
62
|
+
"ISC", # flake8-implicit-str-concat
|
|
63
|
+
"N", # pep8-naming
|
|
64
|
+
"PIE", # flake8-pie
|
|
65
|
+
"PT", # flake8-pytest-style
|
|
66
|
+
"RUF", # ruff
|
|
67
|
+
"RET", # flake8-return
|
|
68
|
+
"SIM", # flake8-simplify
|
|
69
|
+
"UP", # pyupgrade
|
|
70
|
+
"TC", # flake8-type-checking
|
|
71
|
+
"PTH", # Use pathlib instead of os.path
|
|
72
|
+
"D", # pydocstyle
|
|
73
|
+
"W", # pycodestyle
|
|
74
|
+
]
|
|
75
|
+
ignore = [
|
|
76
|
+
# Docstrings are not mandatory for public functions.
|
|
77
|
+
"D10",
|
|
78
|
+
"D202",
|
|
79
|
+
"TC006", # no need to quote 'cast's since we use 'from __future__ import annotations'
|
|
80
|
+
]
|
|
81
|
+
fixable = ["ALL"]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint.flake8-type-checking]
|
|
84
|
+
# FastAPI route decorators evaluate annotations at import time, so dependency
|
|
85
|
+
# imports used in endpoints must remain runtime imports.
|
|
86
|
+
runtime-evaluated-decorators = [
|
|
87
|
+
"fastapi.APIRouter.delete",
|
|
88
|
+
"fastapi.APIRouter.get",
|
|
89
|
+
"fastapi.APIRouter.head",
|
|
90
|
+
"fastapi.APIRouter.options",
|
|
91
|
+
"fastapi.APIRouter.patch",
|
|
92
|
+
"fastapi.APIRouter.post",
|
|
93
|
+
"fastapi.APIRouter.put",
|
|
94
|
+
"fastapi.APIRouter.trace",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.per-file-ignores]
|
|
98
|
+
"tests/**" = [
|
|
99
|
+
"ANN", # full annotations on tests add noise; mypy still checks them
|
|
100
|
+
"D",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
[tool.ruff.lint.isort]
|
|
104
|
+
known-first-party = ["coasti_planner"]
|
|
105
|
+
|
|
106
|
+
[tool.mypy]
|
|
107
|
+
files = ["src", "tests"]
|
|
108
|
+
python_version = "3.13"
|
|
109
|
+
mypy_path = ["src"]
|
|
110
|
+
warn_unreachable = true
|
|
111
|
+
strict = true
|
|
112
|
+
check_untyped_defs = true
|
|
113
|
+
disallow_untyped_decorators = true
|
|
114
|
+
allow_redefinition = true
|
|
115
|
+
warn_unused_configs = true
|
|
116
|
+
|
|
117
|
+
[tool.pytest.ini_options]
|
|
118
|
+
testpaths = ["tests"]
|
|
119
|
+
addopts = "-ra --strict-markers --strict-config"
|
|
120
|
+
|
|
121
|
+
[tool.coverage.run]
|
|
122
|
+
source = ["coasti_planner"]
|
|
123
|
+
omit = ["*/__main__.py"]
|
|
124
|
+
branch = true
|
|
125
|
+
|
|
126
|
+
[tool.coverage.report]
|
|
127
|
+
fail_under = 0
|
|
128
|
+
show_missing = true
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Coasti Planner backend package and CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
import uvicorn
|
|
11
|
+
|
|
12
|
+
from coasti_planner.config import Config
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Run the Coasti Planner backend.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command()
|
|
18
|
+
def server(
|
|
19
|
+
config_file: Annotated[
|
|
20
|
+
Path | None,
|
|
21
|
+
typer.Argument(help="Path to the YAML config file"),
|
|
22
|
+
] = None,
|
|
23
|
+
dev: Annotated[
|
|
24
|
+
bool,
|
|
25
|
+
typer.Option(
|
|
26
|
+
help="Run with Uvicorn autoreload and without distribution assets",
|
|
27
|
+
),
|
|
28
|
+
] = False,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Start the backend server."""
|
|
31
|
+
config_file = config_file or Path.cwd() / "config.yml"
|
|
32
|
+
if not config_file.is_file():
|
|
33
|
+
raise FileNotFoundError(f"Configuration file does not exist: {config_file}")
|
|
34
|
+
|
|
35
|
+
# We just validate the config here to raise early
|
|
36
|
+
resolved_config_file = config_file.resolve()
|
|
37
|
+
|
|
38
|
+
# setting the env var propagates the config file to the server init in server.py
|
|
39
|
+
os.environ["EYCONF_CONFIG_FILE"] = str(resolved_config_file)
|
|
40
|
+
config = Config()
|
|
41
|
+
|
|
42
|
+
if dev:
|
|
43
|
+
print("Running uvicorn in dev mode with live reloading")
|
|
44
|
+
|
|
45
|
+
uvicorn.run(
|
|
46
|
+
"coasti_planner.server:app",
|
|
47
|
+
host=config.backend.host,
|
|
48
|
+
port=config.backend.port,
|
|
49
|
+
reload=dev,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
app()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Runtime configuration schema and loading for the backend."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from eyconf import EYConf
|
|
7
|
+
from eyconf.validation.backends.pydantic import PydanticValidator
|
|
8
|
+
from sqlalchemy.engine import make_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class BackendConfig:
|
|
13
|
+
host: str = "127.0.0.1"
|
|
14
|
+
port: int = 8000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DatabaseConfig:
|
|
19
|
+
connection_string: str = "sqlite:///:memory:"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AssetSource:
|
|
24
|
+
"""Configuration for a local build or a running development server."""
|
|
25
|
+
|
|
26
|
+
dist_folder: Path = field(
|
|
27
|
+
default_factory=lambda: Path(__file__).parent.parent / "dist"
|
|
28
|
+
)
|
|
29
|
+
sql_folder: Path = field(default_factory=lambda: Path("sql"))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ServerConfigSchema:
|
|
34
|
+
"""Configuration for the framework and content-package assets."""
|
|
35
|
+
|
|
36
|
+
backend: BackendConfig = field(default_factory=BackendConfig)
|
|
37
|
+
database: DatabaseConfig = field(default_factory=DatabaseConfig)
|
|
38
|
+
framework: AssetSource = field(default_factory=AssetSource)
|
|
39
|
+
extension: AssetSource = field(default_factory=AssetSource)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Config(EYConf[ServerConfigSchema]):
|
|
43
|
+
"""Configuration for the Coasti Planner backend."""
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
super().__init__(ServerConfigSchema, validator=PydanticValidator())
|
|
47
|
+
|
|
48
|
+
# absolute paths for dist bundles
|
|
49
|
+
for source in (self.data.framework, self.data.extension):
|
|
50
|
+
if not source.dist_folder.is_absolute():
|
|
51
|
+
source.dist_folder = (
|
|
52
|
+
self.get_file().parent / source.dist_folder
|
|
53
|
+
).resolve()
|
|
54
|
+
if not source.sql_folder.is_absolute():
|
|
55
|
+
source.sql_folder = (
|
|
56
|
+
self.get_file().parent / source.sql_folder
|
|
57
|
+
).resolve()
|
|
58
|
+
|
|
59
|
+
# abs path for sqlite database
|
|
60
|
+
database_url = make_url(self.data.database.connection_string)
|
|
61
|
+
if database_url.drivername == "sqlite" and database_url.database:
|
|
62
|
+
database_path = Path(database_url.database)
|
|
63
|
+
if not database_path.is_absolute() and database_path.name != ":memory:":
|
|
64
|
+
self.data.database.connection_string = str(
|
|
65
|
+
database_url.set(
|
|
66
|
+
database=str(self.get_file().parent / database_path)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def framework(self) -> AssetSource:
|
|
72
|
+
return self.data.framework
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def extension(self) -> AssetSource:
|
|
76
|
+
return self.data.extension
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def backend(self) -> BackendConfig:
|
|
80
|
+
return self.data.backend
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import create_engine, text
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Database:
|
|
9
|
+
"""Execute SQL against the configured database."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, connection_string: str) -> None:
|
|
12
|
+
self._engine = create_engine(connection_string)
|
|
13
|
+
|
|
14
|
+
def execute_query(
|
|
15
|
+
self,
|
|
16
|
+
query: str,
|
|
17
|
+
parameters: dict[str, Any] | None = None,
|
|
18
|
+
) -> tuple[list[str], list[dict[str, Any]]]:
|
|
19
|
+
with self._engine.connect() as connection:
|
|
20
|
+
result = connection.execute(text(query), parameters or {})
|
|
21
|
+
columns = list(result.keys())
|
|
22
|
+
rows = [dict(row) for row in result.mappings()]
|
|
23
|
+
return columns, rows
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Dependencies shared by the FastAPI route handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated, cast
|
|
6
|
+
|
|
7
|
+
from fastapi import Depends, Request
|
|
8
|
+
|
|
9
|
+
from coasti_planner.config import Config
|
|
10
|
+
from coasti_planner.database import Database
|
|
11
|
+
from coasti_planner.sql_io import SQLIO
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_config(request: Request) -> Config:
|
|
15
|
+
"""Return the configuration stored on the application state."""
|
|
16
|
+
return cast("Config", request.app.state.config)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_sqlio(request: Request, config: ConfigDep) -> SQLIO:
|
|
20
|
+
"""Load and return the SQL interaction layer."""
|
|
21
|
+
|
|
22
|
+
database = Database(config.data.database.connection_string)
|
|
23
|
+
return SQLIO(config.data.extension.sql_folder, database)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ConfigDep = Annotated[Config, Depends(get_config)]
|
|
27
|
+
SqlioDep = Annotated[SQLIO, Depends(get_sqlio)]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Serialization of backend exceptions for the frontend error format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SerializedException(BaseModel):
|
|
11
|
+
type: str
|
|
12
|
+
message: str
|
|
13
|
+
description: str | None
|
|
14
|
+
trace: str | None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def to_serialized_exception(exception: Exception) -> SerializedException:
|
|
18
|
+
"""Convert a backend exception into the frontend error format."""
|
|
19
|
+
trace = "".join(traceback.format_exception(exception))
|
|
20
|
+
return SerializedException(
|
|
21
|
+
type=type(exception).__name__,
|
|
22
|
+
message=str(exception),
|
|
23
|
+
description=exception.__doc__,
|
|
24
|
+
trace=trace,
|
|
25
|
+
)
|