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.
- vibetuner/__init__.py +2 -0
- vibetuner/__main__.py +4 -0
- vibetuner/cli/__init__.py +141 -0
- vibetuner/cli/run.py +160 -0
- vibetuner/cli/scaffold.py +187 -0
- vibetuner/config.py +143 -0
- vibetuner/context.py +28 -0
- vibetuner/frontend/__init__.py +107 -0
- vibetuner/frontend/deps.py +41 -0
- vibetuner/frontend/email.py +45 -0
- vibetuner/frontend/hotreload.py +13 -0
- vibetuner/frontend/lifespan.py +37 -0
- vibetuner/frontend/middleware.py +151 -0
- vibetuner/frontend/oauth.py +196 -0
- vibetuner/frontend/routes/__init__.py +12 -0
- vibetuner/frontend/routes/auth.py +156 -0
- vibetuner/frontend/routes/debug.py +414 -0
- vibetuner/frontend/routes/health.py +37 -0
- vibetuner/frontend/routes/language.py +43 -0
- vibetuner/frontend/routes/meta.py +55 -0
- vibetuner/frontend/routes/user.py +94 -0
- vibetuner/frontend/templates.py +176 -0
- vibetuner/logging.py +87 -0
- vibetuner/models/__init__.py +14 -0
- vibetuner/models/blob.py +89 -0
- vibetuner/models/email_verification.py +84 -0
- vibetuner/models/mixins.py +76 -0
- vibetuner/models/oauth.py +57 -0
- vibetuner/models/registry.py +15 -0
- vibetuner/models/types.py +16 -0
- vibetuner/models/user.py +91 -0
- vibetuner/mongo.py +33 -0
- vibetuner/paths.py +250 -0
- vibetuner/services/__init__.py +0 -0
- vibetuner/services/blob.py +175 -0
- vibetuner/services/email.py +50 -0
- vibetuner/tasks/__init__.py +0 -0
- vibetuner/tasks/lifespan.py +28 -0
- vibetuner/tasks/worker.py +15 -0
- vibetuner/templates/email/magic_link.html.jinja +17 -0
- vibetuner/templates/email/magic_link.txt.jinja +5 -0
- vibetuner/templates/frontend/base/favicons.html.jinja +1 -0
- vibetuner/templates/frontend/base/footer.html.jinja +3 -0
- vibetuner/templates/frontend/base/header.html.jinja +0 -0
- vibetuner/templates/frontend/base/opengraph.html.jinja +7 -0
- vibetuner/templates/frontend/base/skeleton.html.jinja +45 -0
- vibetuner/templates/frontend/debug/collections.html.jinja +105 -0
- vibetuner/templates/frontend/debug/components/debug_nav.html.jinja +55 -0
- vibetuner/templates/frontend/debug/index.html.jinja +85 -0
- vibetuner/templates/frontend/debug/info.html.jinja +258 -0
- vibetuner/templates/frontend/debug/users.html.jinja +139 -0
- vibetuner/templates/frontend/debug/version.html.jinja +55 -0
- vibetuner/templates/frontend/email/magic_link.txt.jinja +5 -0
- vibetuner/templates/frontend/email_sent.html.jinja +83 -0
- vibetuner/templates/frontend/index.html.jinja +20 -0
- vibetuner/templates/frontend/lang/select.html.jinja +4 -0
- vibetuner/templates/frontend/login.html.jinja +89 -0
- vibetuner/templates/frontend/meta/browserconfig.xml.jinja +10 -0
- vibetuner/templates/frontend/meta/robots.txt.jinja +3 -0
- vibetuner/templates/frontend/meta/site.webmanifest.jinja +7 -0
- vibetuner/templates/frontend/meta/sitemap.xml.jinja +6 -0
- vibetuner/templates/frontend/user/edit.html.jinja +86 -0
- vibetuner/templates/frontend/user/profile.html.jinja +157 -0
- vibetuner/templates/markdown/.placeholder +0 -0
- vibetuner/templates.py +146 -0
- vibetuner/time.py +57 -0
- vibetuner/versioning.py +12 -0
- vibetuner-2.26.6.dist-info/METADATA +241 -0
- vibetuner-2.26.6.dist-info/RECORD +71 -0
- vibetuner-2.26.6.dist-info/WHEEL +4 -0
- vibetuner-2.26.6.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
{% extends "base/skeleton.html.jinja" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}
|
|
4
|
+
{{ _("User Profile") }}
|
|
5
|
+
{% endblock title %}
|
|
6
|
+
{% block body %}
|
|
7
|
+
<div class="min-h-screen bg-gray-50">
|
|
8
|
+
<div class="container mx-auto px-4 py-8">
|
|
9
|
+
<div class="max-w-4xl mx-auto">
|
|
10
|
+
<!-- Page Header -->
|
|
11
|
+
<div class="mb-8">
|
|
12
|
+
<h1 class="text-3xl font-bold text-gray-900">{{ _("User Profile") }}</h1>
|
|
13
|
+
<p class="text-gray-600 mt-2">{{ _("Manage your account information and settings") }}</p>
|
|
14
|
+
</div>
|
|
15
|
+
<!-- Profile Card -->
|
|
16
|
+
<div class="bg-white shadow-lg rounded-lg overflow-hidden">
|
|
17
|
+
<!-- Profile Header -->
|
|
18
|
+
<div class="bg-gradient-to-r from-blue-500 to-indigo-600 px-6 py-8">
|
|
19
|
+
<div class="flex items-center space-x-4">
|
|
20
|
+
<!-- Avatar -->
|
|
21
|
+
{% if user.picture or (user.oauth_accounts and user.oauth_accounts[0].picture) %}
|
|
22
|
+
<img src="{{ user.picture or user.oauth_accounts[0].picture }}"
|
|
23
|
+
alt="{{ user.email }}"
|
|
24
|
+
width="80"
|
|
25
|
+
height="80"
|
|
26
|
+
class="w-20 h-20 rounded-full object-cover border-4 border-white shadow-lg" />
|
|
27
|
+
{% else %}
|
|
28
|
+
<div class="w-20 h-20 bg-white rounded-full flex items-center justify-center text-3xl font-bold text-gray-700 shadow-lg">
|
|
29
|
+
{{ user.email[0].upper() if user.email }}
|
|
30
|
+
</div>
|
|
31
|
+
{% endif %}
|
|
32
|
+
<!-- User Info -->
|
|
33
|
+
<div class="text-white">
|
|
34
|
+
<h2 class="text-2xl font-semibold">{{ user.email }}</h2>
|
|
35
|
+
<p class="text-blue-100">{{ _("Member since") }} {{ user.db_insert_dt | timeago }}</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<!-- Profile Content -->
|
|
40
|
+
<div class="p-6">
|
|
41
|
+
<!-- Account Information Section -->
|
|
42
|
+
<div class="mb-8">
|
|
43
|
+
<h3 class="text-xl font-semibold text-gray-800 mb-4">{{ _("Account Information") }}</h3>
|
|
44
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
45
|
+
<!-- Email -->
|
|
46
|
+
<div>
|
|
47
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">{{ _("Email Address") }}</label>
|
|
48
|
+
<div class="bg-gray-50 px-4 py-3 rounded-md">
|
|
49
|
+
<p class="text-gray-900">{{ user.email }}</p>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<!-- User ID -->
|
|
53
|
+
<div>
|
|
54
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">{{ _("User ID") }}</label>
|
|
55
|
+
<div class="bg-gray-50 px-4 py-3 rounded-md">
|
|
56
|
+
<p class="text-gray-900 font-mono text-sm">{{ user.id }}</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<!-- Created Date -->
|
|
60
|
+
<div>
|
|
61
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">{{ _("Account Created") }}</label>
|
|
62
|
+
<div class="bg-gray-50 px-4 py-3 rounded-md">
|
|
63
|
+
<p class="text-gray-900">{{ user.db_insert_dt | format_datetime }}</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<!-- Last Updated -->
|
|
67
|
+
<div>
|
|
68
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">{{ _("Last Updated") }}</label>
|
|
69
|
+
<div class="bg-gray-50 px-4 py-3 rounded-md">
|
|
70
|
+
<p class="text-gray-900">{{ user.db_update_dt | format_datetime }}</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
<!-- OAuth Accounts Section -->
|
|
76
|
+
{% if user.oauth_accounts %}
|
|
77
|
+
<div class="mb-8">
|
|
78
|
+
<h3 class="text-xl font-semibold text-gray-800 mb-4">{{ _("Connected Accounts") }}</h3>
|
|
79
|
+
<div class="space-y-3">
|
|
80
|
+
{% for account in user.oauth_accounts %}
|
|
81
|
+
<div class="flex items-center justify-between bg-gray-50 px-4 py-3 rounded-md">
|
|
82
|
+
<div class="flex items-center space-x-3">
|
|
83
|
+
{% if account.picture %}
|
|
84
|
+
<img src="{{ account.picture }}"
|
|
85
|
+
alt="{{ account.provider }}"
|
|
86
|
+
width="40"
|
|
87
|
+
height="40"
|
|
88
|
+
class="w-10 h-10 rounded-full object-cover" />
|
|
89
|
+
{% else %}
|
|
90
|
+
<div class="w-10 h-10 bg-gray-200 rounded-full flex items-center justify-center">
|
|
91
|
+
<span class="text-gray-600 font-medium">{{ account.provider[0].upper() }}</span>
|
|
92
|
+
</div>
|
|
93
|
+
{% endif %}
|
|
94
|
+
<div>
|
|
95
|
+
<p class="font-medium text-gray-900">{{ account.provider|title }}</p>
|
|
96
|
+
<p class="text-sm text-gray-600">{{ account.email or _("No email") }}</p>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<span class="text-sm text-green-600 font-medium">{{ _("Connected") }}</span>
|
|
100
|
+
</div>
|
|
101
|
+
{% endfor %}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
{% endif %}
|
|
105
|
+
<!-- Actions -->
|
|
106
|
+
<div class="border-t pt-6">
|
|
107
|
+
<div class="flex flex-col sm:flex-row gap-4">
|
|
108
|
+
<button class="btn btn-primary"
|
|
109
|
+
hx-get="/user/edit"
|
|
110
|
+
hx-target="#main-content"
|
|
111
|
+
hx-swap="innerHTML">{{ _("Edit Profile") }}</button>
|
|
112
|
+
<a href="{{ url_for('auth_logout') }}" class="btn btn-outline btn-error">{{ _("Sign Out") }}</a>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<!-- Additional Settings Card -->
|
|
118
|
+
<div class="mt-6 bg-white shadow-lg rounded-lg p-6">
|
|
119
|
+
<h3 class="text-xl font-semibold text-gray-800 mb-4">{{ _("Account Settings") }}</h3>
|
|
120
|
+
<div class="space-y-4">
|
|
121
|
+
<!-- Language Setting -->
|
|
122
|
+
<div class="flex items-center justify-between">
|
|
123
|
+
<div>
|
|
124
|
+
<p class="font-medium text-gray-900">{{ _("Preferred Language") }}</p>
|
|
125
|
+
<p class="text-sm text-gray-600">{{ _("Your preferred language for the interface") }}</p>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="text-gray-900">
|
|
128
|
+
{% if user.user_settings.language %}
|
|
129
|
+
<span class="badge badge-primary">{{ user.user_settings.language|upper }}</span>
|
|
130
|
+
{% else %}
|
|
131
|
+
<span class="text-gray-500">{{ _("Not set") }}</span>
|
|
132
|
+
{% endif %}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<!-- Additional settings can be added here as the UserSettings model is extended -->
|
|
136
|
+
<!-- Email Notifications -->
|
|
137
|
+
<div class="flex items-center justify-between">
|
|
138
|
+
<div>
|
|
139
|
+
<p class="font-medium text-gray-900">{{ _("Email Notifications") }}</p>
|
|
140
|
+
<p class="text-sm text-gray-600">{{ _("Receive updates about your account") }}</p>
|
|
141
|
+
</div>
|
|
142
|
+
<input type="checkbox" class="toggle toggle-primary" checked />
|
|
143
|
+
</div>
|
|
144
|
+
<!-- Privacy Settings -->
|
|
145
|
+
<div class="flex items-center justify-between">
|
|
146
|
+
<div>
|
|
147
|
+
<p class="font-medium text-gray-900">{{ _("Profile Visibility") }}</p>
|
|
148
|
+
<p class="text-sm text-gray-600">{{ _("Make your profile visible to others") }}</p>
|
|
149
|
+
</div>
|
|
150
|
+
<input type="checkbox" class="toggle toggle-primary" />
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
{% endblock body %}
|
|
File without changes
|
vibetuner/templates.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
from jinja2 import Environment, FileSystemLoader, TemplateNotFound
|
|
5
|
+
|
|
6
|
+
from . import paths
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_base_paths_for_namespace(
|
|
10
|
+
namespace: str | None,
|
|
11
|
+
template_path: Path | list[Path] | None,
|
|
12
|
+
) -> list[Path]:
|
|
13
|
+
"""Get base template paths based on namespace and template_path."""
|
|
14
|
+
if template_path is not None:
|
|
15
|
+
return [template_path] if isinstance(template_path, Path) else template_path
|
|
16
|
+
|
|
17
|
+
# Map known namespaces to their predefined paths
|
|
18
|
+
if namespace == "email":
|
|
19
|
+
return paths.email_templates
|
|
20
|
+
if namespace == "markdown":
|
|
21
|
+
return paths.markdown_templates
|
|
22
|
+
if namespace == "frontend":
|
|
23
|
+
return paths.frontend_templates
|
|
24
|
+
|
|
25
|
+
# Default for unknown or None namespace
|
|
26
|
+
# Only include app_templates if project root has been set
|
|
27
|
+
path_list = []
|
|
28
|
+
if paths.app_templates is not None:
|
|
29
|
+
path_list.append(paths.app_templates)
|
|
30
|
+
path_list.append(paths.core_templates)
|
|
31
|
+
return path_list
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_search_paths(
|
|
35
|
+
base_paths: list[Path],
|
|
36
|
+
namespace: str | None,
|
|
37
|
+
template_path: Path | list[Path] | None,
|
|
38
|
+
) -> list[Path]:
|
|
39
|
+
"""Build list of directories to search for templates."""
|
|
40
|
+
search_paths: list[Path] = []
|
|
41
|
+
known_namespaces = ("email", "markdown", "frontend")
|
|
42
|
+
|
|
43
|
+
for base_path in base_paths:
|
|
44
|
+
# If namespace is known and we're using default paths, they already include it
|
|
45
|
+
if namespace in known_namespaces and template_path is None:
|
|
46
|
+
if base_path.is_dir():
|
|
47
|
+
search_paths.append(base_path)
|
|
48
|
+
elif namespace:
|
|
49
|
+
# Append namespace to path for custom namespaces or explicit paths
|
|
50
|
+
ns_path = base_path / namespace
|
|
51
|
+
if ns_path.is_dir():
|
|
52
|
+
search_paths.append(ns_path)
|
|
53
|
+
else:
|
|
54
|
+
if base_path.is_dir():
|
|
55
|
+
search_paths.append(base_path)
|
|
56
|
+
|
|
57
|
+
return search_paths
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _render_template_with_env(
|
|
61
|
+
env: Environment,
|
|
62
|
+
jinja_template_name: str,
|
|
63
|
+
lang: str | None,
|
|
64
|
+
context: dict[str, Any],
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Render template using Jinja environment with language fallback."""
|
|
67
|
+
# Try language-specific folder first
|
|
68
|
+
if lang:
|
|
69
|
+
try:
|
|
70
|
+
template = env.get_template(f"{lang}/{jinja_template_name}")
|
|
71
|
+
return template.render(**context)
|
|
72
|
+
except TemplateNotFound:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
# Fallback to default folder
|
|
76
|
+
template = env.get_template(f"default/{jinja_template_name}")
|
|
77
|
+
return template.render(**context)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def render_static_template(
|
|
81
|
+
template_name: str,
|
|
82
|
+
*,
|
|
83
|
+
template_path: Path | list[Path] | None = None,
|
|
84
|
+
namespace: Optional[str] = None,
|
|
85
|
+
context: Optional[Dict[str, Any]] = None,
|
|
86
|
+
lang: Optional[str] = None,
|
|
87
|
+
) -> str:
|
|
88
|
+
"""Render a Jinja template with optional i18n and namespace support.
|
|
89
|
+
|
|
90
|
+
This simplified functional helper replaces the old ``TemplateRenderer``
|
|
91
|
+
class while adding **namespace** awareness:
|
|
92
|
+
|
|
93
|
+
1. Optionally switch to *template_path / namespace* if that directory
|
|
94
|
+
exists, letting you segment templates per tenant, brand, or feature
|
|
95
|
+
module without changing call‑sites.
|
|
96
|
+
2. Within the selected base directory attempt ``<lang>/<name>.jinja``
|
|
97
|
+
when *lang* is provided.
|
|
98
|
+
3. Fallback to ``default/<name>.jinja``.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
template_name: Base filename without extension (e.g. ``"invoice"``).
|
|
102
|
+
template_path: Root directory or list of directories containing template
|
|
103
|
+
collections. When a list is provided, searches in order (project templates
|
|
104
|
+
override package templates). Defaults to the package's bundled templates.
|
|
105
|
+
namespace: Optional subfolder under *template_path* to confine the
|
|
106
|
+
lookup. Ignored when the directory does not exist.
|
|
107
|
+
context: Variables passed to the template while rendering.
|
|
108
|
+
lang: Language code such as ``"en"`` or ``"es"`` for localized
|
|
109
|
+
templates.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The rendered template as a string.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
TemplateNotFound: When no suitable template could be located after all
|
|
116
|
+
fallbacks.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
context = context or {}
|
|
120
|
+
|
|
121
|
+
# Determine base paths from namespace and template_path
|
|
122
|
+
base_paths = _get_base_paths_for_namespace(namespace, template_path)
|
|
123
|
+
|
|
124
|
+
# Build search paths from base paths
|
|
125
|
+
search_paths = _build_search_paths(base_paths, namespace, template_path)
|
|
126
|
+
|
|
127
|
+
if not search_paths:
|
|
128
|
+
raise TemplateNotFound(
|
|
129
|
+
f"No valid template paths found for namespace '{namespace}'"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Create Jinja environment with search paths
|
|
133
|
+
env = Environment( # noqa: S701
|
|
134
|
+
loader=FileSystemLoader(search_paths),
|
|
135
|
+
trim_blocks=True,
|
|
136
|
+
lstrip_blocks=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Render template with language fallback
|
|
140
|
+
jinja_template_name = f"{template_name}.jinja"
|
|
141
|
+
try:
|
|
142
|
+
return _render_template_with_env(env, jinja_template_name, lang, context)
|
|
143
|
+
except TemplateNotFound as err:
|
|
144
|
+
raise TemplateNotFound(
|
|
145
|
+
f"Template '{jinja_template_name}' not found under '{search_paths}'."
|
|
146
|
+
) from err
|
vibetuner/time.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from datetime import (
|
|
2
|
+
UTC,
|
|
3
|
+
datetime,
|
|
4
|
+
timedelta,
|
|
5
|
+
)
|
|
6
|
+
from enum import StrEnum, auto
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Unit(StrEnum):
|
|
10
|
+
"""Return units for `.age_in()`."""
|
|
11
|
+
|
|
12
|
+
SECONDS = auto()
|
|
13
|
+
MINUTES = auto()
|
|
14
|
+
HOURS = auto()
|
|
15
|
+
DAYS = auto()
|
|
16
|
+
WEEKS = auto()
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def factor(self) -> int:
|
|
20
|
+
return {
|
|
21
|
+
Unit.SECONDS: 1,
|
|
22
|
+
Unit.MINUTES: 60,
|
|
23
|
+
Unit.HOURS: 3_600,
|
|
24
|
+
Unit.DAYS: 86_400,
|
|
25
|
+
Unit.WEEKS: 604_800,
|
|
26
|
+
}[self]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def now() -> datetime:
|
|
30
|
+
return datetime.now(UTC)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def age_in_days(dt: datetime) -> int:
|
|
34
|
+
# Ensure dt is timezone-aware, if it isn't already
|
|
35
|
+
if dt.tzinfo is None:
|
|
36
|
+
dt = dt.replace(tzinfo=UTC)
|
|
37
|
+
|
|
38
|
+
return int((now() - dt).total_seconds() / 60 / 60 / 24)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def age_in_minutes(dt: datetime) -> int:
|
|
42
|
+
# Ensure dt is timezone-aware, if it isn't already
|
|
43
|
+
if dt.tzinfo is None:
|
|
44
|
+
dt = dt.replace(tzinfo=UTC)
|
|
45
|
+
|
|
46
|
+
return int((now() - dt).total_seconds() / 60)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def age_in_timedelta(dt: datetime) -> timedelta:
|
|
50
|
+
# Ensure dt is timezone-aware, if it isn't already
|
|
51
|
+
if dt.tzinfo is None:
|
|
52
|
+
dt = dt.replace(tzinfo=UTC)
|
|
53
|
+
|
|
54
|
+
return now() - dt
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Custom functions below
|
vibetuner/versioning.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from vibetuner.logging import logger
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__version__ = "0.0.0-default"
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from app._version import version as __version__ # type: ignore
|
|
8
|
+
except (ImportError, ModuleNotFoundError) as e:
|
|
9
|
+
# Log warning for both ImportError and ModuleNotFoundError as requested
|
|
10
|
+
logger.warning(f"Failed to import app._version: {e}. Using default version.")
|
|
11
|
+
|
|
12
|
+
version = __version__
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: vibetuner
|
|
3
|
+
Version: 2.26.6
|
|
4
|
+
Summary: Core Python framework and blessed dependencies for production-ready FastAPI + MongoDB + HTMX projects
|
|
5
|
+
Keywords: fastapi,mongodb,htmx,web-framework,scaffolding,oauth,background-jobs
|
|
6
|
+
Author: All Tuner Labs, S.L.
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Framework :: FastAPI
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
19
|
+
Requires-Dist: aioboto3>=15.5.0
|
|
20
|
+
Requires-Dist: arel>=0.4.0
|
|
21
|
+
Requires-Dist: asyncer>=0.0.10
|
|
22
|
+
Requires-Dist: authlib>=1.6.5
|
|
23
|
+
Requires-Dist: beanie[zstd]>=2.0.0
|
|
24
|
+
Requires-Dist: click>=8.3.1
|
|
25
|
+
Requires-Dist: copier>=9.10.3,<9.10.4
|
|
26
|
+
Requires-Dist: email-validator>=2.3.0
|
|
27
|
+
Requires-Dist: fastapi[standard-no-fastapi-cloud-cli]>=0.121.2
|
|
28
|
+
Requires-Dist: granian[pname]>=2.6.0
|
|
29
|
+
Requires-Dist: httpx[http2]>=0.28.1
|
|
30
|
+
Requires-Dist: itsdangerous>=2.2.0
|
|
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
|
|
34
|
+
Requires-Dist: pydantic-settings>=2.12.0
|
|
35
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
36
|
+
Requires-Dist: redis[hiredis]>=7.0.1
|
|
37
|
+
Requires-Dist: rich>=14.2.0
|
|
38
|
+
Requires-Dist: sse-starlette>=3.0.3
|
|
39
|
+
Requires-Dist: starlette-babel>=1.0.3
|
|
40
|
+
Requires-Dist: starlette-htmx>=0.1.1
|
|
41
|
+
Requires-Dist: streaq[web]<6.0.0
|
|
42
|
+
Requires-Dist: typer-slim[standard]>=0.20.0
|
|
43
|
+
Requires-Dist: babel>=2.17.0 ; extra == 'dev'
|
|
44
|
+
Requires-Dist: cloudflare>=4.3.1 ; extra == 'dev'
|
|
45
|
+
Requires-Dist: djlint>=1.36.4 ; extra == 'dev'
|
|
46
|
+
Requires-Dist: dunamai>=1.25.0 ; extra == 'dev'
|
|
47
|
+
Requires-Dist: gh-bin>=2.83.0 ; extra == 'dev'
|
|
48
|
+
Requires-Dist: granian[pname,reload]>=2.6.0 ; extra == 'dev'
|
|
49
|
+
Requires-Dist: just-bin>=1.43.0 ; extra == 'dev'
|
|
50
|
+
Requires-Dist: pre-commit>=4.4.0 ; extra == 'dev'
|
|
51
|
+
Requires-Dist: pysemver>=0.5.0 ; extra == 'dev'
|
|
52
|
+
Requires-Dist: ruff>=0.14.5 ; extra == 'dev'
|
|
53
|
+
Requires-Dist: rumdl>=0.0.176 ; extra == 'dev'
|
|
54
|
+
Requires-Dist: semver>=3.0.4 ; extra == 'dev'
|
|
55
|
+
Requires-Dist: taplo>=0.9.3 ; extra == 'dev'
|
|
56
|
+
Requires-Dist: ty>=0.0.1a26 ; extra == 'dev'
|
|
57
|
+
Requires-Dist: types-aioboto3[s3,ses]>=15.5.0 ; extra == 'dev'
|
|
58
|
+
Requires-Dist: types-authlib>=1.6.5.20251005 ; extra == 'dev'
|
|
59
|
+
Requires-Dist: types-pyyaml>=6.0.12.20250915 ; extra == 'dev'
|
|
60
|
+
Requires-Dist: uv-bump>=0.3.1 ; extra == 'dev'
|
|
61
|
+
Requires-Python: >=3.11
|
|
62
|
+
Project-URL: Changelog, https://github.com/alltuner/vibetuner/blob/main/CHANGELOG.md
|
|
63
|
+
Project-URL: Documentation, https://vibetuner.alltuner.com/
|
|
64
|
+
Project-URL: Homepage, https://vibetuner.alltuner.com/
|
|
65
|
+
Project-URL: Issues, https://github.com/alltuner/vibetuner/issues
|
|
66
|
+
Project-URL: Repository, https://github.com/alltuner/vibetuner
|
|
67
|
+
Provides-Extra: dev
|
|
68
|
+
Description-Content-Type: text/markdown
|
|
69
|
+
|
|
70
|
+
# vibetuner
|
|
71
|
+
|
|
72
|
+
Core Python framework and blessed dependencies for Vibetuner projects
|
|
73
|
+
|
|
74
|
+
This package provides the complete Python framework and curated dependency set for building modern
|
|
75
|
+
web applications with Vibetuner. It includes everything from FastAPI and MongoDB integration to
|
|
76
|
+
authentication, background jobs, and CLI tools.
|
|
77
|
+
|
|
78
|
+
## What is Vibetuner?
|
|
79
|
+
|
|
80
|
+
Vibetuner is a production-ready scaffolding tool for FastAPI + MongoDB + HTMX web applications.
|
|
81
|
+
This package (`vibetuner`) is the Python component that provides:
|
|
82
|
+
|
|
83
|
+
- Complete web application framework built on FastAPI
|
|
84
|
+
- MongoDB integration with Beanie ODM
|
|
85
|
+
- OAuth and magic link authentication out of the box
|
|
86
|
+
- Background job processing with Redis + Streaq
|
|
87
|
+
- CLI framework with Typer
|
|
88
|
+
- Email services, blob storage, and more
|
|
89
|
+
|
|
90
|
+
**This package is designed to be used within projects generated by the Vibetuner scaffolding
|
|
91
|
+
tool.** For standalone use, you'll need to set up the project structure manually.
|
|
92
|
+
|
|
93
|
+
## Installation
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# In a Vibetuner-generated project (automatic)
|
|
97
|
+
uv sync
|
|
98
|
+
|
|
99
|
+
# Add to an existing project
|
|
100
|
+
uv add vibetuner
|
|
101
|
+
|
|
102
|
+
# With development dependencies
|
|
103
|
+
uv add vibetuner[dev]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Quick Start
|
|
107
|
+
|
|
108
|
+
The recommended way to use Vibetuner is via the scaffolding tool:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Create a new project with all the framework code
|
|
112
|
+
uvx vibetuner scaffold new my-project
|
|
113
|
+
cd my-project
|
|
114
|
+
just dev
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This will generate a complete project with:
|
|
118
|
+
|
|
119
|
+
- Pre-configured FastAPI application
|
|
120
|
+
- Authentication system (OAuth + magic links)
|
|
121
|
+
- MongoDB models and configuration
|
|
122
|
+
- Frontend templates and asset pipeline
|
|
123
|
+
- Docker setup for development and production
|
|
124
|
+
- CLI commands and background job infrastructure
|
|
125
|
+
|
|
126
|
+
## What's Included
|
|
127
|
+
|
|
128
|
+
### Core Framework (`src/vibetuner/`)
|
|
129
|
+
|
|
130
|
+
- **`frontend/`**: FastAPI app, routes, middleware, auth
|
|
131
|
+
- **`models/`**: User, OAuth, email verification, blob storage models
|
|
132
|
+
- **`services/`**: Email (SES), blob storage (S3)
|
|
133
|
+
- **`tasks/`**: Background job infrastructure
|
|
134
|
+
- **`cli/`**: CLI framework with scaffold, run commands
|
|
135
|
+
- **`config.py`**: Pydantic settings management
|
|
136
|
+
- **`mongo.py`**: MongoDB/Beanie setup
|
|
137
|
+
- **`logging.py`**: Structured logging configuration
|
|
138
|
+
|
|
139
|
+
### Blessed Dependencies
|
|
140
|
+
|
|
141
|
+
- **FastAPI** (0.121+): Modern, fast web framework
|
|
142
|
+
- **Beanie**: Async MongoDB ODM with Pydantic
|
|
143
|
+
- **Authlib**: OAuth 1.0/2.0 client
|
|
144
|
+
- **Granian**: High-performance ASGI server
|
|
145
|
+
- **Redis** + **Streaq**: Background task processing
|
|
146
|
+
- **Typer**: CLI framework
|
|
147
|
+
- **Rich**: Beautiful terminal output
|
|
148
|
+
- **Loguru**: Structured logging
|
|
149
|
+
- **Pydantic**: Data validation and settings
|
|
150
|
+
|
|
151
|
+
See [pyproject.toml](./pyproject.toml) for the complete dependency list.
|
|
152
|
+
|
|
153
|
+
## CLI Tools
|
|
154
|
+
|
|
155
|
+
When installed, provides the `vibetuner` command:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
# Create new project from template
|
|
159
|
+
vibetuner scaffold new my-project
|
|
160
|
+
vibetuner scaffold new my-project --defaults
|
|
161
|
+
|
|
162
|
+
# Update existing project
|
|
163
|
+
vibetuner scaffold update
|
|
164
|
+
|
|
165
|
+
# Run development server (in generated projects)
|
|
166
|
+
vibetuner run dev frontend
|
|
167
|
+
vibetuner run dev worker
|
|
168
|
+
|
|
169
|
+
# Run production server (in generated projects)
|
|
170
|
+
vibetuner run prod frontend
|
|
171
|
+
vibetuner run prod worker
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Development Dependencies
|
|
175
|
+
|
|
176
|
+
The `[dev]` extra includes all tools needed for development:
|
|
177
|
+
|
|
178
|
+
- **Ruff**: Fast linting and formatting
|
|
179
|
+
- **Babel**: i18n message extraction
|
|
180
|
+
- **pre-commit**: Git hooks
|
|
181
|
+
- **Type stubs**: For aioboto3, authlib, PyYAML
|
|
182
|
+
- And more...
|
|
183
|
+
|
|
184
|
+
## Usage in Generated Projects
|
|
185
|
+
|
|
186
|
+
In a Vibetuner-generated project, import from `vibetuner`:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
# Use core models
|
|
190
|
+
from vibetuner.models import UserModel, OAuthAccountModel
|
|
191
|
+
|
|
192
|
+
# Use services
|
|
193
|
+
from vibetuner.services.email import send_email
|
|
194
|
+
|
|
195
|
+
# Use configuration
|
|
196
|
+
from vibetuner.config import settings
|
|
197
|
+
|
|
198
|
+
# Extend core routes
|
|
199
|
+
from vibetuner.frontend import app
|
|
200
|
+
|
|
201
|
+
# Add your routes
|
|
202
|
+
@app.get("/api/hello")
|
|
203
|
+
async def hello():
|
|
204
|
+
return {"message": "Hello World"}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Documentation
|
|
208
|
+
|
|
209
|
+
For complete documentation, guides, and examples, see the main Vibetuner repository:
|
|
210
|
+
|
|
211
|
+
**📖 [Vibetuner Documentation](https://vibetuner.alltuner.com/)**
|
|
212
|
+
|
|
213
|
+
## Package Ecosystem
|
|
214
|
+
|
|
215
|
+
Vibetuner consists of three packages that work together:
|
|
216
|
+
|
|
217
|
+
1. **vibetuner** (this package): Python framework and dependencies
|
|
218
|
+
2. **[@alltuner/vibetuner](https://www.npmjs.com/package/@alltuner/vibetuner)**: JavaScript/CSS build dependencies
|
|
219
|
+
3. **Scaffolding template**: Copier template for project generation
|
|
220
|
+
|
|
221
|
+
All three are version-locked and tested together to ensure compatibility.
|
|
222
|
+
|
|
223
|
+
## Contributing
|
|
224
|
+
|
|
225
|
+
Contributions welcome! See the main repository for contribution guidelines:
|
|
226
|
+
|
|
227
|
+
**🤝 [Contributing to Vibetuner](https://github.com/alltuner/vibetuner/blob/main/CONTRIBUTING.md)**
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT License - Copyright (c) 2025 All Tuner Labs, S.L.
|
|
232
|
+
|
|
233
|
+
See [LICENSE](https://github.com/alltuner/vibetuner/blob/main/LICENSE) for details.
|
|
234
|
+
|
|
235
|
+
## Links
|
|
236
|
+
|
|
237
|
+
- **Main Repository**: <https://github.com/alltuner/vibetuner>
|
|
238
|
+
- **Documentation**: <https://vibetuner.alltuner.com/>
|
|
239
|
+
- **Issues**: <https://github.com/alltuner/vibetuner/issues>
|
|
240
|
+
- **PyPI**: <https://pypi.org/project/vibetuner/>
|
|
241
|
+
- **npm Package**: <https://www.npmjs.com/package/@alltuner/vibetuner>
|