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,104 @@
1
+ # Core Services Module
2
+
3
+ **IMMUTABLE SCAFFOLDING CODE** - These are the framework's core services that provide essential functionality.
4
+
5
+ ## What's Here
6
+
7
+ This module contains the scaffolding's core services:
8
+
9
+ - **email.py** - Email sending via AWS SES
10
+ - **blob.py** - File storage and blob management
11
+
12
+ ## Important Rules
13
+
14
+ ⚠️ **DO NOT MODIFY** these core services directly.
15
+
16
+ **For changes to core services:**
17
+
18
+ - File an issue at `https://github.com/alltuner/scaffolding`
19
+ - Core changes benefit all projects using the scaffolding
20
+
21
+ **For your application services:**
22
+
23
+ - Create them in `src/app/services/` instead
24
+ - Import core services when needed: `from vibetuner.services.email import send_email`
25
+
26
+ ## User Service Pattern (for reference)
27
+
28
+ Your application services in `src/app/services/` should follow this pattern:
29
+
30
+ ```python
31
+ from vibetuner.models import UserModel
32
+
33
+ class NotificationService:
34
+ async def send_notification(
35
+ self,
36
+ user: UserModel,
37
+ message: str,
38
+ priority: str = "normal"
39
+ ) -> bool:
40
+ # Implementation
41
+ return True
42
+
43
+ # Singleton
44
+ notification_service = NotificationService()
45
+ ```
46
+
47
+ ## Using Core Services
48
+
49
+ ### Email Service
50
+
51
+ ```python
52
+ from vibetuner.services.email import send_email
53
+
54
+ await send_email(
55
+ to_email="user@example.com",
56
+ subject="Welcome",
57
+ html_content="<h1>Welcome!</h1>",
58
+ text_content="Welcome!"
59
+ )
60
+ ```
61
+
62
+ ### Blob Service
63
+
64
+ ```python
65
+ from vibetuner.services.blob import blob_service
66
+
67
+ # Upload file
68
+ blob = await blob_service.upload(file_data, "image.png")
69
+
70
+ # Get file URL
71
+ url = await blob_service.get_url(blob.id)
72
+ ```
73
+
74
+ ## Creating Your Own Services
75
+
76
+ Place your application services in `src/app/services/`:
77
+
78
+ ```python
79
+ # src/app/services/external_api.py
80
+ import httpx
81
+
82
+ async def call_api(api_url: str, api_key: str, data: dict) -> dict:
83
+ async with httpx.AsyncClient() as client:
84
+ response = await client.post(
85
+ api_url,
86
+ json=data,
87
+ headers={"Authorization": f"Bearer {api_key}"}
88
+ )
89
+ response.raise_for_status()
90
+ return response.json()
91
+ ```
92
+
93
+ ## Dependency Injection
94
+
95
+ ```python
96
+ from fastapi import Depends
97
+
98
+ @router.post("/notify")
99
+ async def notify(
100
+ message: str,
101
+ service=Depends(lambda: notification_service)
102
+ ):
103
+ await service.send_notification(user, message)
104
+ ```
File without changes
@@ -0,0 +1,175 @@
1
+ """Blob storage service for file uploads to S3 or R2.
2
+
3
+ WARNING: This is a scaffolding-managed file. DO NOT MODIFY directly.
4
+ To extend blob functionality, create wrapper services in the parent services directory.
5
+ """
6
+
7
+ import mimetypes
8
+ from pathlib import Path
9
+ from typing import Literal
10
+
11
+ import aioboto3
12
+ from aiobotocore.config import AioConfig
13
+
14
+ from vibetuner.config import settings
15
+ from vibetuner.models import BlobModel
16
+ from vibetuner.models.blob import BlobStatus
17
+
18
+
19
+ S3_SERVICE_NAME: Literal["s3"] = "s3"
20
+ DEFAULT_CONTENT_TYPE: str = "application/octet-stream"
21
+
22
+
23
+ class BlobService:
24
+ def __init__(
25
+ self,
26
+ session: aioboto3.Session | None = None,
27
+ default_bucket: str | None = None,
28
+ ) -> None:
29
+ if (
30
+ settings.r2_bucket_endpoint_url is None
31
+ or settings.r2_access_key is None
32
+ or settings.r2_secret_key is None
33
+ ):
34
+ raise ValueError(
35
+ "R2 bucket endpoint URL, access key, and secret key must be set in settings."
36
+ )
37
+ self.session = session or aioboto3.Session(
38
+ aws_access_key_id=settings.r2_access_key.get_secret_value(),
39
+ aws_secret_access_key=settings.r2_secret_key.get_secret_value(),
40
+ region_name=settings.r2_default_region,
41
+ )
42
+ self.endpoint_url = str(settings.r2_bucket_endpoint_url)
43
+ self.config = AioConfig(
44
+ request_checksum_calculation="when_required",
45
+ response_checksum_validation="when_required",
46
+ )
47
+
48
+ if not default_bucket:
49
+ if settings.r2_default_bucket_name is None:
50
+ raise ValueError(
51
+ "Default bucket name must be provided either in settings or as an argument."
52
+ )
53
+ self.default_bucket = settings.r2_default_bucket_name
54
+ else:
55
+ self.default_bucket = default_bucket
56
+
57
+ async def put_object(
58
+ self,
59
+ body: bytes,
60
+ content_type: str = DEFAULT_CONTENT_TYPE,
61
+ bucket: str | None = None,
62
+ namespace: str | None = None,
63
+ original_filename: str | None = None,
64
+ ) -> BlobModel:
65
+ """Put an object into the R2 bucket and return the blob model"""
66
+
67
+ bucket = bucket or self.default_bucket
68
+
69
+ blob = BlobModel.from_bytes(
70
+ body=body,
71
+ content_type=content_type,
72
+ bucket=bucket,
73
+ namespace=namespace,
74
+ original_filename=original_filename,
75
+ )
76
+
77
+ await blob.insert()
78
+
79
+ if not blob.id:
80
+ raise ValueError("Blob ID must be set before uploading to R2.")
81
+
82
+ try:
83
+ async with self.session.client(
84
+ service_name=S3_SERVICE_NAME,
85
+ endpoint_url=self.endpoint_url,
86
+ config=self.config,
87
+ ) as s3_client:
88
+ await s3_client.put_object(
89
+ Bucket=bucket,
90
+ Key=blob.full_path,
91
+ Body=body,
92
+ ContentType=content_type,
93
+ )
94
+ blob.status = BlobStatus.UPLOADED
95
+ except Exception:
96
+ blob.status = BlobStatus.ERROR
97
+ finally:
98
+ await blob.save()
99
+
100
+ return blob
101
+
102
+ async def put_object_with_extension(
103
+ self,
104
+ body: bytes,
105
+ extension: str,
106
+ bucket: str | None = None,
107
+ namespace: str | None = None,
108
+ ) -> BlobModel:
109
+ """Put an object into the R2 bucket with content type guessed from extension"""
110
+ content_type, _ = mimetypes.guess_type(f"file.{extension.lstrip('.')}")
111
+ content_type = content_type or DEFAULT_CONTENT_TYPE
112
+
113
+ return await self.put_object(body, content_type, bucket, namespace)
114
+
115
+ async def put_file(
116
+ self,
117
+ file_path: Path | str,
118
+ content_type: str | None = None,
119
+ bucket: str | None = None,
120
+ namespace: str | None = None,
121
+ ) -> BlobModel:
122
+ """Put a file from filesystem into the R2 bucket"""
123
+ file_path = Path(file_path)
124
+
125
+ if not file_path.exists():
126
+ raise FileNotFoundError(f"File not found: {file_path}")
127
+
128
+ # Auto-detect content type if not provided
129
+ if content_type is None:
130
+ content_type, _ = mimetypes.guess_type(str(file_path))
131
+ content_type = content_type or DEFAULT_CONTENT_TYPE
132
+
133
+ return await self.put_object(
134
+ file_path.read_bytes(),
135
+ content_type,
136
+ bucket,
137
+ namespace,
138
+ original_filename=file_path.name,
139
+ )
140
+
141
+ async def get_object(self, key: str) -> bytes:
142
+ """Retrieve an object from the R2 bucket"""
143
+ blob = await BlobModel.get(key)
144
+ if not blob:
145
+ raise ValueError(f"Blob not found: {key}")
146
+
147
+ async with self.session.client(
148
+ service_name=S3_SERVICE_NAME,
149
+ endpoint_url=self.endpoint_url,
150
+ config=self.config,
151
+ ) as s3_client:
152
+ response = await s3_client.get_object(
153
+ Bucket=blob.bucket,
154
+ Key=blob.full_path,
155
+ )
156
+ return await response["Body"].read()
157
+
158
+ async def delete_object(self, key: str) -> None:
159
+ """Delete an object from the R2 bucket"""
160
+ blob = await BlobModel.get(key)
161
+ if not blob:
162
+ raise ValueError(f"Blob not found: {key}")
163
+
164
+ blob.status = BlobStatus.DELETED
165
+
166
+ await blob.save()
167
+
168
+ async def object_exists(self, key: str, check_bucket: bool = False) -> bool:
169
+ """Check if an object exists in the R2 bucket"""
170
+
171
+ blob = await BlobModel.get(key)
172
+ if not blob:
173
+ return False
174
+
175
+ return True
@@ -0,0 +1,50 @@
1
+ """Email service for sending transactional emails via AWS SES.
2
+
3
+ WARNING: This is a scaffolding-managed file. DO NOT MODIFY directly.
4
+ To extend email functionality, create wrapper services in the parent services directory.
5
+ """
6
+
7
+ from typing import Literal
8
+
9
+ import boto3
10
+
11
+ from vibetuner.config import settings
12
+
13
+
14
+ SES_SERVICE_NAME: Literal["ses"] = "ses"
15
+
16
+
17
+ class SESEmailService:
18
+ def __init__(
19
+ self,
20
+ ses_client=None,
21
+ from_email: str | None = None,
22
+ ) -> None:
23
+ self.ses_client = ses_client or boto3.client(
24
+ service_name=SES_SERVICE_NAME,
25
+ region_name=settings.project.aws_default_region,
26
+ aws_access_key_id=settings.aws_access_key_id.get_secret_value()
27
+ if settings.aws_access_key_id
28
+ else None,
29
+ aws_secret_access_key=settings.aws_secret_access_key.get_secret_value()
30
+ if settings.aws_secret_access_key
31
+ else None,
32
+ )
33
+ self.from_email = from_email or settings.project.from_email
34
+
35
+ async def send_email(
36
+ self, to_address: str, subject: str, html_body: str, text_body: str
37
+ ):
38
+ """Send email using Amazon SES"""
39
+ response = self.ses_client.send_email(
40
+ Source=self.from_email,
41
+ Destination={"ToAddresses": [to_address]},
42
+ Message={
43
+ "Subject": {"Data": subject, "Charset": "UTF-8"},
44
+ "Body": {
45
+ "Html": {"Data": html_body, "Charset": "UTF-8"},
46
+ "Text": {"Data": text_body, "Charset": "UTF-8"},
47
+ },
48
+ },
49
+ )
50
+ return response
@@ -0,0 +1,98 @@
1
+ # Core Tasks Module
2
+
3
+ **IMMUTABLE SCAFFOLDING CODE** - This is the framework's core background task infrastructure.
4
+
5
+ ## What's Here
6
+
7
+ This module contains the scaffolding's core task components:
8
+
9
+ - **worker.py** - Streaq worker setup and configuration
10
+ - **context.py** - Task context management (DB, HTTP client, etc.)
11
+ - ****init**.py** - Task infrastructure exports
12
+
13
+ ## Important Rules
14
+
15
+ ⚠️ **DO NOT MODIFY** these core task components directly.
16
+
17
+ **For changes to core tasks:**
18
+
19
+ - File an issue at `https://github.com/alltuner/scaffolding`
20
+ - Core changes benefit all projects using the scaffolding
21
+
22
+ **For your application tasks:**
23
+
24
+ - Create them in `src/app/tasks/` instead
25
+ - Import the worker from vibetuner: `from vibetuner.tasks.worker import worker`
26
+
27
+ ## Quick Reference
28
+
29
+ Tasks are only available if job queue was enabled during scaffolding.
30
+
31
+ The worker is defined in `src/vibetuner/tasks/worker.py` and should be imported from there in your app tasks.
32
+
33
+ ## User Task Pattern (for reference)
34
+
35
+ Your application tasks in `src/app/tasks/` should follow this pattern:
36
+
37
+ ```python
38
+ # src/app/tasks/emails.py
39
+ from vibetuner.models import UserModel
40
+ from vibetuner.tasks.worker import worker
41
+
42
+ @worker.task()
43
+ async def send_welcome_email(user_id: str) -> dict[str, str]:
44
+ """Example background job."""
45
+
46
+ # Access context
47
+ res = await worker.context.http_client.get(url)
48
+
49
+ if user := await UserModel.get(user_id):
50
+ # Perform side effects
51
+ return {"status": "sent", "user": user.email}
52
+ return {"status": "skipped"}
53
+ ```
54
+
55
+ ## Queueing Tasks
56
+
57
+ ```python
58
+ # In your routes: src/app/frontend/routes/auth.py
59
+ from app.tasks.emails import send_welcome_email
60
+
61
+ @router.post("/signup")
62
+ async def signup(email: str):
63
+ user = await create_user(email)
64
+
65
+ task = await send_welcome_email.enqueue(user.id)
66
+ # Optional: await task.result() or check task.id
67
+
68
+ return {"status": "registered", "job_id": task.id}
69
+ ```
70
+
71
+ Note: Import your task functions from `src/app/tasks/` but the worker itself comes from `vibetuner.tasks.worker`.
72
+
73
+ ## Worker Management
74
+
75
+ ```bash
76
+ just worker-dev # Run worker locally with auto-reload
77
+ ```
78
+
79
+ ## Task Registration
80
+
81
+ Add new task modules at the end of `src/app/tasks/__init__.py`:
82
+
83
+ ```python
84
+ # src/app/tasks/__init__.py
85
+ # Import your task modules so decorators register with worker
86
+ from . import emails # noqa: F401
87
+ from . import new_tasks # noqa: F401
88
+ ```
89
+
90
+ ## Monitoring
91
+
92
+ ```python
93
+ task = await send_digest_email.enqueue(account_id)
94
+
95
+ status = await task.status()
96
+ result = await task.result(timeout=30)
97
+ await task.abort() # Cancel if needed
98
+ ```
@@ -0,0 +1,98 @@
1
+ # Core Tasks Module
2
+
3
+ **IMMUTABLE SCAFFOLDING CODE** - This is the framework's core background task infrastructure.
4
+
5
+ ## What's Here
6
+
7
+ This module contains the scaffolding's core task components:
8
+
9
+ - **worker.py** - Streaq worker setup and configuration
10
+ - **context.py** - Task context management (DB, HTTP client, etc.)
11
+ - ****init**.py** - Task infrastructure exports
12
+
13
+ ## Important Rules
14
+
15
+ ⚠️ **DO NOT MODIFY** these core task components directly.
16
+
17
+ **For changes to core tasks:**
18
+
19
+ - File an issue at `https://github.com/alltuner/scaffolding`
20
+ - Core changes benefit all projects using the scaffolding
21
+
22
+ **For your application tasks:**
23
+
24
+ - Create them in `src/app/tasks/` instead
25
+ - Import the worker from vibetuner: `from vibetuner.tasks.worker import worker`
26
+
27
+ ## Quick Reference
28
+
29
+ Tasks are only available if job queue was enabled during scaffolding.
30
+
31
+ The worker is defined in `src/vibetuner/tasks/worker.py` and should be imported from there in your app tasks.
32
+
33
+ ## User Task Pattern (for reference)
34
+
35
+ Your application tasks in `src/app/tasks/` should follow this pattern:
36
+
37
+ ```python
38
+ # src/app/tasks/emails.py
39
+ from vibetuner.models import UserModel
40
+ from vibetuner.tasks.worker import worker
41
+
42
+ @worker.task()
43
+ async def send_welcome_email(user_id: str) -> dict[str, str]:
44
+ """Example background job."""
45
+
46
+ # Access context
47
+ res = await worker.context.http_client.get(url)
48
+
49
+ if user := await UserModel.get(user_id):
50
+ # Perform side effects
51
+ return {"status": "sent", "user": user.email}
52
+ return {"status": "skipped"}
53
+ ```
54
+
55
+ ## Queueing Tasks
56
+
57
+ ```python
58
+ # In your routes: src/app/frontend/routes/auth.py
59
+ from app.tasks.emails import send_welcome_email
60
+
61
+ @router.post("/signup")
62
+ async def signup(email: str):
63
+ user = await create_user(email)
64
+
65
+ task = await send_welcome_email.enqueue(user.id)
66
+ # Optional: await task.result() or check task.id
67
+
68
+ return {"status": "registered", "job_id": task.id}
69
+ ```
70
+
71
+ Note: Import your task functions from `src/app/tasks/` but the worker itself comes from `vibetuner.tasks.worker`.
72
+
73
+ ## Worker Management
74
+
75
+ ```bash
76
+ just worker-dev # Run worker locally with auto-reload
77
+ ```
78
+
79
+ ## Task Registration
80
+
81
+ Add new task modules at the end of `src/app/tasks/__init__.py`:
82
+
83
+ ```python
84
+ # src/app/tasks/__init__.py
85
+ # Import your task modules so decorators register with worker
86
+ from . import emails # noqa: F401
87
+ from . import new_tasks # noqa: F401
88
+ ```
89
+
90
+ ## Monitoring
91
+
92
+ ```python
93
+ task = await send_digest_email.enqueue(account_id)
94
+
95
+ status = await task.status()
96
+ result = await task.result(timeout=30)
97
+ await task.abort() # Cancel if needed
98
+ ```
@@ -0,0 +1,2 @@
1
+ # Import all your tasks here with (noqa: F401)
2
+ # from . import x_tasks
@@ -0,0 +1,34 @@
1
+ from contextlib import asynccontextmanager
2
+ from typing import AsyncIterator
3
+
4
+ from httpx import AsyncClient
5
+ from pydantic import BaseModel
6
+
7
+ from vibetuner.mongo import init_models
8
+
9
+
10
+ class Context(BaseModel):
11
+ http_client: AsyncClient
12
+ # Add the context properties for your tasks below
13
+
14
+ # Until here
15
+ model_config = {"arbitrary_types_allowed": True}
16
+
17
+
18
+ @asynccontextmanager
19
+ async def lifespan() -> AsyncIterator[Context]:
20
+ await init_models()
21
+ # Add below anything that should happen before startup
22
+
23
+ # Until here
24
+ async with (
25
+ AsyncClient() as http_client,
26
+ # Add any other async context managers you need here
27
+ ):
28
+ yield Context(
29
+ http_client=http_client,
30
+ # Add any other async context managers you need here
31
+ )
32
+
33
+ # Add below anything that should happen before shutdown
34
+ # Until here
@@ -0,0 +1,18 @@
1
+ from streaq import Worker
2
+
3
+ from vibetuner.config import settings
4
+ from vibetuner.tasks.context import lifespan
5
+
6
+
7
+ worker = Worker(
8
+ redis_url=str(settings.project.redis_url),
9
+ queue_name=(
10
+ settings.project.project_slug
11
+ if not settings.debug
12
+ else f"debug-{settings.project.project_slug}"
13
+ ),
14
+ lifespan=lifespan,
15
+ )
16
+
17
+ # Register tasks
18
+ # use something like from . import task_module_name // noqa: E402, F401
@@ -0,0 +1,48 @@
1
+ # Core Email Templates - DO NOT MODIFY
2
+
3
+ **⚠️ IMPORTANT**: Package-managed files. Changes will be lost on package updates.
4
+
5
+ ## How to Override
6
+
7
+ **NEVER modify files in this directory!** Instead:
8
+
9
+ 1. Copy template to your project's `templates/email/`
10
+ 2. Maintain the same directory structure
11
+ 3. Your version overrides automatically
12
+
13
+ ### Example
14
+
15
+ ```bash
16
+ # Core template (DO NOT EDIT, bundled in vibetuner package):
17
+ vibetuner/templates/email/default/magic_link.html.jinja
18
+
19
+ # Your override (CREATE THIS in your project):
20
+ templates/email/default/magic_link.html.jinja
21
+ ```
22
+
23
+ ## Template Structure
24
+
25
+ ```text
26
+ vibetuner/email/
27
+ └── default/
28
+ ├── magic_link.html.jinja # Passwordless login email (HTML)
29
+ └── magic_link.txt.jinja # Passwordless login email (text)
30
+ ```
31
+
32
+ ## Magic Link Email
33
+
34
+ The core provides magic link authentication emails used by the auth system.
35
+
36
+ ### Variables Available
37
+
38
+ - `login_url` - The magic link URL for authentication
39
+ - `project_name` - Your project's display name
40
+
41
+ Override these templates to customize branding, styling, and content.
42
+
43
+ ## Best Practices
44
+
45
+ 1. Always provide both HTML and text versions
46
+ 2. Test overrides after scaffolding updates
47
+ 3. Keep branding consistent across all emails
48
+ 4. Use inline styles for HTML emails
@@ -0,0 +1,48 @@
1
+ # Core Email Templates - DO NOT MODIFY
2
+
3
+ **⚠️ IMPORTANT**: Package-managed files. Changes will be lost on package updates.
4
+
5
+ ## How to Override
6
+
7
+ **NEVER modify files in this directory!** Instead:
8
+
9
+ 1. Copy template to your project's `templates/email/`
10
+ 2. Maintain the same directory structure
11
+ 3. Your version overrides automatically
12
+
13
+ ### Example
14
+
15
+ ```bash
16
+ # Core template (DO NOT EDIT, bundled in vibetuner package):
17
+ vibetuner/templates/email/default/magic_link.html.jinja
18
+
19
+ # Your override (CREATE THIS in your project):
20
+ templates/email/default/magic_link.html.jinja
21
+ ```
22
+
23
+ ## Template Structure
24
+
25
+ ```text
26
+ vibetuner/email/
27
+ └── default/
28
+ ├── magic_link.html.jinja # Passwordless login email (HTML)
29
+ └── magic_link.txt.jinja # Passwordless login email (text)
30
+ ```
31
+
32
+ ## Magic Link Email
33
+
34
+ The core provides magic link authentication emails used by the auth system.
35
+
36
+ ### Variables Available
37
+
38
+ - `login_url` - The magic link URL for authentication
39
+ - `project_name` - Your project's display name
40
+
41
+ Override these templates to customize branding, styling, and content.
42
+
43
+ ## Best Practices
44
+
45
+ 1. Always provide both HTML and text versions
46
+ 2. Test overrides after scaffolding updates
47
+ 3. Keep branding consistent across all emails
48
+ 4. Use inline styles for HTML emails
@@ -0,0 +1,16 @@
1
+ <html>
2
+ <body>
3
+ <h2>Sign in to {{ project_name }}</h2>
4
+ <p>Click the link below to sign in to your account:</p>
5
+ <p>
6
+ <a href="{{ login_url }}"
7
+ style="background-color: #007bff;
8
+ color: white;
9
+ padding: 12px 24px;
10
+ text-decoration: none;
11
+ border-radius: 4px">Sign In</a>
12
+ </p>
13
+ <p>This link will expire in 15 minutes.</p>
14
+ <p>If you didn't request this, you can safely ignore this email.</p>
15
+ </body>
16
+ </html>