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 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 typing import Annotated
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
- if service == "worker":
34
- # Worker mode
35
- from streaq.cli import main as streaq_main
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
- if workers_count > 1:
44
- console.print(
45
- "[yellow]Warning: Multiple workers not supported in dev mode, using 1[/yellow]"
46
- )
47
-
48
- # Call streaq programmatically
49
- streaq_main(
50
- worker_path="vibetuner.tasks.worker.worker",
51
- workers=1,
52
- reload=True,
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
- elif service == "frontend":
59
- # Frontend mode
60
- from pathlib import Path
43
+ raise typer.Exit(code=0)
61
44
 
62
- from granian import Granian
63
- from granian.constants import Interfaces
45
+ is_dev = mode == "dev"
64
46
 
65
- frontend_port = port if port else 8000
47
+ if is_dev and workers > 1:
66
48
  console.print(
67
- f"[green]Starting frontend in dev mode on {host}:{frontend_port}[/green]"
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
- # Define paths to watch for changes
72
- reload_paths = [
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
- server = Granian(
80
- target="vibetuner.frontend:app",
81
- address=host,
82
- port=frontend_port,
83
- interface=Interfaces.ASGI,
84
- workers=workers_count,
85
- reload=True,
86
- reload_paths=reload_paths,
87
- log_level="info",
88
- log_access=True,
89
- )
90
-
91
- server.serve()
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
- os.environ["ENVIRONMENT"] = "production"
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="link")
191
- def link(
192
- target: Annotated[
193
- Path,
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 = MongoDsn("mongodb://localhost:27017")
110
- redis_url: RedisDsn = RedisDsn("redis://localhost:6379")
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
@@ -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 init_models
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 init_models()
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
@@ -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
+ )
@@ -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
- async def init_models() -> None:
12
- """Initialize MongoDB connection and register all Beanie models."""
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
- # Try to import user models to trigger their registration
44
+ # Import user models so they register themselves
15
45
  try:
16
46
  import_module("app.models")
17
47
  except ModuleNotFoundError:
18
- # Silent pass for missing app.models module (expected in some projects)
19
- pass
48
+ logger.debug("app.models not found; skipping user model import.")
20
49
  except ImportError as e:
21
- # Log warning for any import error (including syntax errors, missing dependencies, etc.)
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=client[settings.mongo_dbname], document_models=get_all_models()
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
@@ -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 init_models
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 init_models()
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 = Worker(
8
- redis_url=str(settings.redis_url),
9
- queue_name=settings.redis_key_prefix.rstrip(":"),
10
- lifespan=lifespan,
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-gradient-to-br from-primary to-secondary flex items-center justify-center p-4">
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-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex flex-col justify-between" %}
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-gradient-to-r from-blue-400 to-indigo-500 rounded-full"></div>
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-gradient-to-br from-primary to-secondary flex items-center justify-center p-4">
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="flex-shrink-0">
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-gradient-to-r from-blue-500 to-indigo-600 px-6 py-8">
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.30.1
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.10
22
- Requires-Dist: authlib>=1.6.5
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.122.0
28
- Requires-Dist: granian[pname]>=2.6.0
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.4
33
- Requires-Dist: pydantic-extra-types[pycountry]>=2.10.6
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.0.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.20.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.1 ; extra == 'dev'
48
- Requires-Dist: granian[pname,reload]>=2.6.0 ; extra == 'dev'
49
- Requires-Dist: just-bin>=1.43.1 ; extra == 'dev'
50
- Requires-Dist: prek>=0.2.18 ; extra == 'dev'
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.6 ; extra == 'dev'
53
- Requires-Dist: rumdl>=0.0.182 ; extra == 'dev'
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.1a27 ; extra == 'dev'
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.5.20251005 ; extra == 'dev'
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.3.1 ; extra == 'dev'
61
- Requires-Dist: pytest>=9.0.1 ; extra == 'test'
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 + MongoDB + HTMX web applications.
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
- - MongoDB integration with Beanie ODM
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=lmuLn8ytkO0JwX3ILm6AFQmrzEMe5LeHmwV2oyyy0sI,3543
4
- vibetuner/cli/run.py,sha256=TILyvy-bZTKWcAK-K2SNYqqD-G3ypCay-ghadGztqRQ,5021
5
- vibetuner/cli/scaffold.py,sha256=5MipgasDKsK6WDa_vpfY4my1kyismkaskvXE277NE-s,6450
6
- vibetuner/config.py,sha256=UfEyAlow6lE_npr53dgESb-KZQuohP18vlTncOuWJC4,4571
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=ZduSyH6E2_IW9vFWH5jww2R22mRybSm_mNfbtDHnsS0,1074
13
- vibetuner/frontend/middleware.py,sha256=iOqcp-7WwRqHLYa1dg3xDzAXGrpqmYpqBmtQcdd9obY,4890
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=BLBBcF9e-EUDQXeOUzCHgQx1D0lCMkLfUXgNCwG6CtE,6607
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=BejYrqHrupNfvNhEu6xUakCnRQy3YdiExk2r6hkafOs,1056
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=BKOI-wbMLOqEbR4NgCKI5c9OP8W1RpxOswBL3XzPTWc,861
40
- vibetuner/tasks/worker.py,sha256=ZCfBpaqpyDG5OD3wtSf9YdIWie6CaQ49KwLNn_iTNCc,248
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=Pq3J_WOwY_zFe9QJkvO29o6pw06JEnYxh068kyl_ZEg,5236
56
- vibetuner/templates/frontend/index.html.jinja,sha256=m8_xNS1O7JlrfWOHy0y7prf64HyVokQEeKvFqRWjxrQ,925
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=Y_dKk47-H6M8StepiFXntJcorBNXiOXKjORJN--SjOA,5548
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=dhmLFsokTWbtnmIE10S8gC6rDHToC6vNIUB2BRximKQ,5443
64
- vibetuner/templates/frontend/user/profile.html.jinja,sha256=qXFonyeCy99JE5o3rEtaOHtnxgg2-AlNykwIZyxpk_M,9964
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.30.1.dist-info/WHEEL,sha256=YUH1mBqsx8Dh2cQG2rlcuRYUhJddG9iClegy4IgnHik,79
70
- vibetuner-2.30.1.dist-info/entry_points.txt,sha256=aKIj9YCCXizjYupx9PeWkUJePg3ncHke_LTS5rmCsfs,49
71
- vibetuner-2.30.1.dist-info/METADATA,sha256=O25ikO_Zt59f7VbnTMTwQV4ADEFZb0cj7ze2zZzwiE8,8227
72
- vibetuner-2.30.1.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.11
2
+ Generator: uv 0.9.22
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any