vibetuner 2.30.1__py3-none-any.whl → 2.44.1__py3-none-any.whl
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.
Potentially problematic release.
This version of vibetuner might be problematic. Click here for more details.
- vibetuner/cli/__init__.py +2 -0
- vibetuner/cli/db.py +40 -0
- vibetuner/cli/run.py +126 -110
- vibetuner/cli/scaffold.py +5 -23
- vibetuner/config.py +42 -2
- vibetuner/frontend/lifespan.py +7 -2
- vibetuner/frontend/middleware.py +0 -25
- vibetuner/frontend/proxy.py +14 -0
- vibetuner/frontend/templates.py +34 -0
- vibetuner/mongo.py +55 -15
- vibetuner/sqlmodel.py +109 -0
- vibetuner/tasks/lifespan.py +7 -2
- vibetuner/tasks/worker.py +9 -4
- vibetuner/templates/frontend/email_sent.html.jinja +1 -1
- vibetuner/templates/frontend/index.html.jinja +2 -2
- vibetuner/templates/frontend/login.html.jinja +1 -1
- vibetuner/templates/frontend/user/edit.html.jinja +1 -1
- vibetuner/templates/frontend/user/profile.html.jinja +1 -1
- {vibetuner-2.30.1.dist-info → vibetuner-2.44.1.dist-info}/METADATA +33 -26
- {vibetuner-2.30.1.dist-info → vibetuner-2.44.1.dist-info}/RECORD +22 -19
- {vibetuner-2.30.1.dist-info → vibetuner-2.44.1.dist-info}/WHEEL +1 -1
- {vibetuner-2.30.1.dist-info → vibetuner-2.44.1.dist-info}/entry_points.txt +0 -0
vibetuner/cli/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ import typer
|
|
|
10
10
|
from rich.console import Console
|
|
11
11
|
from rich.table import Table
|
|
12
12
|
|
|
13
|
+
from vibetuner.cli.db import db_app
|
|
13
14
|
from vibetuner.cli.run import run_app
|
|
14
15
|
from vibetuner.cli.scaffold import scaffold_app
|
|
15
16
|
from vibetuner.logging import LogLevel, logger, setup_logging
|
|
@@ -109,6 +110,7 @@ def version(
|
|
|
109
110
|
console.print(table)
|
|
110
111
|
|
|
111
112
|
|
|
113
|
+
app.add_typer(db_app, name="db")
|
|
112
114
|
app.add_typer(run_app, name="run")
|
|
113
115
|
app.add_typer(scaffold_app, name="scaffold")
|
|
114
116
|
|
vibetuner/cli/db.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# ABOUTME: Database management CLI commands for SQLModel.
|
|
2
|
+
# ABOUTME: Provides schema creation and other database utilities.
|
|
3
|
+
|
|
4
|
+
import asyncer
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
db_app = typer.Typer(help="Database management commands", no_args_is_help=True)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@db_app.command("create-schema")
|
|
12
|
+
def create_schema_cmd() -> None:
|
|
13
|
+
"""
|
|
14
|
+
Create database tables from SQLModel metadata.
|
|
15
|
+
|
|
16
|
+
This command creates all tables defined in your SQLModel models.
|
|
17
|
+
It's idempotent - existing tables are not modified.
|
|
18
|
+
|
|
19
|
+
Run this during initial setup or after adding new models.
|
|
20
|
+
"""
|
|
21
|
+
from importlib import import_module
|
|
22
|
+
|
|
23
|
+
from vibetuner.logging import logger
|
|
24
|
+
|
|
25
|
+
# Import app.models to register SQLModel tables before schema creation
|
|
26
|
+
try:
|
|
27
|
+
import_module("app.models")
|
|
28
|
+
except ModuleNotFoundError:
|
|
29
|
+
logger.warning("app.models not found. Only core models will be created.")
|
|
30
|
+
except ImportError as e:
|
|
31
|
+
logger.warning(f"Failed to import app.models: {e}")
|
|
32
|
+
|
|
33
|
+
async def _create() -> None:
|
|
34
|
+
from vibetuner.sqlmodel import create_schema
|
|
35
|
+
|
|
36
|
+
await create_schema()
|
|
37
|
+
|
|
38
|
+
typer.echo("Creating database schema...")
|
|
39
|
+
asyncer.runnify(_create)()
|
|
40
|
+
typer.echo("Database schema created successfully.")
|
vibetuner/cli/run.py
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# ABOUTME: Run commands for starting the application in different modes
|
|
2
2
|
# ABOUTME: Supports dev/prod modes for frontend and worker services
|
|
3
|
+
import hashlib
|
|
3
4
|
import os
|
|
4
|
-
from
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Literal
|
|
5
7
|
|
|
6
8
|
import typer
|
|
7
9
|
from rich.console import Console
|
|
8
10
|
|
|
11
|
+
from vibetuner.logging import logger
|
|
12
|
+
|
|
9
13
|
|
|
10
14
|
console = Console()
|
|
11
15
|
|
|
@@ -13,88 +17,148 @@ run_app = typer.Typer(
|
|
|
13
17
|
help="Run the application in different modes", no_args_is_help=True
|
|
14
18
|
)
|
|
15
19
|
|
|
20
|
+
DEFAULT_FRONTEND_PORT = 8000
|
|
21
|
+
DEFAULT_WORKER_PORT = 11111
|
|
16
22
|
|
|
17
|
-
@run_app.command(name="dev")
|
|
18
|
-
def dev(
|
|
19
|
-
service: Annotated[
|
|
20
|
-
str, typer.Argument(help="Service to run: 'frontend' or 'worker'")
|
|
21
|
-
] = "frontend",
|
|
22
|
-
port: int = typer.Option(
|
|
23
|
-
None, help="Port to run on (8000 for frontend, 11111 for worker)"
|
|
24
|
-
),
|
|
25
|
-
host: str = typer.Option("0.0.0.0", help="Host to bind to (frontend only)"), # noqa: S104
|
|
26
|
-
workers_count: int = typer.Option(
|
|
27
|
-
1, "--workers", help="Number of worker processes"
|
|
28
|
-
),
|
|
29
|
-
) -> None:
|
|
30
|
-
"""Run in development mode with hot reload (frontend or worker)."""
|
|
31
|
-
os.environ["DEBUG"] = "1"
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
def _compute_auto_port() -> int:
|
|
25
|
+
"""Compute deterministic port from current directory path."""
|
|
26
|
+
cwd = os.getcwd()
|
|
27
|
+
hash_bytes = hashlib.sha256(cwd.encode()).digest()
|
|
28
|
+
hash_int = int.from_bytes(hash_bytes[:4], "big")
|
|
29
|
+
return 8001 + (hash_int % 999)
|
|
36
30
|
|
|
37
|
-
worker_port = port if port else 11111
|
|
38
|
-
console.print(
|
|
39
|
-
f"[green]Starting worker in dev mode on port {worker_port}[/green]"
|
|
40
|
-
)
|
|
41
|
-
console.print("[dim]Hot reload enabled[/dim]")
|
|
42
31
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
verbose=True,
|
|
54
|
-
web=True,
|
|
55
|
-
host="0.0.0.0", # noqa: S104
|
|
56
|
-
port=worker_port,
|
|
32
|
+
def _run_worker(mode: Literal["dev", "prod"], port: int, workers: int) -> None:
|
|
33
|
+
"""Start the background worker process."""
|
|
34
|
+
from streaq.cli import main as streaq_main
|
|
35
|
+
|
|
36
|
+
from vibetuner.config import settings
|
|
37
|
+
|
|
38
|
+
if not settings.workers_available:
|
|
39
|
+
logger.warning("Redis URL not configured. Workers will not be started.")
|
|
40
|
+
console.print(
|
|
41
|
+
"[red]Error: Redis URL not configured. Workers will not be started.[/red]"
|
|
57
42
|
)
|
|
58
|
-
|
|
59
|
-
# Frontend mode
|
|
60
|
-
from pathlib import Path
|
|
43
|
+
raise typer.Exit(code=0)
|
|
61
44
|
|
|
62
|
-
|
|
63
|
-
from granian.constants import Interfaces
|
|
45
|
+
is_dev = mode == "dev"
|
|
64
46
|
|
|
65
|
-
|
|
47
|
+
if is_dev and workers > 1:
|
|
66
48
|
console.print(
|
|
67
|
-
|
|
49
|
+
"[yellow]Warning: Multiple workers not supported in dev mode, using 1[/yellow]"
|
|
68
50
|
)
|
|
51
|
+
workers = 1
|
|
52
|
+
|
|
53
|
+
console.print(f"[green]Starting worker in {mode} mode on port {port}[/green]")
|
|
54
|
+
if is_dev:
|
|
55
|
+
console.print("[dim]Hot reload enabled[/dim]")
|
|
56
|
+
else:
|
|
57
|
+
console.print(f"[dim]Workers: {workers}[/dim]")
|
|
58
|
+
|
|
59
|
+
streaq_main(
|
|
60
|
+
worker_path="vibetuner.tasks.worker.worker",
|
|
61
|
+
workers=workers,
|
|
62
|
+
reload=is_dev,
|
|
63
|
+
verbose=True if is_dev else settings.debug,
|
|
64
|
+
web=True,
|
|
65
|
+
host="0.0.0.0", # noqa: S104
|
|
66
|
+
port=port,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _run_frontend(
|
|
71
|
+
mode: Literal["dev", "prod"], host: str, port: int, workers: int
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Start the frontend server."""
|
|
74
|
+
from granian import Granian
|
|
75
|
+
from granian.constants import Interfaces
|
|
76
|
+
|
|
77
|
+
is_dev = mode == "dev"
|
|
78
|
+
|
|
79
|
+
console.print(f"[green]Starting frontend in {mode} mode on {host}:{port}[/green]")
|
|
80
|
+
console.print(f"[cyan]website reachable at http://localhost:{port}[/cyan]")
|
|
81
|
+
console.print(
|
|
82
|
+
f"[cyan]website reachable at https://{port}.localdev.alltuner.com:12000/[/cyan]"
|
|
83
|
+
)
|
|
84
|
+
if is_dev:
|
|
69
85
|
console.print("[dim]Watching for changes in src/ and templates/[/dim]")
|
|
86
|
+
else:
|
|
87
|
+
console.print(f"[dim]Workers: {workers}[/dim]")
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
reload_paths = (
|
|
90
|
+
[
|
|
73
91
|
Path("src/app"),
|
|
74
92
|
Path("templates/frontend"),
|
|
75
93
|
Path("templates/email"),
|
|
76
94
|
Path("templates/markdown"),
|
|
77
95
|
]
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
if is_dev
|
|
97
|
+
else None
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
server = Granian(
|
|
101
|
+
target="vibetuner.frontend.proxy:app",
|
|
102
|
+
address=host,
|
|
103
|
+
port=port,
|
|
104
|
+
interface=Interfaces.ASGI,
|
|
105
|
+
workers=workers,
|
|
106
|
+
reload=is_dev,
|
|
107
|
+
reload_paths=reload_paths,
|
|
108
|
+
log_level="info",
|
|
109
|
+
log_access=True,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
server.serve()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _run_service(
|
|
116
|
+
mode: Literal["dev", "prod"],
|
|
117
|
+
service: str,
|
|
118
|
+
host: str,
|
|
119
|
+
port: int | None,
|
|
120
|
+
workers: int,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Dispatch to the appropriate service runner."""
|
|
123
|
+
if service == "worker":
|
|
124
|
+
_run_worker(mode, port or DEFAULT_WORKER_PORT, workers)
|
|
125
|
+
elif service == "frontend":
|
|
126
|
+
_run_frontend(mode, host, port or DEFAULT_FRONTEND_PORT, workers)
|
|
92
127
|
else:
|
|
93
128
|
console.print(f"[red]Error: Unknown service '{service}'[/red]")
|
|
94
129
|
console.print("[yellow]Valid services: 'frontend' or 'worker'[/yellow]")
|
|
95
130
|
raise typer.Exit(code=1)
|
|
96
131
|
|
|
97
132
|
|
|
133
|
+
@run_app.command(name="dev")
|
|
134
|
+
def dev(
|
|
135
|
+
service: Annotated[
|
|
136
|
+
str, typer.Argument(help="Service to run: 'frontend' or 'worker'")
|
|
137
|
+
] = "frontend",
|
|
138
|
+
port: int | None = typer.Option(
|
|
139
|
+
None, help="Port to run on (8000 for frontend, 11111 for worker)"
|
|
140
|
+
),
|
|
141
|
+
auto_port: bool = typer.Option(
|
|
142
|
+
False,
|
|
143
|
+
"--auto-port",
|
|
144
|
+
help="Use deterministic port based on project path (8001-8999)",
|
|
145
|
+
),
|
|
146
|
+
host: str = typer.Option("0.0.0.0", help="Host to bind to (frontend only)"), # noqa: S104
|
|
147
|
+
workers_count: int = typer.Option(
|
|
148
|
+
1, "--workers", help="Number of worker processes"
|
|
149
|
+
),
|
|
150
|
+
) -> None:
|
|
151
|
+
"""Run in development mode with hot reload (frontend or worker)."""
|
|
152
|
+
if port is not None and auto_port:
|
|
153
|
+
console.print("[red]Error: --port and --auto-port are mutually exclusive[/red]")
|
|
154
|
+
raise typer.Exit(code=1)
|
|
155
|
+
|
|
156
|
+
if auto_port:
|
|
157
|
+
port = _compute_auto_port()
|
|
158
|
+
|
|
159
|
+
_run_service("dev", service, host, port, workers_count)
|
|
160
|
+
|
|
161
|
+
|
|
98
162
|
@run_app.command(name="prod")
|
|
99
163
|
def prod(
|
|
100
164
|
service: Annotated[
|
|
@@ -109,52 +173,4 @@ def prod(
|
|
|
109
173
|
),
|
|
110
174
|
) -> None:
|
|
111
175
|
"""Run in production mode (frontend or worker)."""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if service == "worker":
|
|
115
|
-
# Worker mode
|
|
116
|
-
from streaq.cli import main as streaq_main
|
|
117
|
-
|
|
118
|
-
worker_port = port if port else 11111
|
|
119
|
-
console.print(
|
|
120
|
-
f"[green]Starting worker in prod mode on port {worker_port}[/green]"
|
|
121
|
-
)
|
|
122
|
-
console.print(f"[dim]Workers: {workers_count}[/dim]")
|
|
123
|
-
|
|
124
|
-
# Call streaq programmatically
|
|
125
|
-
streaq_main(
|
|
126
|
-
worker_path="vibetuner.tasks.worker.worker",
|
|
127
|
-
workers=workers_count,
|
|
128
|
-
reload=False,
|
|
129
|
-
verbose=False,
|
|
130
|
-
web=True,
|
|
131
|
-
host="0.0.0.0", # noqa: S104
|
|
132
|
-
port=worker_port,
|
|
133
|
-
)
|
|
134
|
-
elif service == "frontend":
|
|
135
|
-
# Frontend mode
|
|
136
|
-
from granian import Granian
|
|
137
|
-
from granian.constants import Interfaces
|
|
138
|
-
|
|
139
|
-
frontend_port = port if port else 8000
|
|
140
|
-
console.print(
|
|
141
|
-
f"[green]Starting frontend in prod mode on {host}:{frontend_port}[/green]"
|
|
142
|
-
)
|
|
143
|
-
console.print(f"[dim]Workers: {workers_count}[/dim]")
|
|
144
|
-
|
|
145
|
-
server = Granian(
|
|
146
|
-
target="vibetuner.frontend:app",
|
|
147
|
-
address=host,
|
|
148
|
-
port=frontend_port,
|
|
149
|
-
interface=Interfaces.ASGI,
|
|
150
|
-
workers=workers_count,
|
|
151
|
-
reload=False,
|
|
152
|
-
log_level="info",
|
|
153
|
-
log_access=True,
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
server.serve()
|
|
157
|
-
else:
|
|
158
|
-
console.print(f"[red]Error: Unknown service '{service}'[/red]")
|
|
159
|
-
console.print("[yellow]Valid services: 'frontend' or 'worker'[/yellow]")
|
|
160
|
-
raise typer.Exit(code=1)
|
|
176
|
+
_run_service("prod", service, host, port, workers_count)
|
vibetuner/cli/scaffold.py
CHANGED
|
@@ -10,6 +10,7 @@ from rich.console import Console
|
|
|
10
10
|
|
|
11
11
|
console = Console()
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
scaffold_app = typer.Typer(
|
|
14
15
|
help="Create new projects from the vibetuner template", no_args_is_help=True
|
|
15
16
|
)
|
|
@@ -187,26 +188,7 @@ def update(
|
|
|
187
188
|
raise typer.Exit(code=1) from None
|
|
188
189
|
|
|
189
190
|
|
|
190
|
-
@scaffold_app.command(name="
|
|
191
|
-
def
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
typer.Argument(
|
|
195
|
-
help="Path where the 'core' symlink should be created or updated",
|
|
196
|
-
),
|
|
197
|
-
],
|
|
198
|
-
) -> None:
|
|
199
|
-
"""Create or update a 'core' symlink to the package templates directory.
|
|
200
|
-
|
|
201
|
-
This command creates a symlink from the specified target path to the core
|
|
202
|
-
templates directory in the vibetuner package. It is used during development
|
|
203
|
-
to enable Tailwind and other build tools to scan core templates.
|
|
204
|
-
|
|
205
|
-
Examples:
|
|
206
|
-
|
|
207
|
-
# Create symlink in templates/core
|
|
208
|
-
vibetuner scaffold link templates/core
|
|
209
|
-
"""
|
|
210
|
-
from vibetuner.paths import create_core_templates_symlink
|
|
211
|
-
|
|
212
|
-
create_core_templates_symlink(target)
|
|
191
|
+
@scaffold_app.command(name="copy-core-templates", hidden=True)
|
|
192
|
+
def copy_core_templates() -> None:
|
|
193
|
+
"""Deprecated: This command is a no-op kept for backwards compatibility."""
|
|
194
|
+
console.print("[dim]This command is deprecated and does nothing.[/dim]")
|
vibetuner/config.py
CHANGED
|
@@ -7,11 +7,16 @@ from typing import Annotated, Literal
|
|
|
7
7
|
import yaml
|
|
8
8
|
from pydantic import (
|
|
9
9
|
UUID4,
|
|
10
|
+
AnyUrl,
|
|
10
11
|
Field,
|
|
11
12
|
HttpUrl,
|
|
13
|
+
MariaDBDsn,
|
|
12
14
|
MongoDsn,
|
|
15
|
+
MySQLDsn,
|
|
16
|
+
PostgresDsn,
|
|
13
17
|
RedisDsn,
|
|
14
18
|
SecretStr,
|
|
19
|
+
UrlConstraints,
|
|
15
20
|
computed_field,
|
|
16
21
|
)
|
|
17
22
|
from pydantic_extra_types.language_code import LanguageAlpha2
|
|
@@ -23,6 +28,24 @@ from .paths import config_vars as config_vars_path
|
|
|
23
28
|
from .versioning import version
|
|
24
29
|
|
|
25
30
|
|
|
31
|
+
class SQLiteDsn(AnyUrl):
|
|
32
|
+
"""A type that will accept any SQLite DSN.
|
|
33
|
+
|
|
34
|
+
* User info not required
|
|
35
|
+
* TLD not required
|
|
36
|
+
* Host not required (file-based database)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
_constraints = UrlConstraints(
|
|
40
|
+
allowed_schemes=[
|
|
41
|
+
"sqlite",
|
|
42
|
+
"sqlite+aiosqlite",
|
|
43
|
+
"sqlite+pysqlite",
|
|
44
|
+
],
|
|
45
|
+
host_required=False,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
26
49
|
current_year: int = datetime.now().year
|
|
27
50
|
|
|
28
51
|
|
|
@@ -106,8 +129,9 @@ class CoreConfiguration(BaseSettings):
|
|
|
106
129
|
debug_access_token: str | None = None
|
|
107
130
|
|
|
108
131
|
# Database and Cache URLs
|
|
109
|
-
mongodb_url: MongoDsn =
|
|
110
|
-
redis_url: RedisDsn =
|
|
132
|
+
mongodb_url: MongoDsn | None = None
|
|
133
|
+
redis_url: RedisDsn | None = None
|
|
134
|
+
database_url: PostgresDsn | MariaDBDsn | MySQLDsn | SQLiteDsn | None = None
|
|
111
135
|
|
|
112
136
|
aws_access_key_id: SecretStr | None = None
|
|
113
137
|
aws_secret_access_key: SecretStr | None = None
|
|
@@ -118,6 +142,18 @@ class CoreConfiguration(BaseSettings):
|
|
|
118
142
|
r2_secret_key: SecretStr | None = None
|
|
119
143
|
r2_default_region: str = "auto"
|
|
120
144
|
|
|
145
|
+
worker_concurrency: int = 16
|
|
146
|
+
|
|
147
|
+
# Proxy configuration for X-Forwarded-For/Proto headers
|
|
148
|
+
# Comma-separated list of trusted proxy IPs/CIDRs (e.g., "127.0.0.1,192.168.1.0/24")
|
|
149
|
+
# SECURITY: Only IPs in this list can set forwarded headers. Use "*" to trust all (NOT recommended for production)
|
|
150
|
+
trusted_proxy_hosts: str = "127.0.0.1"
|
|
151
|
+
|
|
152
|
+
@cached_property
|
|
153
|
+
def trusted_proxy_hosts_list(self) -> list[str]:
|
|
154
|
+
"""Parse trusted proxy hosts into a list for Granian's proxy header wrapper."""
|
|
155
|
+
return [h.strip() for h in self.trusted_proxy_hosts.split(",") if h.strip()]
|
|
156
|
+
|
|
121
157
|
@computed_field
|
|
122
158
|
@cached_property
|
|
123
159
|
def v_hash(self) -> str:
|
|
@@ -130,6 +166,10 @@ class CoreConfiguration(BaseSettings):
|
|
|
130
166
|
|
|
131
167
|
return url_safe_hash
|
|
132
168
|
|
|
169
|
+
@property
|
|
170
|
+
def workers_available(self) -> bool:
|
|
171
|
+
return self.redis_url is not None
|
|
172
|
+
|
|
133
173
|
@cached_property
|
|
134
174
|
def mongo_dbname(self) -> str:
|
|
135
175
|
return self.project.project_slug
|
vibetuner/frontend/lifespan.py
CHANGED
|
@@ -5,7 +5,8 @@ from fastapi import FastAPI
|
|
|
5
5
|
|
|
6
6
|
from vibetuner.context import ctx
|
|
7
7
|
from vibetuner.logging import logger
|
|
8
|
-
from vibetuner.mongo import
|
|
8
|
+
from vibetuner.mongo import init_mongodb, teardown_mongodb
|
|
9
|
+
from vibetuner.sqlmodel import init_sqlmodel, teardown_sqlmodel
|
|
9
10
|
|
|
10
11
|
from .hotreload import hotreload
|
|
11
12
|
|
|
@@ -16,7 +17,8 @@ async def base_lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
|
16
17
|
if ctx.DEBUG:
|
|
17
18
|
await hotreload.startup()
|
|
18
19
|
|
|
19
|
-
await
|
|
20
|
+
await init_mongodb()
|
|
21
|
+
await init_sqlmodel()
|
|
20
22
|
|
|
21
23
|
yield
|
|
22
24
|
|
|
@@ -25,6 +27,9 @@ async def base_lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
|
25
27
|
await hotreload.shutdown()
|
|
26
28
|
logger.info("Vibetuner frontend stopped")
|
|
27
29
|
|
|
30
|
+
await teardown_sqlmodel()
|
|
31
|
+
await teardown_mongodb()
|
|
32
|
+
|
|
28
33
|
|
|
29
34
|
try:
|
|
30
35
|
from app.frontend.lifespan import lifespan # ty: ignore
|
vibetuner/frontend/middleware.py
CHANGED
|
@@ -6,7 +6,6 @@ from starlette.middleware.authentication import AuthenticationMiddleware
|
|
|
6
6
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
7
7
|
from starlette.middleware.sessions import SessionMiddleware
|
|
8
8
|
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
|
9
|
-
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
10
9
|
from starlette_babel import (
|
|
11
10
|
LocaleFromCookie,
|
|
12
11
|
LocaleFromQuery,
|
|
@@ -84,29 +83,6 @@ class AdjustLangCookieMiddleware(BaseHTTPMiddleware):
|
|
|
84
83
|
return response
|
|
85
84
|
|
|
86
85
|
|
|
87
|
-
class ForwardedProtocolMiddleware:
|
|
88
|
-
def __init__(self, app: ASGIApp):
|
|
89
|
-
self.app = app
|
|
90
|
-
|
|
91
|
-
# Based on https://github.com/encode/uvicorn/blob/master/uvicorn/middleware/proxy_headers.py
|
|
92
|
-
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
93
|
-
if scope["type"] == "lifespan":
|
|
94
|
-
return await self.app(scope, receive, send)
|
|
95
|
-
|
|
96
|
-
headers = dict(scope["headers"])
|
|
97
|
-
|
|
98
|
-
if b"x-forwarded-proto" in headers:
|
|
99
|
-
x_forwarded_proto = headers[b"x-forwarded-proto"].decode("latin1").strip()
|
|
100
|
-
|
|
101
|
-
if x_forwarded_proto in {"http", "https", "ws", "wss"}:
|
|
102
|
-
if scope["type"] == "websocket":
|
|
103
|
-
scope["scheme"] = x_forwarded_proto.replace("http", "ws")
|
|
104
|
-
else:
|
|
105
|
-
scope["scheme"] = x_forwarded_proto
|
|
106
|
-
|
|
107
|
-
return await self.app(scope, receive, send)
|
|
108
|
-
|
|
109
|
-
|
|
110
86
|
class AuthBackend(AuthenticationBackend):
|
|
111
87
|
async def authenticate(
|
|
112
88
|
self,
|
|
@@ -128,7 +104,6 @@ class AuthBackend(AuthenticationBackend):
|
|
|
128
104
|
|
|
129
105
|
middlewares: list[Middleware] = [
|
|
130
106
|
Middleware(TrustedHostMiddleware),
|
|
131
|
-
Middleware(ForwardedProtocolMiddleware),
|
|
132
107
|
Middleware(HtmxMiddleware),
|
|
133
108
|
Middleware(SessionMiddleware, secret_key=settings.session_key.get_secret_value()),
|
|
134
109
|
Middleware(
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# ABOUTME: ASGI app wrapper that processes X-Forwarded-For and X-Forwarded-Proto headers
|
|
2
|
+
# ABOUTME: Uses Granian's proxy header support to extract real client IP and protocol
|
|
3
|
+
from granian.utils.proxies import wrap_asgi_with_proxy_headers
|
|
4
|
+
|
|
5
|
+
from vibetuner.config import settings
|
|
6
|
+
from vibetuner.frontend import app as _app
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Wrap the FastAPI app with Granian's proxy header processor
|
|
10
|
+
# This enables real client IP detection when behind reverse proxies (nginx, Cloudflare, etc.)
|
|
11
|
+
# Only headers from trusted_proxy_hosts will be processed to prevent IP spoofing
|
|
12
|
+
app = wrap_asgi_with_proxy_headers(
|
|
13
|
+
_app, trusted_hosts=settings.trusted_proxy_hosts_list
|
|
14
|
+
)
|
vibetuner/frontend/templates.py
CHANGED
|
@@ -17,6 +17,8 @@ from .hotreload import hotreload
|
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"render_static_template",
|
|
20
|
+
"render_template",
|
|
21
|
+
"render_template_string",
|
|
20
22
|
"register_filter",
|
|
21
23
|
]
|
|
22
24
|
|
|
@@ -187,6 +189,38 @@ def render_template(
|
|
|
187
189
|
return templates.TemplateResponse(template, merged_ctx, **kwargs)
|
|
188
190
|
|
|
189
191
|
|
|
192
|
+
def render_template_string(
|
|
193
|
+
template: str,
|
|
194
|
+
request: Request,
|
|
195
|
+
ctx: dict[str, Any] | None = None,
|
|
196
|
+
) -> str:
|
|
197
|
+
"""Render a template to a string instead of HTMLResponse.
|
|
198
|
+
|
|
199
|
+
Useful for Server-Sent Events (SSE), AJAX responses, or any case where you need
|
|
200
|
+
the rendered HTML as a string rather than a full HTTP response.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
template: Path to template file (e.g., "admin/partials/episode.html.jinja")
|
|
204
|
+
request: FastAPI Request object
|
|
205
|
+
ctx: Optional context dictionary to pass to template
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
str: Rendered template as a string
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
html = render_template_string(
|
|
212
|
+
"admin/partials/episode_article.html.jinja",
|
|
213
|
+
request,
|
|
214
|
+
{"episode": episode}
|
|
215
|
+
)
|
|
216
|
+
"""
|
|
217
|
+
ctx = ctx or {}
|
|
218
|
+
merged_ctx = {**data_ctx.model_dump(), "request": request, **ctx}
|
|
219
|
+
|
|
220
|
+
template_obj = templates.get_template(template)
|
|
221
|
+
return template_obj.render(merged_ctx)
|
|
222
|
+
|
|
223
|
+
|
|
190
224
|
# Global Vars
|
|
191
225
|
jinja_env.globals.update({"DEBUG": data_ctx.DEBUG})
|
|
192
226
|
jinja_env.globals.update({"hotreload": hotreload})
|
vibetuner/mongo.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from importlib import import_module
|
|
2
|
+
from typing import Optional
|
|
2
3
|
|
|
3
4
|
from beanie import init_beanie
|
|
5
|
+
from deprecated import deprecated
|
|
4
6
|
from pymongo import AsyncMongoClient
|
|
5
7
|
|
|
6
8
|
from vibetuner.config import settings
|
|
@@ -8,26 +10,64 @@ from vibetuner.logging import logger
|
|
|
8
10
|
from vibetuner.models.registry import get_all_models
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
# Global singleton, created lazily
|
|
14
|
+
mongo_client: Optional[AsyncMongoClient] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _ensure_client() -> None:
|
|
18
|
+
"""
|
|
19
|
+
Lazily create the global MongoDB client if mongodb_url is configured.
|
|
20
|
+
Safe to call many times.
|
|
21
|
+
"""
|
|
22
|
+
global mongo_client
|
|
23
|
+
|
|
24
|
+
if settings.mongodb_url is None:
|
|
25
|
+
logger.warning("MongoDB URL is not configured. Mongo engine disabled.")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if mongo_client is None:
|
|
29
|
+
mongo_client = AsyncMongoClient(
|
|
30
|
+
host=str(settings.mongodb_url),
|
|
31
|
+
compressors=["zstd"],
|
|
32
|
+
)
|
|
33
|
+
logger.debug("MongoDB client created.")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def init_mongodb() -> None:
|
|
37
|
+
"""Initialize MongoDB and register Beanie models."""
|
|
38
|
+
_ensure_client()
|
|
39
|
+
|
|
40
|
+
if mongo_client is None:
|
|
41
|
+
# Nothing to do; URL missing
|
|
42
|
+
return
|
|
13
43
|
|
|
14
|
-
#
|
|
44
|
+
# Import user models so they register themselves
|
|
15
45
|
try:
|
|
16
46
|
import_module("app.models")
|
|
17
47
|
except ModuleNotFoundError:
|
|
18
|
-
|
|
19
|
-
pass
|
|
48
|
+
logger.debug("app.models not found; skipping user model import.")
|
|
20
49
|
except ImportError as e:
|
|
21
|
-
|
|
22
|
-
logger.warning(
|
|
23
|
-
f"Failed to import app.models: {e}. User models will not be registered."
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
client: AsyncMongoClient = AsyncMongoClient(
|
|
27
|
-
host=str(settings.mongodb_url),
|
|
28
|
-
compressors=["zstd"],
|
|
29
|
-
)
|
|
50
|
+
logger.warning(f"Failed to import app.models: {e}. User models may be missing.")
|
|
30
51
|
|
|
31
52
|
await init_beanie(
|
|
32
|
-
database=
|
|
53
|
+
database=mongo_client[settings.mongo_dbname],
|
|
54
|
+
document_models=get_all_models(),
|
|
33
55
|
)
|
|
56
|
+
logger.info("MongoDB + Beanie initialized successfully.")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def teardown_mongodb() -> None:
|
|
60
|
+
"""Dispose the MongoDB client."""
|
|
61
|
+
global mongo_client
|
|
62
|
+
|
|
63
|
+
if mongo_client is not None:
|
|
64
|
+
await mongo_client.close()
|
|
65
|
+
mongo_client = None
|
|
66
|
+
logger.info("MongoDB client closed.")
|
|
67
|
+
else:
|
|
68
|
+
logger.debug("MongoDB client was never initialized; nothing to tear down.")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@deprecated(reason="Use init_mongodb() instead")
|
|
72
|
+
async def init_models() -> None:
|
|
73
|
+
await init_mongodb()
|
vibetuner/sqlmodel.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# ABOUTME: SQLModel/SQLAlchemy async engine setup and session management.
|
|
2
|
+
# ABOUTME: Provides database initialization, teardown, and FastAPI dependency injection.
|
|
3
|
+
|
|
4
|
+
from typing import AsyncGenerator, Optional
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.ext.asyncio import (
|
|
7
|
+
AsyncEngine,
|
|
8
|
+
AsyncSession,
|
|
9
|
+
async_sessionmaker,
|
|
10
|
+
create_async_engine,
|
|
11
|
+
)
|
|
12
|
+
from sqlmodel import SQLModel
|
|
13
|
+
|
|
14
|
+
from vibetuner.config import settings
|
|
15
|
+
from vibetuner.logging import logger
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# These will be filled lazily if/when database_url is set
|
|
19
|
+
engine: Optional[AsyncEngine] = None
|
|
20
|
+
async_session: Optional[async_sessionmaker[AsyncSession]] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ensure_engine() -> None:
|
|
24
|
+
"""
|
|
25
|
+
Lazily configure the engine + sessionmaker if database_url is set.
|
|
26
|
+
|
|
27
|
+
Safe to call multiple times.
|
|
28
|
+
"""
|
|
29
|
+
global engine, async_session
|
|
30
|
+
|
|
31
|
+
if settings.database_url is None:
|
|
32
|
+
logger.warning("database_url is not configured. SQLModel engine is disabled.")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if engine is None:
|
|
36
|
+
engine = create_async_engine(
|
|
37
|
+
str(settings.database_url),
|
|
38
|
+
echo=settings.debug,
|
|
39
|
+
)
|
|
40
|
+
async_session = async_sessionmaker(
|
|
41
|
+
engine,
|
|
42
|
+
class_=AsyncSession,
|
|
43
|
+
expire_on_commit=False,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def init_sqlmodel() -> None:
|
|
48
|
+
"""
|
|
49
|
+
Called from lifespan/startup.
|
|
50
|
+
Initializes the database engine if DB is configured.
|
|
51
|
+
|
|
52
|
+
Note: This does NOT create tables. Use `vibetuner db create-schema` CLI command
|
|
53
|
+
for schema creation, or call `create_schema()` directly.
|
|
54
|
+
"""
|
|
55
|
+
_ensure_engine()
|
|
56
|
+
|
|
57
|
+
if engine is None:
|
|
58
|
+
# Nothing to do, DB not configured
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
logger.info("SQLModel engine initialized successfully.")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def create_schema() -> None:
|
|
65
|
+
"""
|
|
66
|
+
Create all tables defined in SQLModel metadata.
|
|
67
|
+
|
|
68
|
+
Call this from the CLI command `vibetuner db create-schema` or manually
|
|
69
|
+
during initial setup. This is idempotent - existing tables are not modified.
|
|
70
|
+
"""
|
|
71
|
+
_ensure_engine()
|
|
72
|
+
|
|
73
|
+
if engine is None:
|
|
74
|
+
raise RuntimeError("database_url is not configured. Cannot create schema.")
|
|
75
|
+
|
|
76
|
+
async with engine.begin() as conn:
|
|
77
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
78
|
+
logger.info("SQLModel schema created successfully.")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def teardown_sqlmodel() -> None:
|
|
82
|
+
"""
|
|
83
|
+
Called from lifespan/shutdown.
|
|
84
|
+
"""
|
|
85
|
+
global engine
|
|
86
|
+
|
|
87
|
+
if engine is None:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
await engine.dispose()
|
|
91
|
+
engine = None
|
|
92
|
+
logger.info("SQLModel engine disposed.")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|
96
|
+
"""
|
|
97
|
+
FastAPI dependency.
|
|
98
|
+
|
|
99
|
+
If the DB is not configured, you can:
|
|
100
|
+
- raise a RuntimeError (fail fast), OR
|
|
101
|
+
- raise HTTPException(500), OR
|
|
102
|
+
- return a dummy object in tests.
|
|
103
|
+
"""
|
|
104
|
+
if async_session is None:
|
|
105
|
+
# Fail fast – you can customize this to HTTPException if used only in web context
|
|
106
|
+
raise RuntimeError("database_url is not configured. No DB session available.")
|
|
107
|
+
|
|
108
|
+
async with async_session() as session:
|
|
109
|
+
yield session
|
vibetuner/tasks/lifespan.py
CHANGED
|
@@ -3,17 +3,22 @@ from typing import AsyncGenerator
|
|
|
3
3
|
|
|
4
4
|
from vibetuner.context import Context, ctx
|
|
5
5
|
from vibetuner.logging import logger
|
|
6
|
-
from vibetuner.mongo import
|
|
6
|
+
from vibetuner.mongo import init_mongodb, teardown_mongodb
|
|
7
|
+
from vibetuner.sqlmodel import init_sqlmodel, teardown_sqlmodel
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
@asynccontextmanager
|
|
10
11
|
async def base_lifespan() -> AsyncGenerator[Context, None]:
|
|
11
12
|
logger.info("Vibetuner task worker starting")
|
|
12
13
|
|
|
13
|
-
await
|
|
14
|
+
await init_mongodb()
|
|
15
|
+
await init_sqlmodel()
|
|
14
16
|
|
|
15
17
|
yield ctx
|
|
16
18
|
|
|
19
|
+
await teardown_sqlmodel()
|
|
20
|
+
await teardown_mongodb()
|
|
21
|
+
|
|
17
22
|
logger.info("Vibetuner task worker stopping")
|
|
18
23
|
|
|
19
24
|
|
vibetuner/tasks/worker.py
CHANGED
|
@@ -4,8 +4,13 @@ from vibetuner.config import settings
|
|
|
4
4
|
from vibetuner.tasks.lifespan import lifespan
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
worker =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
worker: Worker | None = (
|
|
8
|
+
Worker(
|
|
9
|
+
redis_url=str(settings.redis_url),
|
|
10
|
+
queue_name=settings.redis_key_prefix.rstrip(":"),
|
|
11
|
+
lifespan=lifespan,
|
|
12
|
+
concurrency=settings.worker_concurrency,
|
|
13
|
+
)
|
|
14
|
+
if settings.workers_available
|
|
15
|
+
else None
|
|
11
16
|
)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{{ _("Check Your Email") }}
|
|
5
5
|
{% endblock title %}
|
|
6
6
|
{% block body %}
|
|
7
|
-
<div class="min-h-screen bg-
|
|
7
|
+
<div class="min-h-screen bg-linear-to-br from-primary to-secondary flex items-center justify-center p-4">
|
|
8
8
|
<div class="card w-full max-w-md bg-base-100 shadow-2xl backdrop-blur-sm bg-opacity-95">
|
|
9
9
|
<div class="card-body text-center">
|
|
10
10
|
<!-- Success Icon -->
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{% set BODY_CLASS = "min-h-screen bg-
|
|
1
|
+
{% set BODY_CLASS = "min-h-screen bg-linear-to-br from-slate-50 via-blue-50 to-indigo-100 flex flex-col justify-between" %}
|
|
2
2
|
{% extends "base/skeleton.html.jinja" %}
|
|
3
3
|
|
|
4
4
|
{% block body %}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</p>
|
|
14
14
|
<!-- Decorative element -->
|
|
15
15
|
<div class="mt-12 flex justify-center">
|
|
16
|
-
<div class="w-24 h-1 bg-
|
|
16
|
+
<div class="w-24 h-1 bg-linear-to-r from-blue-400 to-indigo-500 rounded-full"></div>
|
|
17
17
|
</div>
|
|
18
18
|
</div>
|
|
19
19
|
</div>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{{ _("Sign In") }}
|
|
5
5
|
{% endblock title %}
|
|
6
6
|
{% block body %}
|
|
7
|
-
<div class="min-h-screen bg-
|
|
7
|
+
<div class="min-h-screen bg-linear-to-br from-primary to-secondary flex items-center justify-center p-4">
|
|
8
8
|
<div class="card w-full max-w-md bg-base-100 shadow-2xl backdrop-blur-sm bg-opacity-95">
|
|
9
9
|
<div class="card-body">
|
|
10
10
|
<!-- Header -->
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
<!-- Additional Info Card -->
|
|
65
65
|
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
66
66
|
<div class="flex items-start">
|
|
67
|
-
<div class="
|
|
67
|
+
<div class="shrink-0">
|
|
68
68
|
<svg class="h-5 w-5 text-blue-600"
|
|
69
69
|
xmlns="http://www.w3.org/2000/svg"
|
|
70
70
|
viewBox="0 0 20 20"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<!-- Profile Card -->
|
|
16
16
|
<div class="bg-white shadow-lg rounded-lg overflow-hidden">
|
|
17
17
|
<!-- Profile Header -->
|
|
18
|
-
<div class="bg-
|
|
18
|
+
<div class="bg-linear-to-r from-blue-500 to-indigo-600 px-6 py-8">
|
|
19
19
|
<div class="flex items-center space-x-4">
|
|
20
20
|
<!-- Avatar -->
|
|
21
21
|
{% if user.picture or (user.oauth_accounts and user.oauth_accounts[0].picture) %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: vibetuner
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.44.1
|
|
4
4
|
Summary: Core Python framework and blessed dependencies for production-ready FastAPI + MongoDB + HTMX projects
|
|
5
5
|
Keywords: fastapi,mongodb,htmx,web-framework,scaffolding,oauth,background-jobs
|
|
6
6
|
Author: All Tuner Labs, S.L.
|
|
@@ -18,47 +18,49 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
|
18
18
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
19
19
|
Requires-Dist: aioboto3>=15.5.0
|
|
20
20
|
Requires-Dist: arel>=0.4.0
|
|
21
|
-
Requires-Dist: asyncer>=0.0.
|
|
22
|
-
Requires-Dist: authlib>=1.6.
|
|
21
|
+
Requires-Dist: asyncer>=0.0.12
|
|
22
|
+
Requires-Dist: authlib>=1.6.6
|
|
23
23
|
Requires-Dist: beanie[zstd]>=2.0.1
|
|
24
24
|
Requires-Dist: click>=8.3.1
|
|
25
25
|
Requires-Dist: copier>=9.11.0,<9.11.1
|
|
26
26
|
Requires-Dist: email-validator>=2.3.0
|
|
27
|
-
Requires-Dist: fastapi[standard-no-fastapi-cloud-cli]>=0.
|
|
28
|
-
Requires-Dist: granian[pname]>=2.6.
|
|
27
|
+
Requires-Dist: fastapi[standard-no-fastapi-cloud-cli]>=0.128.0
|
|
28
|
+
Requires-Dist: granian[pname]>=2.6.1
|
|
29
29
|
Requires-Dist: httpx[http2]>=0.28.1
|
|
30
30
|
Requires-Dist: itsdangerous>=2.2.0
|
|
31
31
|
Requires-Dist: loguru>=0.7.3
|
|
32
|
-
Requires-Dist: pydantic[email]>=2.12.
|
|
33
|
-
Requires-Dist: pydantic-extra-types[pycountry]>=2.
|
|
32
|
+
Requires-Dist: pydantic[email]>=2.12.5
|
|
33
|
+
Requires-Dist: pydantic-extra-types[pycountry]>=2.11.0
|
|
34
34
|
Requires-Dist: pydantic-settings>=2.12.0
|
|
35
35
|
Requires-Dist: pyyaml>=6.0.3
|
|
36
36
|
Requires-Dist: redis[hiredis]>=7.1.0
|
|
37
37
|
Requires-Dist: rich>=14.2.0
|
|
38
|
-
Requires-Dist: sse-starlette>=3.
|
|
38
|
+
Requires-Dist: sse-starlette>=3.1.2
|
|
39
39
|
Requires-Dist: starlette-babel>=1.0.3
|
|
40
40
|
Requires-Dist: starlette-htmx>=0.1.1
|
|
41
41
|
Requires-Dist: streaq[web]<6.0.0
|
|
42
|
-
Requires-Dist: typer-slim[standard]>=0.
|
|
42
|
+
Requires-Dist: typer-slim[standard]>=0.21.1
|
|
43
|
+
Requires-Dist: deprecated>=1.3.1
|
|
44
|
+
Requires-Dist: sqlmodel>=0.0.31
|
|
43
45
|
Requires-Dist: babel>=2.17.0 ; extra == 'dev'
|
|
44
46
|
Requires-Dist: cloudflare>=4.3.1 ; extra == 'dev'
|
|
45
47
|
Requires-Dist: djlint>=1.36.4 ; extra == 'dev'
|
|
46
48
|
Requires-Dist: dunamai>=1.25.0 ; extra == 'dev'
|
|
47
|
-
Requires-Dist: gh-bin>=2.83.
|
|
48
|
-
Requires-Dist: granian[pname,reload]>=2.6.
|
|
49
|
-
Requires-Dist: just-bin>=1.
|
|
50
|
-
Requires-Dist: prek
|
|
49
|
+
Requires-Dist: gh-bin>=2.83.2 ; extra == 'dev'
|
|
50
|
+
Requires-Dist: granian[pname,reload]>=2.6.1 ; extra == 'dev'
|
|
51
|
+
Requires-Dist: just-bin>=1.46.0 ; extra == 'dev'
|
|
52
|
+
Requires-Dist: prek==0.2.25 ; extra == 'dev'
|
|
51
53
|
Requires-Dist: pysemver>=0.5.0 ; extra == 'dev'
|
|
52
|
-
Requires-Dist: ruff>=0.14.
|
|
53
|
-
Requires-Dist: rumdl>=0.0.
|
|
54
|
+
Requires-Dist: ruff>=0.14.10 ; extra == 'dev'
|
|
55
|
+
Requires-Dist: rumdl>=0.0.211 ; extra == 'dev'
|
|
54
56
|
Requires-Dist: semver>=3.0.4 ; extra == 'dev'
|
|
55
57
|
Requires-Dist: taplo>=0.9.3 ; extra == 'dev'
|
|
56
|
-
Requires-Dist: ty>=0.0.
|
|
58
|
+
Requires-Dist: ty>=0.0.9 ; extra == 'dev'
|
|
57
59
|
Requires-Dist: types-aioboto3[s3,ses]>=15.5.0 ; extra == 'dev'
|
|
58
|
-
Requires-Dist: types-authlib>=1.6.
|
|
60
|
+
Requires-Dist: types-authlib>=1.6.6.20251220 ; extra == 'dev'
|
|
59
61
|
Requires-Dist: types-pyyaml>=6.0.12.20250915 ; extra == 'dev'
|
|
60
|
-
Requires-Dist: uv-bump>=0.
|
|
61
|
-
Requires-Dist: pytest>=9.0.
|
|
62
|
+
Requires-Dist: uv-bump>=0.4.0 ; extra == 'dev'
|
|
63
|
+
Requires-Dist: pytest>=9.0.2 ; extra == 'test'
|
|
62
64
|
Requires-Dist: pytest-asyncio>=1.3.0 ; extra == 'test'
|
|
63
65
|
Requires-Python: >=3.11
|
|
64
66
|
Project-URL: Changelog, https://github.com/alltuner/vibetuner/blob/main/CHANGELOG.md
|
|
@@ -80,13 +82,13 @@ authentication, background jobs, and CLI tools.
|
|
|
80
82
|
|
|
81
83
|
## What is Vibetuner?
|
|
82
84
|
|
|
83
|
-
Vibetuner is a production-ready scaffolding tool for FastAPI
|
|
85
|
+
Vibetuner is a production-ready scaffolding tool for FastAPI web applications.
|
|
84
86
|
This package (`vibetuner`) is the Python component that provides:
|
|
85
87
|
|
|
86
88
|
- Complete web application framework built on FastAPI
|
|
87
|
-
-
|
|
89
|
+
- **Flexible database support**: MongoDB (Beanie ODM) or SQL (SQLModel/SQLAlchemy)
|
|
88
90
|
- OAuth and magic link authentication out of the box
|
|
89
|
-
- Background job processing with Redis + Streaq
|
|
91
|
+
- Background job processing with Redis + Streaq (optional)
|
|
90
92
|
- CLI framework with Typer
|
|
91
93
|
- Email services, blob storage, and more
|
|
92
94
|
|
|
@@ -134,18 +136,20 @@ This will generate a complete project with:
|
|
|
134
136
|
- **`models/`**: User, OAuth, email verification, blob storage models
|
|
135
137
|
- **`services/`**: Email (SES), blob storage (S3)
|
|
136
138
|
- **`tasks/`**: Background job infrastructure
|
|
137
|
-
- **`cli/`**: CLI framework with scaffold, run commands
|
|
139
|
+
- **`cli/`**: CLI framework with scaffold, run, db commands
|
|
138
140
|
- **`config.py`**: Pydantic settings management
|
|
139
|
-
- **`mongo.py`**: MongoDB/Beanie setup
|
|
141
|
+
- **`mongo.py`**: MongoDB/Beanie setup (optional)
|
|
142
|
+
- **`sqlmodel.py`**: SQLModel/SQLAlchemy setup (optional)
|
|
140
143
|
- **`logging.py`**: Structured logging configuration
|
|
141
144
|
|
|
142
145
|
### Blessed Dependencies
|
|
143
146
|
|
|
144
147
|
- **FastAPI** (0.121+): Modern, fast web framework
|
|
145
|
-
- **Beanie**: Async MongoDB ODM with Pydantic
|
|
148
|
+
- **Beanie**: Async MongoDB ODM with Pydantic (optional)
|
|
149
|
+
- **SQLModel** + **SQLAlchemy**: SQL databases - PostgreSQL, MySQL, SQLite (optional)
|
|
146
150
|
- **Authlib**: OAuth 1.0/2.0 client
|
|
147
151
|
- **Granian**: High-performance ASGI server
|
|
148
|
-
- **Redis** + **Streaq**: Background task processing
|
|
152
|
+
- **Redis** + **Streaq**: Background task processing (optional)
|
|
149
153
|
- **Typer**: CLI framework
|
|
150
154
|
- **Rich**: Beautiful terminal output
|
|
151
155
|
- **Loguru**: Structured logging
|
|
@@ -165,6 +169,9 @@ vibetuner scaffold new my-project --defaults
|
|
|
165
169
|
# Update existing project
|
|
166
170
|
vibetuner scaffold update
|
|
167
171
|
|
|
172
|
+
# Database management (SQLModel)
|
|
173
|
+
vibetuner db create-schema # Create SQL database tables
|
|
174
|
+
|
|
168
175
|
# Run development server (in generated projects)
|
|
169
176
|
vibetuner run dev frontend
|
|
170
177
|
vibetuner run dev worker
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
vibetuner/__init__.py,sha256=rFIVCmxkKTT_g477V8biCw0lgpudyuUabXhYxg189lY,90
|
|
2
2
|
vibetuner/__main__.py,sha256=Ye9oBAgXhcYQ4I4yZli3TIXF5lWQ9yY4tTPs4XnDDUY,29
|
|
3
|
-
vibetuner/cli/__init__.py,sha256=
|
|
4
|
-
vibetuner/cli/
|
|
5
|
-
vibetuner/cli/
|
|
6
|
-
vibetuner/
|
|
3
|
+
vibetuner/cli/__init__.py,sha256=Lt3UAQaT8yA0uQYecT915DS_-FiyHg6xdp4deHuP9Gk,3612
|
|
4
|
+
vibetuner/cli/db.py,sha256=fH4gJjMZv6na8uIPgE5Fiv-cGWedegYTLUB2lTxbTR0,1217
|
|
5
|
+
vibetuner/cli/run.py,sha256=1zN0W-cBNlYq3uP3Say1TG6ex9FkkuSBkI35wGqMTNA,5323
|
|
6
|
+
vibetuner/cli/scaffold.py,sha256=o_2BrJ86ZD1lZIC_fcOvHomLTezzfmneabHIv7vM1YE,5968
|
|
7
|
+
vibetuner/config.py,sha256=wtyHvkN2GLHOHDDqnnJPnFTgj3QZ_kBGkUG7PKcUHWg,5747
|
|
7
8
|
vibetuner/context.py,sha256=h4f4FfkmLlOD6WiSLhx7-IjFvIA4zcrsAp6478l6npg,743
|
|
8
9
|
vibetuner/frontend/__init__.py,sha256=COZuPRzTFUfxeyWkvmz0GxBBFFvBHtf2mMGmLw5fy2c,3965
|
|
9
10
|
vibetuner/frontend/deps.py,sha256=b3ocC_ryaK2Jp51SfcFqckrXiaL7V-chkFRqLjzgA_c,1296
|
|
10
11
|
vibetuner/frontend/email.py,sha256=k0d7FCZCge5VYOKp3fLsbx7EA5_SrtBkpMs57o4W7u0,1119
|
|
11
12
|
vibetuner/frontend/hotreload.py,sha256=Gl7FIKJaiCVVoyWQqdErBUOKDP1cGBFUpGzqHMiJd10,285
|
|
12
|
-
vibetuner/frontend/lifespan.py,sha256=
|
|
13
|
-
vibetuner/frontend/middleware.py,sha256=
|
|
13
|
+
vibetuner/frontend/lifespan.py,sha256=Gl9tuqRymxs8_nyimlw_bIdM0ofe_XCJA99wI_YYxvw,1244
|
|
14
|
+
vibetuner/frontend/middleware.py,sha256=lmPAgpkPFFFgEnFs4n0N1aQr4V73_JPMBe3NqO8AWQA,3912
|
|
14
15
|
vibetuner/frontend/oauth.py,sha256=EzEwoOZ_8xn_CiqAWpNoEdhV2NPxZKKwF2bA6W6Bkj0,5884
|
|
16
|
+
vibetuner/frontend/proxy.py,sha256=rfoQLz-T59eY_xCEHxo9fzYDaS-cTZrcWbiV_Tm3gZU,654
|
|
15
17
|
vibetuner/frontend/routes/__init__.py,sha256=nHhiylHIUPZ2R-Bd7vXEGHLJBQ7fNuzPTJodjJR3lyc,428
|
|
16
18
|
vibetuner/frontend/routes/auth.py,sha256=vKE-Dm2yPXReaOLvcxfT4a6df1dKUoteZ4p46v8Elm4,4331
|
|
17
19
|
vibetuner/frontend/routes/debug.py,sha256=hqiuRjcLOvL2DdhA83HUzp5svECKjyFB-LoNn7xDiFE,13269
|
|
@@ -19,7 +21,7 @@ vibetuner/frontend/routes/health.py,sha256=_XkMpdMNUemu7qzkGkqn5TBnZmGrArA3Xps5C
|
|
|
19
21
|
vibetuner/frontend/routes/language.py,sha256=wHNfdewqWfK-2JLXwglu0Q0b_e00HFGd0A2-PYT44LE,1240
|
|
20
22
|
vibetuner/frontend/routes/meta.py,sha256=pSyIxQsiB0QZSYwCQbS07KhkT5oHC5r9jvjUDIqZRGw,1409
|
|
21
23
|
vibetuner/frontend/routes/user.py,sha256=b8ow6IGnfsHosSwSmEIYZtuQJnW_tacnNjp_aMnqWxU,2666
|
|
22
|
-
vibetuner/frontend/templates.py,sha256=
|
|
24
|
+
vibetuner/frontend/templates.py,sha256=jqMMl1-ECYDW8lyGg3r4dPbd9kZQXy90pUvJxsAIwOg,7616
|
|
23
25
|
vibetuner/logging.py,sha256=9eNofqVtKZCBDS33NbBI7Sv2875gM8MNStTSCjX2AXQ,2409
|
|
24
26
|
vibetuner/models/__init__.py,sha256=JvmQvzDIxaI7zlk-ROCWEbuzxXSUOqCshINUjgu-AfQ,325
|
|
25
27
|
vibetuner/models/blob.py,sha256=F30HFS4Z_Bji_PGPflWIv4dOwqKLsEWQHcjW1Oz_79M,2523
|
|
@@ -29,15 +31,16 @@ vibetuner/models/oauth.py,sha256=BdOZbW47Das9ntHTtRmVdl1lB5zLCcXW2fyVJ-tQ_F8,150
|
|
|
29
31
|
vibetuner/models/registry.py,sha256=O5YG7vOrWluqpH5N7m44v72wbscMhU_Pu3TJw_u0MTk,311
|
|
30
32
|
vibetuner/models/types.py,sha256=Lj3ASEvx5eNgQMcVhNyKQHHolJqDxj2yH8S-M9oa4J8,402
|
|
31
33
|
vibetuner/models/user.py,sha256=ttcSH4mVREPhA6bCFUWXKfJ9_8_Iq3lEYXe3rDrslw4,2696
|
|
32
|
-
vibetuner/mongo.py,sha256=
|
|
34
|
+
vibetuner/mongo.py,sha256=f3tVYcDRsIrSCqB340MDA9qt9iesFPAUg0ZUlCGv0l0,2051
|
|
33
35
|
vibetuner/paths.py,sha256=hWWcjwVPNxac74rRuHgHD46CwmQKHJidEALo-euw72k,8360
|
|
34
36
|
vibetuner/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
37
|
vibetuner/services/blob.py,sha256=mYsMDA04E1fgXLaprWDip28RrxEX60T1G7UT1aMZDBY,5029
|
|
36
38
|
vibetuner/services/email.py,sha256=Y0yKgqki2an7fE6n4hQeROjVglzcqIGBgXICJbKZq0s,1669
|
|
37
39
|
vibetuner/services/s3_storage.py,sha256=purEKXKqb_5iroyDgEWCJ2DRMmG08Jy5tnsc2ZE1An4,15407
|
|
40
|
+
vibetuner/sqlmodel.py,sha256=d0fK9ZlzUGW02rTu5n_k3Buuuu3nI5e5wTmaydnl5CM,3000
|
|
38
41
|
vibetuner/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
-
vibetuner/tasks/lifespan.py,sha256=
|
|
40
|
-
vibetuner/tasks/worker.py,sha256=
|
|
42
|
+
vibetuner/tasks/lifespan.py,sha256=IroLZooVmdtQ6MUad2Dwi_40DGSga5__0SCVbtZAP6g,1031
|
|
43
|
+
vibetuner/tasks/worker.py,sha256=brW92T1kdaoLLNulu9K1Hs82ewCvNcKEbYd_8K0Mdcg,384
|
|
41
44
|
vibetuner/templates/email/magic_link.html.jinja,sha256=DzaCnBsYoau2JQh5enPAa2FMFFTyCwdyiM3vGhBQdtA,553
|
|
42
45
|
vibetuner/templates/email/magic_link.txt.jinja,sha256=dANak9ion1cpILt45V3GcI2qnL_gKFPj7PsZKYV0m5s,200
|
|
43
46
|
vibetuner/templates/frontend/base/favicons.html.jinja,sha256=A7s7YXuE82tRd7ZLJs1jGEGwBRiMPrqlWd507xL1iZg,70
|
|
@@ -52,21 +55,21 @@ vibetuner/templates/frontend/debug/info.html.jinja,sha256=Z9yZxW-dyOZ1kWsY1_ZX7j
|
|
|
52
55
|
vibetuner/templates/frontend/debug/users.html.jinja,sha256=BjTiJbOMadZYZJYvtYW0KeFcYN7ooI7_YylhOnfbLD4,7809
|
|
53
56
|
vibetuner/templates/frontend/debug/version.html.jinja,sha256=pJ-QuDRuvB7X04nC43upZiXLaFCC6vmI5QhRS62rt6k,2901
|
|
54
57
|
vibetuner/templates/frontend/email/magic_link.txt.jinja,sha256=fTVl3Wjfvp3EJAB5DYt01EL_O7o9r8lHedDH05YP44c,192
|
|
55
|
-
vibetuner/templates/frontend/email_sent.html.jinja,sha256=
|
|
56
|
-
vibetuner/templates/frontend/index.html.jinja,sha256=
|
|
58
|
+
vibetuner/templates/frontend/email_sent.html.jinja,sha256=5_Mc3DzgZaGB_SvAHaL7PFRXsGXnBpp1xO9QER7hlOQ,5234
|
|
59
|
+
vibetuner/templates/frontend/index.html.jinja,sha256=SwQPn5oqsZHq7_g4SlgWsrxxVvwuDNVfZ2nuIcRwm4A,921
|
|
57
60
|
vibetuner/templates/frontend/lang/select.html.jinja,sha256=4jHo8QWvMOIeK_KqHzSaDzgvuT3v8MlmjTrrYIl2sjk,224
|
|
58
|
-
vibetuner/templates/frontend/login.html.jinja,sha256=
|
|
61
|
+
vibetuner/templates/frontend/login.html.jinja,sha256=QihHEjCdxOevOQMDeuTTcbqLiBJ4OorzjunA-E4i-J4,5546
|
|
59
62
|
vibetuner/templates/frontend/meta/browserconfig.xml.jinja,sha256=5DE-Dowxw3fRg4UJerW6tVrUYdHWUsUOfS_YucoRVXQ,300
|
|
60
63
|
vibetuner/templates/frontend/meta/robots.txt.jinja,sha256=SUBJqQCOW5FFdD4uIkReo04NcAYnjITLyB4Wk1wBcS4,46
|
|
61
64
|
vibetuner/templates/frontend/meta/site.webmanifest.jinja,sha256=QCg2Z2GXd2AwJ3C8CnW9Brvu3cbXcZiquLNEzA8FsOc,150
|
|
62
65
|
vibetuner/templates/frontend/meta/sitemap.xml.jinja,sha256=IhBjk7p5OdqszyK6DR3eUAdeAucKk2s_PpnOfYxgNCo,170
|
|
63
|
-
vibetuner/templates/frontend/user/edit.html.jinja,sha256=
|
|
64
|
-
vibetuner/templates/frontend/user/profile.html.jinja,sha256=
|
|
66
|
+
vibetuner/templates/frontend/user/edit.html.jinja,sha256=tvr3FcH_iS_o4_S1xSrgHu-sk9GUSYPvyTS-wi4MUVE,5438
|
|
67
|
+
vibetuner/templates/frontend/user/profile.html.jinja,sha256=xjeZB5B_OUFdwx-c2N3mHN0geGW1ghXBpfLjF_u0sy4,9962
|
|
65
68
|
vibetuner/templates/markdown/.placeholder,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
66
69
|
vibetuner/templates.py,sha256=xRoMb_oyAI5x4kxfpg56UcLKkT8e9HVn-o3KFAu9ISE,5094
|
|
67
70
|
vibetuner/time.py,sha256=3_DtveCCzI20ocTnAlTh2u7FByUXtINaUoQZO-_uZow,1188
|
|
68
71
|
vibetuner/versioning.py,sha256=c7Wg-SM-oJzQqG2RE0O8gZGHzHTgvwqa4yHn3Dk5-Sk,372
|
|
69
|
-
vibetuner-2.
|
|
70
|
-
vibetuner-2.
|
|
71
|
-
vibetuner-2.
|
|
72
|
-
vibetuner-2.
|
|
72
|
+
vibetuner-2.44.1.dist-info/WHEEL,sha256=KSLUh82mDPEPk0Bx0ScXlWL64bc8KmzIPNcpQZFV-6E,79
|
|
73
|
+
vibetuner-2.44.1.dist-info/entry_points.txt,sha256=aKIj9YCCXizjYupx9PeWkUJePg3ncHke_LTS5rmCsfs,49
|
|
74
|
+
vibetuner-2.44.1.dist-info/METADATA,sha256=nfq8u-nFfVyzbs99IpeLmumHYEbx6rx0Ja4VFCr_BZQ,8603
|
|
75
|
+
vibetuner-2.44.1.dist-info/RECORD,,
|
|
File without changes
|