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
@@ -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
@@ -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>