zenit 1.0.0__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.
- scaffolder/__init__.py +0 -0
- scaffolder/addons/__pycache__/_registry.cpython-314.pyc +0 -0
- scaffolder/addons/_registry.py +30 -0
- scaffolder/addons/celery/__pycache__/addon.cpython-314.pyc +0 -0
- scaffolder/addons/celery/addon.py +68 -0
- scaffolder/addons/celery/files/tasks/celery_app.py.j2 +37 -0
- scaffolder/addons/celery/files/tasks/example_tasks.py.j2 +15 -0
- scaffolder/addons/docker/__pycache__/addon.cpython-314.pyc +0 -0
- scaffolder/addons/docker/addon.py +31 -0
- scaffolder/addons/docker/files/.dockerignore +14 -0
- scaffolder/addons/docker/files/Dockerfile.j2 +34 -0
- scaffolder/addons/docker/files/compose.yml.j2 +26 -0
- scaffolder/addons/github-actions/__pycache__/addon.cpython-314.pyc +0 -0
- scaffolder/addons/github-actions/addon.py +18 -0
- scaffolder/addons/github-actions/files/ci.yml.j2 +83 -0
- scaffolder/addons/redis/__pycache__/addon.cpython-314.pyc +0 -0
- scaffolder/addons/redis/addon.py +61 -0
- scaffolder/addons/redis/files/compose.redis.yml.j2 +11 -0
- scaffolder/addons/redis/files/redis.py +48 -0
- scaffolder/addons/sentry/__pycache__/addon.cpython-314.pyc +0 -0
- scaffolder/addons/sentry/addon.py +41 -0
- scaffolder/addons/sentry/files/sentry.py.j2 +35 -0
- scaffolder/assembler.py +266 -0
- scaffolder/context.py +85 -0
- scaffolder/dryrun.py +159 -0
- scaffolder/exceptions.py +2 -0
- scaffolder/generate/justfile.j2 +30 -0
- scaffolder/generate/pyproject.toml.j2 +58 -0
- scaffolder/generate.py +86 -0
- scaffolder/git.py +25 -0
- scaffolder/main.py +209 -0
- scaffolder/prompt.py +384 -0
- scaffolder/render.py +47 -0
- scaffolder/rollback.py +35 -0
- scaffolder/schema.py +118 -0
- scaffolder/templates/__pycache__/_load_config.cpython-314.pyc +0 -0
- scaffolder/templates/_common/__pycache__/apply.cpython-314.pyc +0 -0
- scaffolder/templates/_common/apply.py +42 -0
- scaffolder/templates/_common/envrc +4 -0
- scaffolder/templates/_common/gitattributes +13 -0
- scaffolder/templates/_common/gitignore +26 -0
- scaffolder/templates/_common/pre-commit-config.yaml +22 -0
- scaffolder/templates/_common/shell.nix +7 -0
- scaffolder/templates/_load_config.py +22 -0
- scaffolder/templates/blank/__pycache__/template.cpython-314.pyc +0 -0
- scaffolder/templates/blank/files/__main__.py +3 -0
- scaffolder/templates/blank/files/main.py.j2 +7 -0
- scaffolder/templates/blank/files/tests/test_main.py.j2 +9 -0
- scaffolder/templates/blank/template.py +51 -0
- scaffolder/templates/fastapi/__pycache__/template.cpython-314.pyc +0 -0
- scaffolder/templates/fastapi/files/.env.example +4 -0
- scaffolder/templates/fastapi/files/.env.j2 +4 -0
- scaffolder/templates/fastapi/files/alembic/env.py.j2 +50 -0
- scaffolder/templates/fastapi/files/alembic/script.py.mako +24 -0
- scaffolder/templates/fastapi/files/alembic.ini.j2 +33 -0
- scaffolder/templates/fastapi/files/api/router.py +12 -0
- scaffolder/templates/fastapi/files/api/routes/health.py +8 -0
- scaffolder/templates/fastapi/files/core/security.py +44 -0
- scaffolder/templates/fastapi/files/db/base.py +5 -0
- scaffolder/templates/fastapi/files/db/session.py +13 -0
- scaffolder/templates/fastapi/files/exceptions.py +25 -0
- scaffolder/templates/fastapi/files/lifecycle.py +14 -0
- scaffolder/templates/fastapi/files/main.py.j2 +7 -0
- scaffolder/templates/fastapi/files/models/mixins.py +27 -0
- scaffolder/templates/fastapi/files/schemas/common.py +14 -0
- scaffolder/templates/fastapi/files/scripts/wait_db.py +29 -0
- scaffolder/templates/fastapi/files/settings.py.j2 +20 -0
- scaffolder/templates/fastapi/files/tests/conftest.py.j2 +64 -0
- scaffolder/templates/fastapi/files/tests/test_health.py +7 -0
- scaffolder/templates/fastapi/template.py +172 -0
- scaffolder/ui.py +166 -0
- scaffolder/validate.py +85 -0
- zenit-1.0.0.dist-info/METADATA +419 -0
- zenit-1.0.0.dist-info/RECORD +78 -0
- zenit-1.0.0.dist-info/WHEEL +5 -0
- zenit-1.0.0.dist-info/entry_points.txt +3 -0
- zenit-1.0.0.dist-info/licenses/LICENSE +21 -0
- zenit-1.0.0.dist-info/top_level.txt +1 -0
scaffolder/__init__.py
ADDED
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Addon registry — discovers ``addon.py`` files and returns ``AddonConfig`` objects."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from scaffolder.schema import AddonConfig
|
|
7
|
+
|
|
8
|
+
_HERE = Path(__file__).parent.absolute()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_available_addons() -> list[AddonConfig]:
|
|
12
|
+
"""Return one ``AddonConfig`` for every addon directory found under this package."""
|
|
13
|
+
addons: list[AddonConfig] = []
|
|
14
|
+
for addon_dir in sorted(
|
|
15
|
+
p for p in _HERE.iterdir() if p.is_dir() and not p.name.startswith("_")
|
|
16
|
+
):
|
|
17
|
+
try:
|
|
18
|
+
spec = importlib.util.spec_from_file_location(
|
|
19
|
+
"addon_config", addon_dir / "addon.py"
|
|
20
|
+
)
|
|
21
|
+
if spec is None:
|
|
22
|
+
continue
|
|
23
|
+
mod = importlib.util.module_from_spec(spec)
|
|
24
|
+
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
|
25
|
+
cfg: AddonConfig = mod.config
|
|
26
|
+
cfg._module = mod # attach module so post_apply can be called later
|
|
27
|
+
addons.append(cfg)
|
|
28
|
+
except FileNotFoundError, AttributeError:
|
|
29
|
+
continue
|
|
30
|
+
return addons
|
|
Binary file
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from scaffolder.schema import AddonConfig, ComposeService, FileContribution
|
|
4
|
+
|
|
5
|
+
_HERE = Path(__file__).parent.absolute()
|
|
6
|
+
|
|
7
|
+
config = AddonConfig(
|
|
8
|
+
id="celery",
|
|
9
|
+
description="Celery worker + beat scheduler, backed by Redis",
|
|
10
|
+
requires=["redis"],
|
|
11
|
+
files=[
|
|
12
|
+
FileContribution(
|
|
13
|
+
dest="src/{{pkg_name}}/tasks/__init__.py",
|
|
14
|
+
content="",
|
|
15
|
+
),
|
|
16
|
+
FileContribution(
|
|
17
|
+
dest="src/{{pkg_name}}/tasks/celery_app.py",
|
|
18
|
+
source=str(_HERE / "files" / "tasks" / "celery_app.py.j2"),
|
|
19
|
+
template=True,
|
|
20
|
+
),
|
|
21
|
+
FileContribution(
|
|
22
|
+
dest="src/{{pkg_name}}/tasks/example_tasks.py",
|
|
23
|
+
source=str(_HERE / "files" / "tasks" / "example_tasks.py.j2"),
|
|
24
|
+
template=True,
|
|
25
|
+
),
|
|
26
|
+
],
|
|
27
|
+
compose_services=[
|
|
28
|
+
ComposeService(
|
|
29
|
+
name="celery-worker",
|
|
30
|
+
build=".",
|
|
31
|
+
command="celery -A {{pkg_name}}.tasks.celery_app worker --loglevel=info",
|
|
32
|
+
env_file=[".env"],
|
|
33
|
+
environment={"REDIS_URL": "redis://redis:6379/0"},
|
|
34
|
+
depends_on={
|
|
35
|
+
"redis": {"condition": "service_healthy"},
|
|
36
|
+
},
|
|
37
|
+
develop_watch=[{"action": "sync", "path": "./src", "target": "/app/src"}],
|
|
38
|
+
),
|
|
39
|
+
ComposeService(
|
|
40
|
+
name="celery-beat",
|
|
41
|
+
build=".",
|
|
42
|
+
command="celery -A {{pkg_name}}.tasks.celery_app beat --loglevel=info",
|
|
43
|
+
env_file=[".env"],
|
|
44
|
+
environment={"REDIS_URL": "redis://redis:6379/0"},
|
|
45
|
+
depends_on={
|
|
46
|
+
"redis": {"condition": "service_healthy"},
|
|
47
|
+
},
|
|
48
|
+
develop_watch=[{"action": "sync", "path": "./src", "target": "/app/src"}],
|
|
49
|
+
),
|
|
50
|
+
],
|
|
51
|
+
deps=["celery[redis]>=5", "flower"],
|
|
52
|
+
dev_deps=["pytest-celery"],
|
|
53
|
+
just_recipes=[
|
|
54
|
+
"# start celery worker and beat scheduler\n"
|
|
55
|
+
"celery-up:\n"
|
|
56
|
+
" docker compose up -d celery-worker celery-beat",
|
|
57
|
+
"# stop celery worker and beat scheduler\n"
|
|
58
|
+
"celery-down:\n"
|
|
59
|
+
" docker compose stop celery-worker celery-beat",
|
|
60
|
+
"# open flower monitoring UI on port 5555\n"
|
|
61
|
+
"celery-flower:\n"
|
|
62
|
+
" docker compose run --rm celery-worker "
|
|
63
|
+
"celery -A (( pkg_name )).tasks.celery_app flower --port=5555",
|
|
64
|
+
"# tail celery worker logs\n"
|
|
65
|
+
"celery-logs:\n"
|
|
66
|
+
" docker compose logs -f celery-worker",
|
|
67
|
+
],
|
|
68
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Celery application instance.
|
|
2
|
+
|
|
3
|
+
Start the worker:
|
|
4
|
+
celery -A (( pkg_name )).tasks.celery_app worker --loglevel=info
|
|
5
|
+
|
|
6
|
+
Start the beat scheduler:
|
|
7
|
+
celery -A (( pkg_name )).tasks.celery_app beat --loglevel=info
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from celery import Celery
|
|
13
|
+
|
|
14
|
+
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
|
15
|
+
|
|
16
|
+
celery_app = Celery(
|
|
17
|
+
__name__,
|
|
18
|
+
broker=REDIS_URL,
|
|
19
|
+
backend=REDIS_URL,
|
|
20
|
+
include=["(( pkg_name )).tasks.example_tasks"],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
celery_app.conf.update(
|
|
24
|
+
task_serializer="json",
|
|
25
|
+
result_serializer="json",
|
|
26
|
+
accept_content=["json"],
|
|
27
|
+
timezone="UTC",
|
|
28
|
+
enable_utc=True,
|
|
29
|
+
# Beat schedule — add periodic tasks here:
|
|
30
|
+
# beat_schedule={
|
|
31
|
+
# "example-every-30s": {
|
|
32
|
+
# "task": "(( pkg_name )).tasks.example_tasks.add",
|
|
33
|
+
# "schedule": 30.0,
|
|
34
|
+
# "args": (1, 2),
|
|
35
|
+
# },
|
|
36
|
+
# },
|
|
37
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Example Celery tasks.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from (( pkg_name )).tasks.example_tasks import add
|
|
5
|
+
result = add.delay(1, 2)
|
|
6
|
+
print(result.get(timeout=10)) # 3
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .celery_app import celery_app
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@celery_app.task # type: ignore[untyped-decorator]
|
|
13
|
+
def add(x: int, y: int) -> int:
|
|
14
|
+
"""Example task — replace or delete."""
|
|
15
|
+
return x + y
|
|
Binary file
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from scaffolder.schema import AddonConfig, FileContribution
|
|
4
|
+
|
|
5
|
+
_HERE = Path(__file__).parent.absolute()
|
|
6
|
+
|
|
7
|
+
config = AddonConfig(
|
|
8
|
+
id="docker",
|
|
9
|
+
description="Dockerfile + compose.yml + .dockerignore",
|
|
10
|
+
requires=[],
|
|
11
|
+
files=[
|
|
12
|
+
FileContribution(
|
|
13
|
+
dest="Dockerfile",
|
|
14
|
+
source=str(_HERE / "files" / "Dockerfile.j2"),
|
|
15
|
+
template=True,
|
|
16
|
+
),
|
|
17
|
+
FileContribution(
|
|
18
|
+
dest="compose.yml",
|
|
19
|
+
source=str(_HERE / "files" / "compose.yml.j2"),
|
|
20
|
+
template=True,
|
|
21
|
+
),
|
|
22
|
+
FileContribution(
|
|
23
|
+
dest=".dockerignore",
|
|
24
|
+
source=str(_HERE / "files" / ".dockerignore"),
|
|
25
|
+
),
|
|
26
|
+
],
|
|
27
|
+
just_recipes=[
|
|
28
|
+
"# build and start all services\ndocker-up:\n docker compose up --build",
|
|
29
|
+
"# stop all services\ndocker-down:\n docker compose down",
|
|
30
|
+
],
|
|
31
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM python:3.14-slim AS base
|
|
3
|
+
|
|
4
|
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
5
|
+
PYTHONUNBUFFERED=1 \
|
|
6
|
+
UV_PYTHON_DOWNLOADS=never
|
|
7
|
+
|
|
8
|
+
WORKDIR /app
|
|
9
|
+
|
|
10
|
+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|
11
|
+
|
|
12
|
+
RUN addgroup --system app && adduser --system --ingroup app app
|
|
13
|
+
|
|
14
|
+
COPY pyproject.toml uv.lock ./
|
|
15
|
+
RUN uv sync --frozen --no-dev --no-install-project
|
|
16
|
+
|
|
17
|
+
COPY src/ ./src/
|
|
18
|
+
|
|
19
|
+
RUN uv sync --frozen --no-dev
|
|
20
|
+
|
|
21
|
+
RUN chown -R app:app /app
|
|
22
|
+
|
|
23
|
+
USER app
|
|
24
|
+
|
|
25
|
+
ENV PATH="/app/.venv/bin:$PATH" \
|
|
26
|
+
PYTHONPATH="/app/src"
|
|
27
|
+
|
|
28
|
+
EXPOSE 8000
|
|
29
|
+
|
|
30
|
+
[% if template == "fastapi" %]
|
|
31
|
+
CMD ["uvicorn", "(( pkg_name )).main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
32
|
+
[% else %]
|
|
33
|
+
CMD ["python", "-m", "(( pkg_name ))"]
|
|
34
|
+
[% endif %]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
services:
|
|
2
|
+
app:
|
|
3
|
+
build: .
|
|
4
|
+
ports:
|
|
5
|
+
- "8000:8000"
|
|
6
|
+
[% if template == "fastapi" %]
|
|
7
|
+
env_file:
|
|
8
|
+
- .env
|
|
9
|
+
command: uvicorn (( pkg_name )).main:app --host 0.0.0.0 --port 8000 --reload
|
|
10
|
+
[% endif %]
|
|
11
|
+
develop:
|
|
12
|
+
watch:
|
|
13
|
+
- action: sync
|
|
14
|
+
path: ./src
|
|
15
|
+
target: /app/src
|
|
16
|
+
[% if template == "fastapi" %]
|
|
17
|
+
|
|
18
|
+
db:
|
|
19
|
+
image: postgres:16
|
|
20
|
+
environment:
|
|
21
|
+
POSTGRES_PASSWORD: postgres
|
|
22
|
+
ports:
|
|
23
|
+
- "5432:5432"
|
|
24
|
+
volumes:
|
|
25
|
+
- ./.pgdata:/var/lib/postgresql/data
|
|
26
|
+
[% endif %]
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from scaffolder.schema import AddonConfig, FileContribution
|
|
4
|
+
|
|
5
|
+
_HERE = Path(__file__).parent.absolute()
|
|
6
|
+
|
|
7
|
+
config = AddonConfig(
|
|
8
|
+
id="github-actions",
|
|
9
|
+
description="CI workflow (lint, type-check, test on push/PR)",
|
|
10
|
+
requires=[],
|
|
11
|
+
files=[
|
|
12
|
+
FileContribution(
|
|
13
|
+
dest=".github/workflows/ci.yml",
|
|
14
|
+
source=str(_HERE / "files" / "ci.yml.j2"),
|
|
15
|
+
template=True,
|
|
16
|
+
),
|
|
17
|
+
],
|
|
18
|
+
)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ci:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
[% if has_postgres or has_redis %]
|
|
13
|
+
services:
|
|
14
|
+
[% if has_postgres %]
|
|
15
|
+
postgres:
|
|
16
|
+
image: postgres:16
|
|
17
|
+
env:
|
|
18
|
+
POSTGRES_USER: postgres
|
|
19
|
+
POSTGRES_PASSWORD: postgres
|
|
20
|
+
POSTGRES_DB: (( name ))_test
|
|
21
|
+
ports:
|
|
22
|
+
- 5432:5432
|
|
23
|
+
options: >-
|
|
24
|
+
--health-cmd pg_isready
|
|
25
|
+
--health-interval 10s
|
|
26
|
+
--health-timeout 5s
|
|
27
|
+
--health-retries 5
|
|
28
|
+
[% endif %]
|
|
29
|
+
[% if has_redis %]
|
|
30
|
+
redis:
|
|
31
|
+
image: redis:7-alpine
|
|
32
|
+
ports:
|
|
33
|
+
- 6379:6379
|
|
34
|
+
options: >-
|
|
35
|
+
--health-cmd "redis-cli ping"
|
|
36
|
+
--health-interval 10s
|
|
37
|
+
--health-timeout 5s
|
|
38
|
+
--health-retries 5
|
|
39
|
+
[% endif %]
|
|
40
|
+
[% endif %]
|
|
41
|
+
|
|
42
|
+
steps:
|
|
43
|
+
- uses: actions/checkout@v4
|
|
44
|
+
|
|
45
|
+
- name: Install uv
|
|
46
|
+
uses: astral-sh/setup-uv@v4
|
|
47
|
+
with:
|
|
48
|
+
version: "latest"
|
|
49
|
+
enable-cache: true
|
|
50
|
+
cache-dependency-glob: "uv.lock"
|
|
51
|
+
|
|
52
|
+
- name: Set up Python
|
|
53
|
+
run: uv python install 3.14
|
|
54
|
+
|
|
55
|
+
- name: Install dependencies
|
|
56
|
+
run: uv sync --all-extras
|
|
57
|
+
[% if has_postgres %]
|
|
58
|
+
|
|
59
|
+
- name: Run migrations
|
|
60
|
+
env:
|
|
61
|
+
DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/(( name ))_test
|
|
62
|
+
run: uv run alembic upgrade head
|
|
63
|
+
[% endif %]
|
|
64
|
+
|
|
65
|
+
- name: Lint
|
|
66
|
+
run: uv run ruff check .
|
|
67
|
+
|
|
68
|
+
- name: Format check
|
|
69
|
+
run: uv run ruff format --check .
|
|
70
|
+
|
|
71
|
+
- name: Type check
|
|
72
|
+
run: uv run mypy src/
|
|
73
|
+
|
|
74
|
+
- name: Test
|
|
75
|
+
env:
|
|
76
|
+
[% if has_postgres %]
|
|
77
|
+
DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/(( name ))_test
|
|
78
|
+
[% endif %]
|
|
79
|
+
[% if has_redis %]
|
|
80
|
+
REDIS_URL: redis://localhost:6379/0
|
|
81
|
+
[% endif %]
|
|
82
|
+
run: uv run pytest -v
|
|
83
|
+
|
|
Binary file
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Declarative config for the redis addon."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from scaffolder.schema import (
|
|
6
|
+
AddonConfig,
|
|
7
|
+
ComposeService,
|
|
8
|
+
EnvVar,
|
|
9
|
+
FileContribution,
|
|
10
|
+
Injection,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
_HERE = Path(__file__).parent.absolute()
|
|
14
|
+
|
|
15
|
+
config = AddonConfig(
|
|
16
|
+
id="redis",
|
|
17
|
+
description="Redis service + connection helper + compose service",
|
|
18
|
+
requires=[],
|
|
19
|
+
files=[
|
|
20
|
+
FileContribution(
|
|
21
|
+
dest="src/{{pkg_name}}/integrations/__init__.py",
|
|
22
|
+
content="",
|
|
23
|
+
),
|
|
24
|
+
FileContribution(
|
|
25
|
+
dest="src/{{pkg_name}}/integrations/redis.py",
|
|
26
|
+
source=str(_HERE / "files" / "redis.py"),
|
|
27
|
+
),
|
|
28
|
+
],
|
|
29
|
+
compose_services=[
|
|
30
|
+
ComposeService(
|
|
31
|
+
name="redis",
|
|
32
|
+
image="redis:7-alpine",
|
|
33
|
+
ports=["6379:6379"],
|
|
34
|
+
volumes=["redis-data:/data"],
|
|
35
|
+
command="redis-server --appendonly yes",
|
|
36
|
+
healthcheck={
|
|
37
|
+
"test": ["CMD", "redis-cli", "ping"],
|
|
38
|
+
"interval": "1s",
|
|
39
|
+
"timeout": "3s",
|
|
40
|
+
"retries": 5,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
],
|
|
44
|
+
compose_volumes=["redis-data"],
|
|
45
|
+
env_vars=[
|
|
46
|
+
EnvVar(key="REDIS_URL", default="redis://localhost:6379/0"),
|
|
47
|
+
],
|
|
48
|
+
deps=["redis>=5", "hiredis"],
|
|
49
|
+
dev_deps=["fakeredis[aioredis]"],
|
|
50
|
+
just_recipes=[
|
|
51
|
+
"# start redis\nredis-up:\n docker compose up -d redis",
|
|
52
|
+
"# stop redis\nredis-down:\n docker compose stop redis",
|
|
53
|
+
"# open redis-cli\nredis-cli:\n redis-cli",
|
|
54
|
+
],
|
|
55
|
+
injections=[
|
|
56
|
+
Injection(
|
|
57
|
+
point="settings_fields",
|
|
58
|
+
content=' redis_url: str = "redis://localhost:6379/0"',
|
|
59
|
+
),
|
|
60
|
+
],
|
|
61
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Redis connection helper.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from mypackage.integrations.redis import get_redis
|
|
5
|
+
|
|
6
|
+
@router.get("/example")
|
|
7
|
+
async def example(redis: Redis = Depends(get_redis)) -> dict:
|
|
8
|
+
await redis.set("key", "value", ex=60)
|
|
9
|
+
value = await redis.get("key")
|
|
10
|
+
return {"value": value}
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from collections.abc import AsyncGenerator
|
|
15
|
+
|
|
16
|
+
import redis.asyncio as aioredis
|
|
17
|
+
from redis.asyncio import Redis
|
|
18
|
+
|
|
19
|
+
# REDIS_URL is read from the environment directly.
|
|
20
|
+
# For FastAPI projects, settings.py exposes this via the redis_url field,
|
|
21
|
+
# which is set from the REDIS_URL environment variable.
|
|
22
|
+
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
|
|
23
|
+
|
|
24
|
+
_pool: aioredis.ConnectionPool | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_pool() -> aioredis.ConnectionPool:
|
|
28
|
+
global _pool
|
|
29
|
+
if _pool is None:
|
|
30
|
+
_pool = aioredis.ConnectionPool.from_url(
|
|
31
|
+
REDIS_URL,
|
|
32
|
+
decode_responses=True,
|
|
33
|
+
)
|
|
34
|
+
return _pool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def get_redis() -> AsyncGenerator[Redis]:
|
|
38
|
+
"""FastAPI dependency that yields a Redis client from the shared pool."""
|
|
39
|
+
async with aioredis.Redis(connection_pool=_get_pool()) as client:
|
|
40
|
+
yield client
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def close_redis() -> None:
|
|
44
|
+
"""Call from lifespan shutdown to cleanly drain the pool."""
|
|
45
|
+
global _pool
|
|
46
|
+
if _pool is not None:
|
|
47
|
+
await _pool.aclose()
|
|
48
|
+
_pool = None
|
|
Binary file
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from scaffolder.schema import AddonConfig, EnvVar, FileContribution, Injection
|
|
4
|
+
|
|
5
|
+
_HERE = Path(__file__).parent.absolute()
|
|
6
|
+
|
|
7
|
+
config = AddonConfig(
|
|
8
|
+
id="sentry",
|
|
9
|
+
description="Sentry error tracking + performance monitoring",
|
|
10
|
+
requires=[],
|
|
11
|
+
files=[
|
|
12
|
+
FileContribution(
|
|
13
|
+
dest="src/{{pkg_name}}/integrations/__init__.py",
|
|
14
|
+
content="",
|
|
15
|
+
),
|
|
16
|
+
FileContribution(
|
|
17
|
+
dest="src/{{pkg_name}}/integrations/sentry.py",
|
|
18
|
+
source=str(_HERE / "files" / "sentry.py.j2"),
|
|
19
|
+
template=True,
|
|
20
|
+
),
|
|
21
|
+
],
|
|
22
|
+
env_vars=[
|
|
23
|
+
EnvVar(key="SENTRY_DSN", default=""),
|
|
24
|
+
EnvVar(key="SENTRY_ENVIRONMENT", default="development"),
|
|
25
|
+
],
|
|
26
|
+
deps=["sentry-sdk[fastapi]"],
|
|
27
|
+
just_recipes=[
|
|
28
|
+
"# print sentry-sdk version\nsentry-check:\n uv run python -c \"import sentry_sdk; print('sentry-sdk', sentry_sdk.VERSION)\"",
|
|
29
|
+
"# check whether SENTRY_DSN is set\nsentry-test:\n uv run python -c \"from (( pkg_name )).integrations.sentry import init_sentry; import os; init_sentry(); print('Sentry DSN:', os.environ.get('SENTRY_DSN') or 'not set')\"",
|
|
30
|
+
],
|
|
31
|
+
injections=[
|
|
32
|
+
Injection(
|
|
33
|
+
point="lifespan_startup",
|
|
34
|
+
content=" from .integrations.sentry import init_sentry\n init_sentry()",
|
|
35
|
+
),
|
|
36
|
+
Injection(
|
|
37
|
+
point="settings_fields",
|
|
38
|
+
content=' sentry_dsn: str = ""\n sentry_environment: str = "development"',
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Sentry initialisation.
|
|
2
|
+
|
|
3
|
+
Set SENTRY_DSN in your .env to enable. Leave blank to disable (safe for local dev).
|
|
4
|
+
|
|
5
|
+
Get your DSN at: https://sentry.io/settings/<org>/projects/<project>/keys/
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# ruff: noqa: I001
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
import sentry_sdk
|
|
12
|
+
[% if template == "fastapi" %]
|
|
13
|
+
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
|
14
|
+
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
|
|
15
|
+
[% endif %]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def init_sentry() -> None:
|
|
19
|
+
dsn = os.getenv("SENTRY_DSN", "")
|
|
20
|
+
if not dsn:
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
sentry_sdk.init(
|
|
24
|
+
dsn=dsn,
|
|
25
|
+
environment=os.getenv("SENTRY_ENVIRONMENT", "development"),
|
|
26
|
+
traces_sample_rate=1.0,
|
|
27
|
+
profiles_sample_rate=1.0,
|
|
28
|
+
[% if template == "fastapi" %]
|
|
29
|
+
integrations=[
|
|
30
|
+
FastApiIntegration(),
|
|
31
|
+
SqlalchemyIntegration(),
|
|
32
|
+
],
|
|
33
|
+
[% endif %]
|
|
34
|
+
send_default_pii=False,
|
|
35
|
+
)
|