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.
- vibetuner/__init__.py +2 -0
- vibetuner/__main__.py +4 -0
- vibetuner/cli/__init__.py +68 -0
- vibetuner/cli/run.py +161 -0
- vibetuner/config.py +128 -0
- vibetuner/context.py +25 -0
- vibetuner/frontend/AGENTS.md +113 -0
- vibetuner/frontend/CLAUDE.md +113 -0
- vibetuner/frontend/__init__.py +94 -0
- vibetuner/frontend/context.py +10 -0
- vibetuner/frontend/deps.py +41 -0
- vibetuner/frontend/email.py +45 -0
- vibetuner/frontend/hotreload.py +13 -0
- vibetuner/frontend/lifespan.py +26 -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 +150 -0
- vibetuner/frontend/routes/debug.py +414 -0
- vibetuner/frontend/routes/health.py +33 -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/AGENTS.md +165 -0
- vibetuner/models/CLAUDE.md +165 -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 +18 -0
- vibetuner/paths.py +112 -0
- vibetuner/services/AGENTS.md +104 -0
- vibetuner/services/CLAUDE.md +104 -0
- vibetuner/services/__init__.py +0 -0
- vibetuner/services/blob.py +175 -0
- vibetuner/services/email.py +50 -0
- vibetuner/tasks/AGENTS.md +98 -0
- vibetuner/tasks/CLAUDE.md +98 -0
- vibetuner/tasks/__init__.py +2 -0
- vibetuner/tasks/context.py +34 -0
- vibetuner/tasks/worker.py +18 -0
- vibetuner/templates/email/AGENTS.md +48 -0
- vibetuner/templates/email/CLAUDE.md +48 -0
- vibetuner/templates/email/default/magic_link.html.jinja +16 -0
- vibetuner/templates/email/default/magic_link.txt.jinja +5 -0
- vibetuner/templates/frontend/AGENTS.md +74 -0
- vibetuner/templates/frontend/CLAUDE.md +74 -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 +42 -0
- vibetuner/templates/frontend/debug/collections.html.jinja +103 -0
- vibetuner/templates/frontend/debug/components/debug_nav.html.jinja +55 -0
- vibetuner/templates/frontend/debug/index.html.jinja +83 -0
- vibetuner/templates/frontend/debug/info.html.jinja +256 -0
- vibetuner/templates/frontend/debug/users.html.jinja +137 -0
- vibetuner/templates/frontend/debug/version.html.jinja +53 -0
- vibetuner/templates/frontend/email/magic_link.txt.jinja +5 -0
- vibetuner/templates/frontend/email_sent.html.jinja +82 -0
- vibetuner/templates/frontend/index.html.jinja +19 -0
- vibetuner/templates/frontend/lang/select.html.jinja +4 -0
- vibetuner/templates/frontend/login.html.jinja +84 -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 +85 -0
- vibetuner/templates/frontend/user/profile.html.jinja +156 -0
- vibetuner/templates/markdown/.placeholder +0 -0
- vibetuner/templates/markdown/AGENTS.md +29 -0
- vibetuner/templates/markdown/CLAUDE.md +29 -0
- vibetuner/templates.py +152 -0
- vibetuner/time.py +57 -0
- vibetuner/versioning.py +8 -0
- {vibetuner-2.6.1.dist-info → vibetuner-2.7.1.dist-info}/METADATA +2 -1
- vibetuner-2.7.1.dist-info/RECORD +84 -0
- vibetuner-2.6.1.dist-info/RECORD +0 -4
- {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,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>
|