vibetuner 2.26.6__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.
Files changed (71) hide show
  1. vibetuner/__init__.py +2 -0
  2. vibetuner/__main__.py +4 -0
  3. vibetuner/cli/__init__.py +141 -0
  4. vibetuner/cli/run.py +160 -0
  5. vibetuner/cli/scaffold.py +187 -0
  6. vibetuner/config.py +143 -0
  7. vibetuner/context.py +28 -0
  8. vibetuner/frontend/__init__.py +107 -0
  9. vibetuner/frontend/deps.py +41 -0
  10. vibetuner/frontend/email.py +45 -0
  11. vibetuner/frontend/hotreload.py +13 -0
  12. vibetuner/frontend/lifespan.py +37 -0
  13. vibetuner/frontend/middleware.py +151 -0
  14. vibetuner/frontend/oauth.py +196 -0
  15. vibetuner/frontend/routes/__init__.py +12 -0
  16. vibetuner/frontend/routes/auth.py +156 -0
  17. vibetuner/frontend/routes/debug.py +414 -0
  18. vibetuner/frontend/routes/health.py +37 -0
  19. vibetuner/frontend/routes/language.py +43 -0
  20. vibetuner/frontend/routes/meta.py +55 -0
  21. vibetuner/frontend/routes/user.py +94 -0
  22. vibetuner/frontend/templates.py +176 -0
  23. vibetuner/logging.py +87 -0
  24. vibetuner/models/__init__.py +14 -0
  25. vibetuner/models/blob.py +89 -0
  26. vibetuner/models/email_verification.py +84 -0
  27. vibetuner/models/mixins.py +76 -0
  28. vibetuner/models/oauth.py +57 -0
  29. vibetuner/models/registry.py +15 -0
  30. vibetuner/models/types.py +16 -0
  31. vibetuner/models/user.py +91 -0
  32. vibetuner/mongo.py +33 -0
  33. vibetuner/paths.py +250 -0
  34. vibetuner/services/__init__.py +0 -0
  35. vibetuner/services/blob.py +175 -0
  36. vibetuner/services/email.py +50 -0
  37. vibetuner/tasks/__init__.py +0 -0
  38. vibetuner/tasks/lifespan.py +28 -0
  39. vibetuner/tasks/worker.py +15 -0
  40. vibetuner/templates/email/magic_link.html.jinja +17 -0
  41. vibetuner/templates/email/magic_link.txt.jinja +5 -0
  42. vibetuner/templates/frontend/base/favicons.html.jinja +1 -0
  43. vibetuner/templates/frontend/base/footer.html.jinja +3 -0
  44. vibetuner/templates/frontend/base/header.html.jinja +0 -0
  45. vibetuner/templates/frontend/base/opengraph.html.jinja +7 -0
  46. vibetuner/templates/frontend/base/skeleton.html.jinja +45 -0
  47. vibetuner/templates/frontend/debug/collections.html.jinja +105 -0
  48. vibetuner/templates/frontend/debug/components/debug_nav.html.jinja +55 -0
  49. vibetuner/templates/frontend/debug/index.html.jinja +85 -0
  50. vibetuner/templates/frontend/debug/info.html.jinja +258 -0
  51. vibetuner/templates/frontend/debug/users.html.jinja +139 -0
  52. vibetuner/templates/frontend/debug/version.html.jinja +55 -0
  53. vibetuner/templates/frontend/email/magic_link.txt.jinja +5 -0
  54. vibetuner/templates/frontend/email_sent.html.jinja +83 -0
  55. vibetuner/templates/frontend/index.html.jinja +20 -0
  56. vibetuner/templates/frontend/lang/select.html.jinja +4 -0
  57. vibetuner/templates/frontend/login.html.jinja +89 -0
  58. vibetuner/templates/frontend/meta/browserconfig.xml.jinja +10 -0
  59. vibetuner/templates/frontend/meta/robots.txt.jinja +3 -0
  60. vibetuner/templates/frontend/meta/site.webmanifest.jinja +7 -0
  61. vibetuner/templates/frontend/meta/sitemap.xml.jinja +6 -0
  62. vibetuner/templates/frontend/user/edit.html.jinja +86 -0
  63. vibetuner/templates/frontend/user/profile.html.jinja +157 -0
  64. vibetuner/templates/markdown/.placeholder +0 -0
  65. vibetuner/templates.py +146 -0
  66. vibetuner/time.py +57 -0
  67. vibetuner/versioning.py +12 -0
  68. vibetuner-2.26.6.dist-info/METADATA +241 -0
  69. vibetuner-2.26.6.dist-info/RECORD +71 -0
  70. vibetuner-2.26.6.dist-info/WHEEL +4 -0
  71. vibetuner-2.26.6.dist-info/entry_points.txt +3 -0
vibetuner/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ # Your code goes here (add your code between this line and the EOF comment)
2
+ # End of file
vibetuner/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import app
2
+
3
+
4
+ app()
@@ -0,0 +1,141 @@
1
+ # ABOUTME: Core CLI setup with AsyncTyper wrapper and base configuration
2
+ # ABOUTME: Provides main CLI entry point and logging configuration
3
+ import importlib.metadata
4
+ import inspect
5
+ from functools import partial, wraps
6
+ from importlib import import_module
7
+ from pathlib import Path
8
+ from typing import Annotated
9
+
10
+ import asyncer
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.table import Table
14
+
15
+ from vibetuner.cli.run import run_app
16
+ from vibetuner.cli.scaffold import scaffold_app
17
+ from vibetuner.logging import LogLevel, logger, setup_logging
18
+
19
+
20
+ console = Console()
21
+
22
+
23
+ class AsyncTyper(typer.Typer):
24
+ def __init__(self, *args, **kwargs):
25
+ kwargs.setdefault("no_args_is_help", True)
26
+ super().__init__(*args, **kwargs)
27
+
28
+ @staticmethod
29
+ def maybe_run_async(decorator, f):
30
+ if inspect.iscoroutinefunction(f):
31
+
32
+ @wraps(f)
33
+ def runner(*args, **kwargs):
34
+ return asyncer.runnify(f)(*args, **kwargs)
35
+
36
+ decorator(runner)
37
+ else:
38
+ decorator(f)
39
+ return f
40
+
41
+ def callback(self, *args, **kwargs):
42
+ decorator = super().callback(*args, **kwargs)
43
+ return partial(self.maybe_run_async, decorator)
44
+
45
+ def command(self, *args, **kwargs):
46
+ decorator = super().command(*args, **kwargs)
47
+ return partial(self.maybe_run_async, decorator)
48
+
49
+
50
+ def _get_app_help():
51
+ try:
52
+ from vibetuner.config import settings
53
+
54
+ return f"{settings.project.project_name.title()} CLI"
55
+ except (RuntimeError, ImportError):
56
+ return "Vibetuner CLI"
57
+
58
+
59
+ app = AsyncTyper(help=_get_app_help())
60
+
61
+ LOG_LEVEL_OPTION = typer.Option(
62
+ LogLevel.INFO,
63
+ "--log-level",
64
+ "-l",
65
+ case_sensitive=False,
66
+ help="Set the logging level",
67
+ )
68
+
69
+
70
+ @app.callback()
71
+ def callback(log_level: LogLevel | None = LOG_LEVEL_OPTION) -> None:
72
+ """Initialize logging and other global settings."""
73
+ setup_logging(level=log_level)
74
+
75
+
76
+ @app.command()
77
+ def version(
78
+ show_app: bool = typer.Option(
79
+ False,
80
+ "--app",
81
+ "-a",
82
+ help="Show app settings version even if not in a project directory",
83
+ ),
84
+ ) -> None:
85
+ """Show version information."""
86
+ try:
87
+ # Get vibetuner package version
88
+ vibetuner_version = importlib.metadata.version("vibetuner")
89
+ except importlib.metadata.PackageNotFoundError:
90
+ vibetuner_version = "unknown"
91
+
92
+ # Create table for nice display
93
+ table = Table(title="Version Information")
94
+ table.add_column("Component", style="cyan", no_wrap=True)
95
+ table.add_column("Version", style="green", no_wrap=True)
96
+
97
+ # Always show vibetuner package version
98
+ table.add_row("vibetuner package", vibetuner_version)
99
+
100
+ # Show app version if requested or if in a project
101
+ try:
102
+ from vibetuner.config import CoreConfiguration
103
+
104
+ settings = CoreConfiguration()
105
+ table.add_row(f"{settings.project.project_name} settings", settings.version)
106
+ except Exception:
107
+ if show_app:
108
+ table.add_row("app settings", "not in project directory")
109
+ # else: don't show app version if not in project and not requested
110
+
111
+ console.print(table)
112
+
113
+
114
+ @app.command()
115
+ def core_template_symlink(
116
+ target: Annotated[
117
+ Path,
118
+ typer.Argument(
119
+ help="Path where the 'core' symlink should be created or updated",
120
+ ),
121
+ ],
122
+ ) -> None:
123
+ """Create or update a 'core' symlink to the package templates directory."""
124
+ from vibetuner.paths import create_core_templates_symlink
125
+
126
+ create_core_templates_symlink(target)
127
+
128
+
129
+ app.add_typer(run_app, name="run")
130
+ app.add_typer(scaffold_app, name="scaffold")
131
+
132
+ try:
133
+ import_module("app.cli")
134
+ except ModuleNotFoundError:
135
+ # Silent pass for missing app.cli module (expected in some projects)
136
+ pass
137
+ except ImportError as e:
138
+ # Log warning for any import error (including syntax errors, missing dependencies, etc.)
139
+ logger.warning(
140
+ f"Failed to import app.cli: {e}. User CLI commands will not be available."
141
+ )
vibetuner/cli/run.py ADDED
@@ -0,0 +1,160 @@
1
+ # ABOUTME: Run commands for starting the application in different modes
2
+ # ABOUTME: Supports dev/prod modes for frontend and worker services
3
+ import os
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from rich.console import Console
8
+
9
+
10
+ console = Console()
11
+
12
+ run_app = typer.Typer(
13
+ help="Run the application in different modes", no_args_is_help=True
14
+ )
15
+
16
+
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
+
33
+ if service == "worker":
34
+ # Worker mode
35
+ from streaq.cli import main as streaq_main
36
+
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
+
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,
57
+ )
58
+ elif service == "frontend":
59
+ # Frontend mode
60
+ from pathlib import Path
61
+
62
+ from granian import Granian
63
+ from granian.constants import Interfaces
64
+
65
+ frontend_port = port if port else 8000
66
+ console.print(
67
+ f"[green]Starting frontend in dev mode on {host}:{frontend_port}[/green]"
68
+ )
69
+ console.print("[dim]Watching for changes in src/ and templates/[/dim]")
70
+
71
+ # Define paths to watch for changes
72
+ reload_paths = [
73
+ Path("src/app"),
74
+ Path("templates/frontend"),
75
+ Path("templates/email"),
76
+ Path("templates/markdown"),
77
+ ]
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()
92
+ else:
93
+ console.print(f"[red]Error: Unknown service '{service}'[/red]")
94
+ console.print("[yellow]Valid services: 'frontend' or 'worker'[/yellow]")
95
+ raise typer.Exit(code=1)
96
+
97
+
98
+ @run_app.command(name="prod")
99
+ def prod(
100
+ service: Annotated[
101
+ str, typer.Argument(help="Service to run: 'frontend' or 'worker'")
102
+ ] = "frontend",
103
+ port: int = typer.Option(
104
+ None, help="Port to run on (8000 for frontend, 11111 for worker)"
105
+ ),
106
+ host: str = typer.Option("0.0.0.0", help="Host to bind to (frontend only)"), # noqa: S104
107
+ workers_count: int = typer.Option(
108
+ 4, "--workers", help="Number of worker processes"
109
+ ),
110
+ ) -> None:
111
+ """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)
@@ -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 ADDED
@@ -0,0 +1,143 @@
1
+ import base64
2
+ import hashlib
3
+ from datetime import datetime
4
+ from functools import cached_property
5
+ from typing import Annotated
6
+
7
+ import yaml
8
+ from pydantic import (
9
+ UUID4,
10
+ Field,
11
+ HttpUrl,
12
+ MongoDsn,
13
+ RedisDsn,
14
+ SecretStr,
15
+ computed_field,
16
+ )
17
+ from pydantic_extra_types.language_code import LanguageAlpha2
18
+ from pydantic_settings import BaseSettings, SettingsConfigDict
19
+
20
+ from vibetuner.logging import logger
21
+
22
+ from .paths import config_vars as config_vars_path
23
+ from .versioning import version
24
+
25
+
26
+ current_year: int = datetime.now().year
27
+
28
+
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
+ )
35
+ if not config_vars_path.exists():
36
+ return ProjectConfiguration()
37
+
38
+ yaml_data = yaml.safe_load(config_vars_path.read_text(encoding="utf-8"))
39
+ return ProjectConfiguration(**yaml_data)
40
+
41
+
42
+ class ProjectConfiguration(BaseSettings):
43
+ @classmethod
44
+ def from_project_config(cls) -> "ProjectConfiguration":
45
+ return _load_project_config()
46
+
47
+ project_slug: str = "default_project"
48
+ project_name: str = "default_project"
49
+
50
+ project_description: str = "A default project description."
51
+
52
+ # Language Related Settings
53
+ supported_languages: set[LanguageAlpha2] | None = None
54
+ default_language: LanguageAlpha2 = LanguageAlpha2("en")
55
+
56
+ # AWS Parameters
57
+ aws_default_region: str = "eu-central-1"
58
+
59
+ # Company Name
60
+ company_name: str = "Acme Corp"
61
+
62
+ # From Email for transactional emails
63
+ from_email: str = "no-reply@example.com"
64
+
65
+ # Copyright
66
+ copyright_start: Annotated[int, Field(strict=True, gt=1714, lt=2048)] = current_year
67
+
68
+ # Analytics
69
+ umami_website_id: UUID4 | None = None
70
+
71
+ # Fully Qualified Domain Name
72
+ fqdn: str | None = None
73
+
74
+ @cached_property
75
+ def languages(self) -> set[str]:
76
+ if self.supported_languages is None:
77
+ return {self.language}
78
+
79
+ return {
80
+ str(lang) for lang in (*self.supported_languages, self.default_language)
81
+ }
82
+
83
+ @cached_property
84
+ def language(self) -> str:
85
+ return str(self.default_language)
86
+
87
+ @cached_property
88
+ def copyright(self) -> str:
89
+ year_part = (
90
+ f"{self.copyright_start}-{current_year}"
91
+ if self.copyright_start and self.copyright_start != current_year
92
+ else str(current_year)
93
+ )
94
+ return f"© {year_part}{f' {self.company_name}' if self.company_name else ''}"
95
+
96
+ model_config = SettingsConfigDict(case_sensitive=False, extra="ignore")
97
+
98
+
99
+ class CoreConfiguration(BaseSettings):
100
+ project: ProjectConfiguration = ProjectConfiguration.from_project_config()
101
+
102
+ debug: bool = False
103
+ version: str = version
104
+ session_key: SecretStr = SecretStr("ct-!secret-must-change-me")
105
+
106
+ # Database and Cache URLs
107
+ mongodb_url: MongoDsn = MongoDsn("mongodb://localhost:27017")
108
+ redis_url: RedisDsn = RedisDsn("redis://localhost:6379")
109
+
110
+ aws_access_key_id: SecretStr | None = None
111
+ aws_secret_access_key: SecretStr | None = None
112
+
113
+ r2_default_bucket_name: str | None = None
114
+ r2_bucket_endpoint_url: HttpUrl | None = None
115
+ r2_access_key: SecretStr | None = None
116
+ r2_secret_key: SecretStr | None = None
117
+ r2_default_region: str = "auto"
118
+
119
+ @computed_field
120
+ @cached_property
121
+ def v_hash(self) -> str:
122
+ hash_object = hashlib.sha256(self.version.encode("utf-8"))
123
+ hash_bytes = hash_object.digest()
124
+
125
+ b64_hash = base64.urlsafe_b64encode(hash_bytes).decode("utf-8")
126
+
127
+ url_safe_hash = b64_hash.rstrip("=")[:8]
128
+
129
+ return url_safe_hash
130
+
131
+ @cached_property
132
+ def mongo_dbname(self) -> str:
133
+ return self.project.project_slug
134
+
135
+ model_config = SettingsConfigDict(
136
+ case_sensitive=False, extra="ignore", env_file=".env"
137
+ )
138
+
139
+
140
+ settings = CoreConfiguration()
141
+
142
+
143
+ logger.info("Configuration loaded for project: {}", settings.project.project_name)
vibetuner/context.py ADDED
@@ -0,0 +1,28 @@
1
+ from pydantic import UUID4, BaseModel
2
+
3
+ from vibetuner.config import settings
4
+
5
+
6
+ class Context(BaseModel):
7
+ DEBUG: bool = settings.debug
8
+
9
+ project_name: str = settings.project.project_name
10
+ project_slug: str = settings.project.project_slug
11
+ project_description: str = settings.project.project_description
12
+
13
+ version: str = settings.version
14
+ v_hash: str = settings.v_hash
15
+
16
+ copyright: str = settings.project.copyright
17
+
18
+ default_language: str = settings.project.language
19
+ supported_languages: set[str] = settings.project.languages
20
+
21
+ umami_website_id: UUID4 | None = settings.project.umami_website_id
22
+
23
+ fqdn: str | None = settings.project.fqdn
24
+
25
+ model_config = {"arbitrary_types_allowed": True}
26
+
27
+
28
+ ctx = Context()