vibetuner 2.7.0__py3-none-any.whl → 2.18.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.

Files changed (52) hide show
  1. vibetuner/cli/__init__.py +13 -2
  2. vibetuner/cli/run.py +0 -1
  3. vibetuner/cli/scaffold.py +187 -0
  4. vibetuner/config.py +27 -11
  5. vibetuner/context.py +3 -0
  6. vibetuner/frontend/__init__.py +7 -2
  7. vibetuner/frontend/lifespan.py +12 -7
  8. vibetuner/frontend/middleware.py +3 -3
  9. vibetuner/frontend/routes/auth.py +19 -13
  10. vibetuner/frontend/routes/debug.py +1 -1
  11. vibetuner/frontend/routes/health.py +4 -0
  12. vibetuner/frontend/routes/user.py +1 -1
  13. vibetuner/mongo.py +1 -1
  14. vibetuner/paths.py +197 -80
  15. vibetuner/tasks/worker.py +1 -1
  16. vibetuner/templates/email/{default/magic_link.html.jinja → magic_link.html.jinja} +2 -1
  17. vibetuner/templates/frontend/base/favicons.html.jinja +1 -1
  18. vibetuner/templates/frontend/base/skeleton.html.jinja +5 -2
  19. vibetuner/templates/frontend/debug/collections.html.jinja +2 -0
  20. vibetuner/templates/frontend/debug/components/debug_nav.html.jinja +6 -6
  21. vibetuner/templates/frontend/debug/index.html.jinja +6 -4
  22. vibetuner/templates/frontend/debug/info.html.jinja +2 -0
  23. vibetuner/templates/frontend/debug/users.html.jinja +4 -2
  24. vibetuner/templates/frontend/debug/version.html.jinja +2 -0
  25. vibetuner/templates/frontend/email_sent.html.jinja +2 -1
  26. vibetuner/templates/frontend/index.html.jinja +1 -0
  27. vibetuner/templates/frontend/login.html.jinja +8 -3
  28. vibetuner/templates/frontend/user/edit.html.jinja +3 -2
  29. vibetuner/templates/frontend/user/profile.html.jinja +2 -1
  30. vibetuner/templates.py +9 -15
  31. vibetuner/versioning.py +1 -1
  32. vibetuner-2.18.1.dist-info/METADATA +241 -0
  33. vibetuner-2.18.1.dist-info/RECORD +72 -0
  34. {vibetuner-2.7.0.dist-info → vibetuner-2.18.1.dist-info}/WHEEL +1 -1
  35. vibetuner-2.18.1.dist-info/entry_points.txt +3 -0
  36. vibetuner/frontend/AGENTS.md +0 -113
  37. vibetuner/frontend/CLAUDE.md +0 -113
  38. vibetuner/models/AGENTS.md +0 -165
  39. vibetuner/models/CLAUDE.md +0 -165
  40. vibetuner/services/AGENTS.md +0 -104
  41. vibetuner/services/CLAUDE.md +0 -104
  42. vibetuner/tasks/AGENTS.md +0 -98
  43. vibetuner/tasks/CLAUDE.md +0 -98
  44. vibetuner/templates/email/AGENTS.md +0 -48
  45. vibetuner/templates/email/CLAUDE.md +0 -48
  46. vibetuner/templates/frontend/AGENTS.md +0 -74
  47. vibetuner/templates/frontend/CLAUDE.md +0 -74
  48. vibetuner/templates/markdown/AGENTS.md +0 -29
  49. vibetuner/templates/markdown/CLAUDE.md +0 -29
  50. vibetuner-2.7.0.dist-info/METADATA +0 -48
  51. vibetuner-2.7.0.dist-info/RECORD +0 -84
  52. /vibetuner/templates/email/{default/magic_link.txt.jinja → magic_link.txt.jinja} +0 -0
vibetuner/cli/__init__.py CHANGED
@@ -9,7 +9,7 @@ import typer
9
9
  from rich.console import Console
10
10
 
11
11
  from vibetuner.cli.run import run_app
12
- from vibetuner.config import settings
12
+ from vibetuner.cli.scaffold import scaffold_app
13
13
  from vibetuner.logging import LogLevel, setup_logging
14
14
 
15
15
 
@@ -43,7 +43,16 @@ class AsyncTyper(typer.Typer):
43
43
  return partial(self.maybe_run_async, decorator)
44
44
 
45
45
 
46
- app = AsyncTyper(help=f"{settings.project.project_name.title()} CLI")
46
+ def _get_app_help():
47
+ try:
48
+ from vibetuner.config import settings
49
+
50
+ return f"{settings.project.project_name.title()} CLI"
51
+ except (RuntimeError, ImportError):
52
+ return "Vibetuner CLI"
53
+
54
+
55
+ app = AsyncTyper(help=_get_app_help())
47
56
 
48
57
  LOG_LEVEL_OPTION = typer.Option(
49
58
  LogLevel.INFO,
@@ -61,8 +70,10 @@ def callback(log_level: LogLevel | None = LOG_LEVEL_OPTION) -> None:
61
70
 
62
71
 
63
72
  app.add_typer(run_app, name="run")
73
+ app.add_typer(scaffold_app, name="scaffold")
64
74
 
65
75
  try:
66
76
  import_module("app.cli")
67
77
  except (ImportError, ModuleNotFoundError):
68
78
  pass
79
+ # Cache buster
vibetuner/cli/run.py CHANGED
@@ -70,7 +70,6 @@ def dev(
70
70
 
71
71
  # Define paths to watch for changes
72
72
  reload_paths = [
73
- Path("src/vibetuner"),
74
73
  Path("src/app"),
75
74
  Path("templates/frontend"),
76
75
  Path("templates/email"),
@@ -0,0 +1,187 @@
1
+ # ABOUTME: Scaffolding commands for creating new projects from the vibetuner template
2
+ # ABOUTME: Uses Copier to generate FastAPI+MongoDB+HTMX projects with interactive prompts
3
+ from pathlib import Path
4
+ from typing import Annotated
5
+
6
+ import copier
7
+ import typer
8
+ from rich.console import Console
9
+
10
+
11
+ console = Console()
12
+
13
+ scaffold_app = typer.Typer(
14
+ help="Create new projects from the vibetuner template", no_args_is_help=True
15
+ )
16
+
17
+
18
+ @scaffold_app.command(name="new")
19
+ def new(
20
+ destination: Annotated[
21
+ Path,
22
+ typer.Argument(
23
+ help="Destination directory for the new project",
24
+ exists=False,
25
+ ),
26
+ ],
27
+ defaults: Annotated[
28
+ bool,
29
+ typer.Option(
30
+ "--defaults",
31
+ "-d",
32
+ help="Use default values for all prompts (non-interactive mode)",
33
+ ),
34
+ ] = False,
35
+ data: Annotated[
36
+ list[str] | None,
37
+ typer.Option(
38
+ "--data",
39
+ help="Override template variables in key=value format (can be used multiple times)",
40
+ ),
41
+ ] = None,
42
+ branch: Annotated[
43
+ str | None,
44
+ typer.Option(
45
+ "--branch",
46
+ "-b",
47
+ help="Use specific branch/tag from the vibetuner template repository",
48
+ ),
49
+ ] = None,
50
+ ) -> None:
51
+ """Create a new project from the vibetuner template.
52
+
53
+ Examples:
54
+
55
+ # Interactive mode (prompts for all values)
56
+ vibetuner scaffold new my-project
57
+
58
+ # Use defaults for all prompts
59
+ vibetuner scaffold new my-project --defaults
60
+
61
+ # Override specific values
62
+ vibetuner scaffold new my-project --data project_name="My App" --data python_version="3.13"
63
+
64
+ # Use specific branch for testing
65
+ vibetuner scaffold new my-project --branch fix/scaffold-command
66
+ """
67
+ # Use the official vibetuner template from GitHub
68
+ template_src = "gh:alltuner/vibetuner"
69
+ vcs_ref = branch or "main" # Use specified branch or default to main
70
+
71
+ if branch:
72
+ console.print(
73
+ f"[dim]Using vibetuner template from GitHub ({branch} branch)[/dim]"
74
+ )
75
+ else:
76
+ console.print("[dim]Using vibetuner template from GitHub (main branch)[/dim]")
77
+
78
+ # Parse data overrides
79
+ data_dict = {}
80
+ if data:
81
+ for item in data:
82
+ if "=" not in item:
83
+ console.print(
84
+ f"[red]Error: Invalid data format '{item}'. Expected key=value[/red]"
85
+ )
86
+ raise typer.Exit(code=1)
87
+ key, value = item.split("=", 1)
88
+ data_dict[key] = value
89
+
90
+ # When using defaults, provide sensible default values for required fields
91
+ if defaults:
92
+ default_values = {
93
+ "company_name": "Acme Corp",
94
+ "author_name": "Developer",
95
+ "author_email": "dev@example.com",
96
+ "supported_languages": [],
97
+ }
98
+ # Merge: user overrides take precedence over defaults
99
+ data_dict = {**default_values, **data_dict}
100
+
101
+ # Run copier
102
+ try:
103
+ console.print(f"\n[green]Creating new project in: {destination}[/green]\n")
104
+
105
+ copier.run_copy(
106
+ src_path=str(template_src),
107
+ dst_path=destination,
108
+ data=data_dict if data_dict else None,
109
+ defaults=defaults,
110
+ quiet=defaults, # Suppress prompts when using defaults
111
+ unsafe=True, # Allow running post-generation tasks
112
+ vcs_ref=vcs_ref, # Use the specified branch or default to main
113
+ )
114
+
115
+ console.print("\n[green]✓ Project created successfully![/green]")
116
+ console.print("\nNext steps:")
117
+ console.print(f" cd {destination}")
118
+ console.print(" just dev")
119
+
120
+ except Exception as e:
121
+ console.print(f"[red]Error creating project: {e}[/red]")
122
+ raise typer.Exit(code=1) from None
123
+
124
+
125
+ @scaffold_app.command(name="update")
126
+ def update(
127
+ path: Annotated[
128
+ Path | None,
129
+ typer.Argument(
130
+ help="Path to the project to update",
131
+ ),
132
+ ] = None,
133
+ skip_answered: Annotated[
134
+ bool,
135
+ typer.Option(
136
+ "--skip-answered",
137
+ "-s",
138
+ help="Skip questions that have already been answered",
139
+ ),
140
+ ] = True,
141
+ ) -> None:
142
+ """Update an existing project to the latest template version.
143
+
144
+ This will update the project's files to match the latest template version,
145
+ while preserving your answers to the original questions.
146
+
147
+ Examples:
148
+
149
+ # Update current directory
150
+ vibetuner scaffold update
151
+
152
+ # Update specific directory
153
+ vibetuner scaffold update /path/to/project
154
+
155
+ # Re-prompt for all questions
156
+ vibetuner scaffold update --no-skip-answered
157
+ """
158
+ if path is None:
159
+ path = Path.cwd()
160
+
161
+ if not path.exists():
162
+ console.print(f"[red]Error: Directory does not exist: {path}[/red]")
163
+ raise typer.Exit(code=1)
164
+
165
+ # Check if it's a copier project
166
+ answers_file = path / ".copier-answers.yml"
167
+ if not answers_file.exists():
168
+ console.print(
169
+ "[red]Error: Not a copier project (missing .copier-answers.yml)[/red]"
170
+ )
171
+ console.print(f"[yellow]Directory: {path}[/yellow]")
172
+ raise typer.Exit(code=1)
173
+
174
+ try:
175
+ console.print(f"\n[green]Updating project: {path}[/green]\n")
176
+
177
+ copier.run_update(
178
+ dst_path=path,
179
+ skip_answered=skip_answered,
180
+ unsafe=True, # Allow running post-generation tasks
181
+ )
182
+
183
+ console.print("\n[green]✓ Project updated successfully![/green]")
184
+
185
+ except Exception as e:
186
+ console.print(f"[red]Error updating project: {e}[/red]")
187
+ raise typer.Exit(code=1) from None
vibetuner/config.py CHANGED
@@ -17,22 +17,33 @@ from pydantic import (
17
17
  from pydantic_extra_types.language_code import LanguageAlpha2
18
18
  from pydantic_settings import BaseSettings, SettingsConfigDict
19
19
 
20
- from vibetuner.paths import config_vars as config_vars_path
21
- from vibetuner.versioning import version
20
+ from vibetuner.logging import logger
21
+
22
+ from .paths import config_vars as config_vars_path
23
+ from .versioning import version
22
24
 
23
25
 
24
26
  current_year: int = datetime.now().year
25
27
 
26
28
 
27
29
  def _load_project_config() -> "ProjectConfiguration":
30
+ if config_vars_path is None:
31
+ raise RuntimeError(
32
+ "Project root not detected. Cannot load project configuration. "
33
+ "Ensure you're running from within a project directory with .copier-answers.yml"
34
+ )
28
35
  if not config_vars_path.exists():
29
36
  return ProjectConfiguration()
30
- return ProjectConfiguration(
31
- **yaml.safe_load(config_vars_path.read_text(encoding="utf-8"))
32
- )
37
+
38
+ yaml_data = yaml.safe_load(config_vars_path.read_text(encoding="utf-8"))
39
+ return ProjectConfiguration(**yaml_data)
33
40
 
34
41
 
35
42
  class ProjectConfiguration(BaseSettings):
43
+ @classmethod
44
+ def from_project_config(cls) -> "ProjectConfiguration":
45
+ return _load_project_config()
46
+
36
47
  project_slug: str = "default_project"
37
48
  project_name: str = "default_project"
38
49
 
@@ -42,9 +53,6 @@ class ProjectConfiguration(BaseSettings):
42
53
  supported_languages: set[LanguageAlpha2] | None = None
43
54
  default_language: LanguageAlpha2 = LanguageAlpha2("en")
44
55
 
45
- mongodb_url: MongoDsn | None = None
46
- redis_url: RedisDsn | None = None
47
-
48
56
  # AWS Parameters
49
57
  aws_default_region: str = "eu-central-1"
50
58
 
@@ -85,16 +93,20 @@ class ProjectConfiguration(BaseSettings):
85
93
  )
86
94
  return f"© {year_part}{f' {self.company_name}' if self.company_name else ''}"
87
95
 
88
- model_config = SettingsConfigDict(extra="ignore")
96
+ model_config = SettingsConfigDict(case_sensitive=False, extra="ignore")
89
97
 
90
98
 
91
99
  class CoreConfiguration(BaseSettings):
92
- project: ProjectConfiguration
100
+ project: ProjectConfiguration = ProjectConfiguration.from_project_config()
93
101
 
94
102
  debug: bool = False
95
103
  version: str = version
96
104
  session_key: SecretStr = SecretStr("ct-!secret-must-change-me")
97
105
 
106
+ # Database and Cache URLs
107
+ mongodb_url: MongoDsn = MongoDsn("mongodb://localhost:27017")
108
+ redis_url: RedisDsn = RedisDsn("redis://localhost:6379")
109
+
98
110
  aws_access_key_id: SecretStr | None = None
99
111
  aws_secret_access_key: SecretStr | None = None
100
112
 
@@ -125,4 +137,8 @@ class CoreConfiguration(BaseSettings):
125
137
  )
126
138
 
127
139
 
128
- settings = CoreConfiguration(project=_load_project_config())
140
+ settings = CoreConfiguration()
141
+
142
+
143
+ logger.info("Configuration loaded for project: {}", settings.project.project_name)
144
+ logger.info("Configuration loaded for project: {}", settings.model_dump())
vibetuner/context.py CHANGED
@@ -23,3 +23,6 @@ class Context(BaseModel):
23
23
  fqdn: str | None = settings.project.fqdn
24
24
 
25
25
  model_config = {"arbitrary_types_allowed": True}
26
+
27
+
28
+ ctx = Context()
@@ -21,8 +21,13 @@ def register_router(router: APIRouter) -> None:
21
21
 
22
22
 
23
23
  try:
24
- import app.frontend.oauth as _app_oauth # noqa: F401
25
- import app.frontend.routes as _app_routes # noqa: F401
24
+ import app.frontend.oauth as _app_oauth # noqa: F401 # type: ignore[unresolved-import]
25
+ import app.frontend.routes as _app_routes # noqa: F401 # type: ignore[unresolved-import]
26
+
27
+ # Register OAuth routes after providers are registered
28
+ from .routes.auth import register_oauth_routes
29
+
30
+ register_oauth_routes()
26
31
  except (ImportError, ModuleNotFoundError):
27
32
  pass
28
33
 
@@ -2,25 +2,30 @@ from contextlib import asynccontextmanager
2
2
 
3
3
  from fastapi import FastAPI
4
4
 
5
+ from vibetuner.context import ctx
6
+ from vibetuner.logging import logger
5
7
  from vibetuner.mongo import init_models
6
8
 
7
- from .context import ctx
8
9
  from .hotreload import hotreload
9
10
 
10
11
 
11
12
  @asynccontextmanager
12
- async def lifespan(app: FastAPI):
13
+ async def base_lifespan(app: FastAPI):
14
+ logger.info("Vibetuner frontend starting")
13
15
  if ctx.DEBUG:
14
16
  await hotreload.startup()
15
17
 
16
18
  await init_models()
17
- # Add below anything that should happen before startup
18
19
 
19
- # Until here
20
20
  yield
21
21
 
22
- # Add below anything that should happen before shutdown
23
-
24
- # Until here
22
+ logger.info("Vibetuner frontend stopping")
25
23
  if ctx.DEBUG:
26
24
  await hotreload.shutdown()
25
+ logger.info("Vibetuner frontend stopped")
26
+
27
+
28
+ try:
29
+ from app.frontend.lifespan import lifespan # ty: ignore
30
+ except ImportError:
31
+ lifespan = base_lifespan
@@ -13,12 +13,12 @@ from starlette_babel import (
13
13
  LocaleMiddleware,
14
14
  get_translator,
15
15
  )
16
- from starlette_htmx.middleware import HtmxMiddleware # type: ignore[import-untyped]
16
+ from starlette_htmx.middleware import HtmxMiddleware
17
17
 
18
18
  from vibetuner.config import settings
19
+ from vibetuner.context import ctx
19
20
  from vibetuner.paths import locales as locales_path
20
21
 
21
- from .context import ctx
22
22
  from .oauth import WebUser
23
23
 
24
24
 
@@ -66,7 +66,7 @@ def user_preference_selector(conn: HTTPConnection) -> str | None:
66
66
 
67
67
 
68
68
  shared_translator = get_translator()
69
- if locales_path.exists() and locales_path.is_dir():
69
+ if locales_path is not None and locales_path.exists() and locales_path.is_dir():
70
70
  # Load translations from the locales directory
71
71
  shared_translator.load_from_directories([locales_path])
72
72
 
@@ -135,16 +135,22 @@ async def email_verify(
135
135
  return next or get_homepage_url(request)
136
136
 
137
137
 
138
- for provider in get_oauth_providers():
139
- router.get(
140
- f"/provider/{provider}",
141
- response_class=RedirectResponse,
142
- name=f"auth_with_{provider}",
143
- response_model=None,
144
- )(_create_auth_handler(provider))
145
-
146
- router.get(
147
- f"/login/provider/{provider}",
148
- name=f"login_with_{provider}",
149
- response_model=None,
150
- )(_create_auth_login_handler(provider))
138
+ def register_oauth_routes() -> None:
139
+ """Register OAuth provider routes dynamically.
140
+
141
+ This must be called after OAuth providers are registered to ensure
142
+ routes are created for all configured providers.
143
+ """
144
+ for provider in get_oauth_providers():
145
+ router.get(
146
+ f"/provider/{provider}",
147
+ response_class=RedirectResponse,
148
+ name=f"auth_with_{provider}",
149
+ response_model=None,
150
+ )(_create_auth_handler(provider))
151
+
152
+ router.get(
153
+ f"/login/provider/{provider}",
154
+ name=f"login_with_{provider}",
155
+ response_model=None,
156
+ )(_create_auth_login_handler(provider))
@@ -10,10 +10,10 @@ from fastapi.responses import (
10
10
  RedirectResponse,
11
11
  )
12
12
 
13
+ from vibetuner.context import ctx
13
14
  from vibetuner.models import UserModel
14
15
  from vibetuner.models.registry import get_all_models
15
16
 
16
- from ..context import ctx
17
17
  from ..deps import MAGIC_COOKIE_NAME
18
18
  from ..templates import render_template
19
19
 
@@ -22,6 +22,10 @@ def health_ping():
22
22
  @router.get("/id")
23
23
  def health_instance_id():
24
24
  """Instance identification endpoint for distinguishing app instances"""
25
+ if root_path is None:
26
+ raise RuntimeError(
27
+ "Project root not detected. Cannot provide instance information."
28
+ )
25
29
  return {
26
30
  "app": settings.project.project_slug,
27
31
  "port": int(os.environ.get("PORT", 8000)),
@@ -3,9 +3,9 @@ from fastapi.responses import HTMLResponse, RedirectResponse
3
3
  from pydantic_extra_types.language_code import LanguageAlpha2
4
4
  from starlette.authentication import requires
5
5
 
6
+ from vibetuner.context import ctx
6
7
  from vibetuner.models import UserModel
7
8
 
8
- from ..context import ctx
9
9
  from ..templates import render_template
10
10
 
11
11
 
vibetuner/mongo.py CHANGED
@@ -9,7 +9,7 @@ async def init_models() -> None:
9
9
  """Initialize MongoDB connection and register all Beanie models."""
10
10
 
11
11
  client: AsyncMongoClient = AsyncMongoClient(
12
- host=str(settings.project.mongodb_url),
12
+ host=str(settings.mongodb_url),
13
13
  compressors=["zstd"],
14
14
  )
15
15