vibetuner 2.6.1__py3-none-any.whl → 2.7.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 (85) hide show
  1. vibetuner/__init__.py +2 -0
  2. vibetuner/__main__.py +4 -0
  3. vibetuner/cli/__init__.py +68 -0
  4. vibetuner/cli/run.py +161 -0
  5. vibetuner/config.py +128 -0
  6. vibetuner/context.py +25 -0
  7. vibetuner/frontend/AGENTS.md +113 -0
  8. vibetuner/frontend/CLAUDE.md +113 -0
  9. vibetuner/frontend/__init__.py +94 -0
  10. vibetuner/frontend/context.py +10 -0
  11. vibetuner/frontend/deps.py +41 -0
  12. vibetuner/frontend/email.py +45 -0
  13. vibetuner/frontend/hotreload.py +13 -0
  14. vibetuner/frontend/lifespan.py +26 -0
  15. vibetuner/frontend/middleware.py +151 -0
  16. vibetuner/frontend/oauth.py +196 -0
  17. vibetuner/frontend/routes/__init__.py +12 -0
  18. vibetuner/frontend/routes/auth.py +150 -0
  19. vibetuner/frontend/routes/debug.py +414 -0
  20. vibetuner/frontend/routes/health.py +33 -0
  21. vibetuner/frontend/routes/language.py +43 -0
  22. vibetuner/frontend/routes/meta.py +55 -0
  23. vibetuner/frontend/routes/user.py +94 -0
  24. vibetuner/frontend/templates.py +176 -0
  25. vibetuner/logging.py +87 -0
  26. vibetuner/models/AGENTS.md +165 -0
  27. vibetuner/models/CLAUDE.md +165 -0
  28. vibetuner/models/__init__.py +14 -0
  29. vibetuner/models/blob.py +89 -0
  30. vibetuner/models/email_verification.py +84 -0
  31. vibetuner/models/mixins.py +76 -0
  32. vibetuner/models/oauth.py +57 -0
  33. vibetuner/models/registry.py +15 -0
  34. vibetuner/models/types.py +16 -0
  35. vibetuner/models/user.py +91 -0
  36. vibetuner/mongo.py +18 -0
  37. vibetuner/paths.py +112 -0
  38. vibetuner/services/AGENTS.md +104 -0
  39. vibetuner/services/CLAUDE.md +104 -0
  40. vibetuner/services/__init__.py +0 -0
  41. vibetuner/services/blob.py +175 -0
  42. vibetuner/services/email.py +50 -0
  43. vibetuner/tasks/AGENTS.md +98 -0
  44. vibetuner/tasks/CLAUDE.md +98 -0
  45. vibetuner/tasks/__init__.py +2 -0
  46. vibetuner/tasks/context.py +34 -0
  47. vibetuner/tasks/worker.py +18 -0
  48. vibetuner/templates/email/AGENTS.md +48 -0
  49. vibetuner/templates/email/CLAUDE.md +48 -0
  50. vibetuner/templates/email/default/magic_link.html.jinja +16 -0
  51. vibetuner/templates/email/default/magic_link.txt.jinja +5 -0
  52. vibetuner/templates/frontend/AGENTS.md +74 -0
  53. vibetuner/templates/frontend/CLAUDE.md +74 -0
  54. vibetuner/templates/frontend/base/favicons.html.jinja +1 -0
  55. vibetuner/templates/frontend/base/footer.html.jinja +3 -0
  56. vibetuner/templates/frontend/base/header.html.jinja +0 -0
  57. vibetuner/templates/frontend/base/opengraph.html.jinja +7 -0
  58. vibetuner/templates/frontend/base/skeleton.html.jinja +42 -0
  59. vibetuner/templates/frontend/debug/collections.html.jinja +103 -0
  60. vibetuner/templates/frontend/debug/components/debug_nav.html.jinja +55 -0
  61. vibetuner/templates/frontend/debug/index.html.jinja +83 -0
  62. vibetuner/templates/frontend/debug/info.html.jinja +256 -0
  63. vibetuner/templates/frontend/debug/users.html.jinja +137 -0
  64. vibetuner/templates/frontend/debug/version.html.jinja +53 -0
  65. vibetuner/templates/frontend/email/magic_link.txt.jinja +5 -0
  66. vibetuner/templates/frontend/email_sent.html.jinja +82 -0
  67. vibetuner/templates/frontend/index.html.jinja +19 -0
  68. vibetuner/templates/frontend/lang/select.html.jinja +4 -0
  69. vibetuner/templates/frontend/login.html.jinja +84 -0
  70. vibetuner/templates/frontend/meta/browserconfig.xml.jinja +10 -0
  71. vibetuner/templates/frontend/meta/robots.txt.jinja +3 -0
  72. vibetuner/templates/frontend/meta/site.webmanifest.jinja +7 -0
  73. vibetuner/templates/frontend/meta/sitemap.xml.jinja +6 -0
  74. vibetuner/templates/frontend/user/edit.html.jinja +85 -0
  75. vibetuner/templates/frontend/user/profile.html.jinja +156 -0
  76. vibetuner/templates/markdown/.placeholder +0 -0
  77. vibetuner/templates/markdown/AGENTS.md +29 -0
  78. vibetuner/templates/markdown/CLAUDE.md +29 -0
  79. vibetuner/templates.py +152 -0
  80. vibetuner/time.py +57 -0
  81. vibetuner/versioning.py +8 -0
  82. {vibetuner-2.6.1.dist-info → vibetuner-2.7.1.dist-info}/METADATA +2 -1
  83. vibetuner-2.7.1.dist-info/RECORD +84 -0
  84. vibetuner-2.6.1.dist-info/RECORD +0 -4
  85. {vibetuner-2.6.1.dist-info → vibetuner-2.7.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,94 @@
1
+ from fastapi import APIRouter, Form, HTTPException, Request
2
+ from fastapi.responses import HTMLResponse, RedirectResponse
3
+ from pydantic_extra_types.language_code import LanguageAlpha2
4
+ from starlette.authentication import requires
5
+
6
+ from vibetuner.models import UserModel
7
+
8
+ from ..context import ctx
9
+ from ..templates import render_template
10
+
11
+
12
+ router = APIRouter(prefix="/user")
13
+
14
+
15
+ @router.get("/")
16
+ @requires("authenticated", redirect="auth_login")
17
+ async def user_profile(request: Request) -> HTMLResponse:
18
+ """User profile endpoint."""
19
+ user = await UserModel.get(request.user.id)
20
+ if not user:
21
+ raise HTTPException(
22
+ status_code=404,
23
+ detail="User not found",
24
+ )
25
+
26
+ await user.fetch_link("oauth_accounts")
27
+ return render_template(
28
+ "user/profile.html.jinja",
29
+ request,
30
+ {"user": user},
31
+ )
32
+
33
+
34
+ @router.get("/edit")
35
+ @requires("authenticated", redirect="auth_login")
36
+ async def user_edit_form(request: Request) -> HTMLResponse:
37
+ """User profile edit form."""
38
+ user = await UserModel.get(request.user.id)
39
+ if not user:
40
+ raise HTTPException(status_code=404, detail="User not found")
41
+
42
+ # Get available languages for the dropdown
43
+ from babel import Locale
44
+
45
+ locale_names = dict(
46
+ sorted(
47
+ {
48
+ locale: (Locale.parse(locale).display_name or locale).capitalize()
49
+ for locale in ctx.supported_languages
50
+ }.items(),
51
+ key=lambda x: x[1],
52
+ ),
53
+ )
54
+
55
+ return render_template(
56
+ "user/edit.html.jinja",
57
+ request,
58
+ {
59
+ "user": user,
60
+ "locale_names": locale_names,
61
+ "current_language": user.user_settings.language,
62
+ },
63
+ )
64
+
65
+
66
+ @router.post("/edit")
67
+ @requires("authenticated", redirect="auth_login")
68
+ async def user_edit_submit(
69
+ request: Request,
70
+ name: str = Form(...),
71
+ language: str = Form(None),
72
+ ) -> RedirectResponse:
73
+ """Handle user profile edit form submission."""
74
+ user = await UserModel.get(request.user.id)
75
+ if not user:
76
+ raise HTTPException(status_code=404, detail="User not found")
77
+
78
+ # Update user fields
79
+ user.name = name
80
+
81
+ # Update language preference if provided
82
+ if language and language in ctx.supported_languages:
83
+ try:
84
+ user.user_settings.language = LanguageAlpha2(language)
85
+ except ValueError:
86
+ pass # Invalid language code, skip update
87
+
88
+ # Save user
89
+ await user.save()
90
+
91
+ # Update session with new data to avoid DB query on next request
92
+ request.session["user"] = user.session_dict
93
+
94
+ return RedirectResponse(url="/user/", status_code=302)
@@ -0,0 +1,176 @@
1
+ from datetime import timedelta
2
+ from typing import Any
3
+
4
+ from fastapi import Request
5
+ from fastapi.templating import Jinja2Templates
6
+ from starlette.responses import HTMLResponse
7
+ from starlette_babel import gettext_lazy as _, gettext_lazy as ngettext
8
+ from starlette_babel.contrib.jinja import configure_jinja_env
9
+
10
+ from vibetuner.context import Context
11
+ from vibetuner.paths import frontend_templates
12
+ from vibetuner.templates import render_static_template
13
+ from vibetuner.time import age_in_timedelta
14
+
15
+ from .hotreload import hotreload
16
+
17
+
18
+ __all__ = [
19
+ "render_static_template",
20
+ ]
21
+
22
+ data_ctx = Context()
23
+
24
+
25
+ def timeago(dt):
26
+ """Converts a datetime object to a human-readable string representing the time elapsed since the given datetime.
27
+
28
+ Args:
29
+ dt (datetime): The datetime object to convert.
30
+
31
+ Returns:
32
+ str: A human-readable string representing the time elapsed since the given datetime,
33
+ such as "X seconds ago", "X minutes ago", "X hours ago", "yesterday", "X days ago",
34
+ "X months ago", or "X years ago". If the datetime is more than 4 years old,
35
+ it returns the date in the format "MMM DD, YYYY".
36
+
37
+ """
38
+ try:
39
+ diff = age_in_timedelta(dt)
40
+
41
+ if diff < timedelta(seconds=60):
42
+ seconds = diff.seconds
43
+ return ngettext(
44
+ "%(seconds)d second ago",
45
+ "%(seconds)d seconds ago",
46
+ seconds,
47
+ ) % {"seconds": seconds}
48
+ if diff < timedelta(minutes=60):
49
+ minutes = diff.seconds // 60
50
+ return ngettext(
51
+ "%(minutes)d minute ago",
52
+ "%(minutes)d minutes ago",
53
+ minutes,
54
+ ) % {"minutes": minutes}
55
+ if diff < timedelta(days=1):
56
+ hours = diff.seconds // 3600
57
+ return ngettext("%(hours)d hour ago", "%(hours)d hours ago", hours) % {
58
+ "hours": hours,
59
+ }
60
+ if diff < timedelta(days=2):
61
+ return _("yesterday")
62
+ if diff < timedelta(days=65):
63
+ days = diff.days
64
+ return ngettext("%(days)d day ago", "%(days)d days ago", days) % {
65
+ "days": days,
66
+ }
67
+ if diff < timedelta(days=365):
68
+ months = diff.days // 30
69
+ return ngettext("%(months)d month ago", "%(months)d months ago", months) % {
70
+ "months": months,
71
+ }
72
+ if diff < timedelta(days=365 * 4):
73
+ years = diff.days // 365
74
+ return ngettext("%(years)d year ago", "%(years)d years ago", years) % {
75
+ "years": years,
76
+ }
77
+ return dt.strftime("%b %d, %Y")
78
+ except Exception:
79
+ return ""
80
+
81
+
82
+ def format_date(dt):
83
+ """Formats a datetime object to display only the date.
84
+
85
+ Args:
86
+ dt (datetime): The datetime object to format.
87
+
88
+ Returns:
89
+ str: A formatted date string in the format "Month DD, YYYY" (e.g., "January 15, 2024").
90
+ Returns empty string if dt is None.
91
+ """
92
+ if dt is None:
93
+ return ""
94
+ try:
95
+ return dt.strftime("%B %d, %Y")
96
+ except Exception:
97
+ return ""
98
+
99
+
100
+ def format_datetime(dt):
101
+ """Formats a datetime object to display date and time without seconds.
102
+
103
+ Args:
104
+ dt (datetime): The datetime object to format.
105
+
106
+ Returns:
107
+ str: A formatted datetime string in the format "Month DD, YYYY at HH:MM AM/PM"
108
+ (e.g., "January 15, 2024 at 3:45 PM"). Returns empty string if dt is None.
109
+ """
110
+ if dt is None:
111
+ return ""
112
+ try:
113
+ return dt.strftime("%B %d, %Y at %I:%M %p")
114
+ except Exception:
115
+ return ""
116
+
117
+
118
+ # Add your functions here
119
+ def format_duration(seconds):
120
+ """Formats duration in seconds to user-friendly format with rounding.
121
+
122
+ Args:
123
+ seconds (float): Duration in seconds.
124
+
125
+ Returns:
126
+ str: For 0-45 seconds, shows "x sec" (e.g., "30 sec").
127
+ For 46 seconds to 1:45, shows "1 min".
128
+ For 1:46 to 2:45, shows "2 min", etc.
129
+ Returns empty string if seconds is None or invalid.
130
+ """
131
+ if seconds is None:
132
+ return ""
133
+ try:
134
+ total_seconds = int(float(seconds))
135
+
136
+ if total_seconds <= 45:
137
+ return f"{total_seconds} sec"
138
+ else:
139
+ # Round to nearest minute for times > 45 seconds
140
+ # 46-105 seconds = 1 min, 106-165 seconds = 2 min, etc.
141
+ minutes = round(total_seconds / 60)
142
+ return f"{minutes} min"
143
+ except (ValueError, TypeError):
144
+ return ""
145
+
146
+
147
+ templates: Jinja2Templates = Jinja2Templates(directory=frontend_templates)
148
+ jinja_env = templates.env
149
+
150
+
151
+ def render_template(
152
+ template: str,
153
+ request: Request,
154
+ ctx: dict[str, Any] | None = None,
155
+ **kwargs: Any,
156
+ ) -> HTMLResponse:
157
+ ctx = ctx or {}
158
+ merged_ctx = {**data_ctx.model_dump(), "request": request, **ctx}
159
+
160
+ return templates.TemplateResponse(template, merged_ctx, **kwargs)
161
+
162
+
163
+ # Global Vars
164
+ jinja_env.globals.update({"DEBUG": data_ctx.DEBUG})
165
+ jinja_env.globals.update({"hotreload": hotreload})
166
+
167
+ # Date Filters
168
+ jinja_env.filters["timeago"] = timeago
169
+ jinja_env.filters["format_date"] = format_date
170
+ jinja_env.filters["format_datetime"] = format_datetime
171
+
172
+ # Duration Filters
173
+ jinja_env.filters["format_duration"] = format_duration
174
+ jinja_env.filters["duration"] = format_duration
175
+
176
+ configure_jinja_env(jinja_env)
vibetuner/logging.py ADDED
@@ -0,0 +1,87 @@
1
+ import logging
2
+ import sys
3
+ from enum import Enum
4
+ from pathlib import Path
5
+
6
+ from loguru import logger
7
+
8
+
9
+ class LogLevel(str, Enum):
10
+ DEBUG = "DEBUG"
11
+ INFO = "INFO"
12
+ WARNING = "WARNING"
13
+ ERROR = "ERROR"
14
+ CRITICAL = "CRITICAL"
15
+
16
+
17
+ class InterceptHandler(logging.Handler):
18
+ def emit(self, record):
19
+ # Get corresponding Loguru level if it exists
20
+ try:
21
+ level = logger.level(record.levelname).name
22
+ except ValueError:
23
+ level = record.levelno
24
+
25
+ # Find caller from where originated the logged message
26
+ frame, depth = logging.currentframe(), 2
27
+ while frame is not None and frame.f_code.co_filename == logging.__file__:
28
+ frame = frame.f_back
29
+ depth += 1
30
+
31
+ # Format the message with module information inline
32
+ logger.opt(depth=depth, exception=record.exc_info).log(
33
+ level,
34
+ f"[{record.name}] {record.getMessage()}",
35
+ )
36
+
37
+
38
+ def setup_logging(
39
+ level: str | int | None = LogLevel.INFO,
40
+ log_file: str | Path | None = None,
41
+ rotation: str = "10 MB",
42
+ retention: str = "1 week",
43
+ ) -> None:
44
+ """Configure logging for the application.
45
+
46
+ Args:
47
+ level: Minimum logging level threshold
48
+ log_file: Optional path to log file. If None, logs only to stderr
49
+ rotation: When to rotate the log file
50
+ retention: How long to keep log files
51
+
52
+ """
53
+ # Remove default handler
54
+ logger.remove()
55
+
56
+ if level is None:
57
+ level = LogLevel.INFO
58
+
59
+ # Define format for the logs
60
+ format_string = (
61
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
62
+ "<level>{level: <8}</level> | "
63
+ "<cyan>{name}</cyan>:<cyan>{line}</cyan> - "
64
+ "<level>{message}</level>"
65
+ )
66
+
67
+ # Add stderr handler with custom format
68
+ logger.add(sys.stderr, level=level, format=format_string, colorize=True)
69
+
70
+ # Add file handler if log_file is specified
71
+ if log_file:
72
+ logger.add(
73
+ str(log_file),
74
+ level=level,
75
+ format=format_string,
76
+ rotation=rotation,
77
+ retention=retention,
78
+ compression="zip",
79
+ )
80
+
81
+ # Intercept standard logging
82
+ logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
83
+
84
+ # Optional: Capture warnings from warnings module
85
+ logging.captureWarnings(True)
86
+
87
+ logger.info("Logging configured successfully")
@@ -0,0 +1,165 @@
1
+ # Core Models Module
2
+
3
+ **IMMUTABLE SCAFFOLDING CODE** - These are the framework's core models that provide essential functionality.
4
+
5
+ ## What's Here
6
+
7
+ This module contains the scaffolding's core models:
8
+
9
+ - **UserModel** - Base user model with authentication support
10
+ - **OAuthAccountModel** - OAuth provider account linking
11
+ - **EmailVerificationTokenModel** - Magic link authentication tokens
12
+ - **BlobModel** - File storage and blob management
13
+ - **Mixins** - Reusable model behaviors (TimeStampMixin, etc.)
14
+ - **Types** - Common field types and validators
15
+
16
+ ## Important Rules
17
+
18
+ ⚠️ **DO NOT MODIFY** these core models directly.
19
+
20
+ **For changes to core models:**
21
+
22
+ - File an issue at `https://github.com/alltuner/scaffolding`
23
+ - Core changes benefit all projects using the scaffolding
24
+
25
+ **For your application models:**
26
+
27
+ - Create them in `src/app/models/` instead
28
+ - Import core models when needed: `from vibetuner.models import UserModel`
29
+ - Use mixins from here: `from vibetuner.models.mixins import TimeStampMixin`
30
+
31
+ ## User Model Pattern (for reference)
32
+
33
+ Your application models in `src/app/models/` should follow this pattern:
34
+
35
+ ```python
36
+ from beanie import Document
37
+ from pydantic import Field
38
+ from vibetuner.models.mixins import TimeStampMixin
39
+
40
+ class Product(Document, TimeStampMixin):
41
+ name: str
42
+ price: float = Field(gt=0)
43
+ stock: int = Field(ge=0)
44
+
45
+ class Settings:
46
+ name = "products"
47
+ indexes = ["name"]
48
+ ```
49
+
50
+ ## Available Mixins
51
+
52
+ ### TimeStampMixin
53
+
54
+ Automatic timestamps for all models:
55
+
56
+ - `db_insert_dt` - Created at (UTC)
57
+ - `db_update_dt` - Updated at (UTC)
58
+ - Methods: `age()`, `age_in()`, `is_older_than()`
59
+
60
+ Import in your app models:
61
+
62
+ ```python
63
+ from vibetuner.models.mixins import TimeStampMixin
64
+ ```
65
+
66
+ ## Queries
67
+
68
+ ### Finding Documents
69
+
70
+ ```python
71
+ from beanie.operators import Eq, In, Gt, Lt
72
+
73
+ # By ID (preferred method)
74
+ product = await Product.get(product_id)
75
+
76
+ # By field (use Beanie operators)
77
+ product = await Product.find_one(Eq(Product.name, "Widget"))
78
+ products = await Product.find(Lt(Product.price, 100)).to_list()
79
+
80
+ # Multiple conditions
81
+ results = await Product.find(
82
+ Eq(Product.category, "electronics"),
83
+ Gt(Product.price, 50)
84
+ ).to_list()
85
+
86
+ # With In operator
87
+ products = await Product.find(
88
+ In(Product.category, ["electronics", "gadgets"])
89
+ ).to_list()
90
+ ```
91
+
92
+ ### Save/Delete
93
+
94
+ ```python
95
+ # Create
96
+ product = Product(name="Widget", price=9.99, stock=100)
97
+ await product.insert()
98
+
99
+ # Update
100
+ product.price = 19.99
101
+ await product.save()
102
+
103
+ # Delete
104
+ await product.delete()
105
+ ```
106
+
107
+ ### Aggregation
108
+
109
+ ```python
110
+ results = await Product.aggregate([
111
+ {"$match": {"price": {"$gt": 50}}},
112
+ {"$group": {"_id": "$category", "total": {"$sum": 1}}}
113
+ ]).to_list()
114
+ ```
115
+
116
+ ## Indexes
117
+
118
+ ```python
119
+ from pymongo import IndexModel, TEXT
120
+
121
+ class Settings:
122
+ indexes = [
123
+ "field_name", # Simple index
124
+ [("field1", 1), ("field2", -1)], # Compound index
125
+ IndexModel([("text_field", TEXT)]) # Text search
126
+ ]
127
+ ```
128
+
129
+ ## Relationships
130
+
131
+ ```python
132
+ from beanie import Link
133
+
134
+ class Order(Document):
135
+ user: Link[User]
136
+ products: list[Link[Product]]
137
+
138
+ # Fetch with relations
139
+ order = await Order.get(order_id, fetch_links=True)
140
+ print(order.user.email) # Automatically loaded
141
+ ```
142
+
143
+ ## Extending Core Models
144
+
145
+ If you need to add fields to User or other core models:
146
+
147
+ 1. **Option A**: File an issue at `https://github.com/alltuner/scaffolding` for widely useful fields
148
+ 2. **Option B**: Create a related model in `src/app/models/` that links to the core model:
149
+
150
+ ```python
151
+ from beanie import Document, Link
152
+ from vibetuner.models import UserModel
153
+
154
+ class UserProfile(Document):
155
+ user: Link[UserModel]
156
+ bio: str
157
+ avatar_url: str
158
+
159
+ class Settings:
160
+ name = "user_profiles"
161
+ ```
162
+
163
+ ## MongoDB MCP
164
+
165
+ Claude Code has MongoDB MCP access for database operations, queries, and debugging.
@@ -0,0 +1,165 @@
1
+ # Core Models Module
2
+
3
+ **IMMUTABLE SCAFFOLDING CODE** - These are the framework's core models that provide essential functionality.
4
+
5
+ ## What's Here
6
+
7
+ This module contains the scaffolding's core models:
8
+
9
+ - **UserModel** - Base user model with authentication support
10
+ - **OAuthAccountModel** - OAuth provider account linking
11
+ - **EmailVerificationTokenModel** - Magic link authentication tokens
12
+ - **BlobModel** - File storage and blob management
13
+ - **Mixins** - Reusable model behaviors (TimeStampMixin, etc.)
14
+ - **Types** - Common field types and validators
15
+
16
+ ## Important Rules
17
+
18
+ ⚠️ **DO NOT MODIFY** these core models directly.
19
+
20
+ **For changes to core models:**
21
+
22
+ - File an issue at `https://github.com/alltuner/scaffolding`
23
+ - Core changes benefit all projects using the scaffolding
24
+
25
+ **For your application models:**
26
+
27
+ - Create them in `src/app/models/` instead
28
+ - Import core models when needed: `from vibetuner.models import UserModel`
29
+ - Use mixins from here: `from vibetuner.models.mixins import TimeStampMixin`
30
+
31
+ ## User Model Pattern (for reference)
32
+
33
+ Your application models in `src/app/models/` should follow this pattern:
34
+
35
+ ```python
36
+ from beanie import Document
37
+ from pydantic import Field
38
+ from vibetuner.models.mixins import TimeStampMixin
39
+
40
+ class Product(Document, TimeStampMixin):
41
+ name: str
42
+ price: float = Field(gt=0)
43
+ stock: int = Field(ge=0)
44
+
45
+ class Settings:
46
+ name = "products"
47
+ indexes = ["name"]
48
+ ```
49
+
50
+ ## Available Mixins
51
+
52
+ ### TimeStampMixin
53
+
54
+ Automatic timestamps for all models:
55
+
56
+ - `db_insert_dt` - Created at (UTC)
57
+ - `db_update_dt` - Updated at (UTC)
58
+ - Methods: `age()`, `age_in()`, `is_older_than()`
59
+
60
+ Import in your app models:
61
+
62
+ ```python
63
+ from vibetuner.models.mixins import TimeStampMixin
64
+ ```
65
+
66
+ ## Queries
67
+
68
+ ### Finding Documents
69
+
70
+ ```python
71
+ from beanie.operators import Eq, In, Gt, Lt
72
+
73
+ # By ID (preferred method)
74
+ product = await Product.get(product_id)
75
+
76
+ # By field (use Beanie operators)
77
+ product = await Product.find_one(Eq(Product.name, "Widget"))
78
+ products = await Product.find(Lt(Product.price, 100)).to_list()
79
+
80
+ # Multiple conditions
81
+ results = await Product.find(
82
+ Eq(Product.category, "electronics"),
83
+ Gt(Product.price, 50)
84
+ ).to_list()
85
+
86
+ # With In operator
87
+ products = await Product.find(
88
+ In(Product.category, ["electronics", "gadgets"])
89
+ ).to_list()
90
+ ```
91
+
92
+ ### Save/Delete
93
+
94
+ ```python
95
+ # Create
96
+ product = Product(name="Widget", price=9.99, stock=100)
97
+ await product.insert()
98
+
99
+ # Update
100
+ product.price = 19.99
101
+ await product.save()
102
+
103
+ # Delete
104
+ await product.delete()
105
+ ```
106
+
107
+ ### Aggregation
108
+
109
+ ```python
110
+ results = await Product.aggregate([
111
+ {"$match": {"price": {"$gt": 50}}},
112
+ {"$group": {"_id": "$category", "total": {"$sum": 1}}}
113
+ ]).to_list()
114
+ ```
115
+
116
+ ## Indexes
117
+
118
+ ```python
119
+ from pymongo import IndexModel, TEXT
120
+
121
+ class Settings:
122
+ indexes = [
123
+ "field_name", # Simple index
124
+ [("field1", 1), ("field2", -1)], # Compound index
125
+ IndexModel([("text_field", TEXT)]) # Text search
126
+ ]
127
+ ```
128
+
129
+ ## Relationships
130
+
131
+ ```python
132
+ from beanie import Link
133
+
134
+ class Order(Document):
135
+ user: Link[User]
136
+ products: list[Link[Product]]
137
+
138
+ # Fetch with relations
139
+ order = await Order.get(order_id, fetch_links=True)
140
+ print(order.user.email) # Automatically loaded
141
+ ```
142
+
143
+ ## Extending Core Models
144
+
145
+ If you need to add fields to User or other core models:
146
+
147
+ 1. **Option A**: File an issue at `https://github.com/alltuner/scaffolding` for widely useful fields
148
+ 2. **Option B**: Create a related model in `src/app/models/` that links to the core model:
149
+
150
+ ```python
151
+ from beanie import Document, Link
152
+ from vibetuner.models import UserModel
153
+
154
+ class UserProfile(Document):
155
+ user: Link[UserModel]
156
+ bio: str
157
+ avatar_url: str
158
+
159
+ class Settings:
160
+ name = "user_profiles"
161
+ ```
162
+
163
+ ## MongoDB MCP
164
+
165
+ Claude Code has MongoDB MCP access for database operations, queries, and debugging.
@@ -0,0 +1,14 @@
1
+ from beanie import Document, View
2
+
3
+ from .blob import BlobModel
4
+ from .email_verification import EmailVerificationTokenModel
5
+ from .oauth import OAuthAccountModel
6
+ from .user import UserModel
7
+
8
+
9
+ __all__: list[type[Document] | type[View]] = [
10
+ BlobModel,
11
+ EmailVerificationTokenModel,
12
+ OAuthAccountModel,
13
+ UserModel,
14
+ ]