Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3878837d25 | ||
|
|
b1db7c274f | ||
|
|
ac0ccf47e5 |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Copier template for a Python backend, either as a `backend/` part next to other parts or alone at the root of the project (`layout`): a uv project with one Docker image and one `python -m <module>` entrypoint per service (`api`, `bot`, `userbot`, `worker`, `scheduler`), pydantic-settings, rich logging, optional Dishka, MongoDB or PostgreSQL, Redis, taskiq (in-memory, Redis or PostgreSQL broker), CryptoPay, pydantic-ai and a DynamicConfig stored in the database.
|
Copier template for a Python backend, either as a `backend/` part next to other parts or alone at the root of the project (`layout`): a uv project with one Docker image and one `python -m <module>` entrypoint per service (`api`, `bot`, `userbot`, `worker`, `scheduler`), pydantic-settings, rich logging, optional Dishka, MongoDB or PostgreSQL, Redis, taskiq (in-memory, Redis or PostgreSQL broker), CryptoPay, pydantic-ai and a DynamicConfig stored in the database.
|
||||||
|
|
||||||
Ships `backend/docker-compose.yml` with root-relative paths, `docker-compose.local.yml` (published ports) and `docker-compose.prod.yml` (the `caddy` network), a `Makefile` with `fmt`, `check`, `build` and service-specific targets, `.env.example` and `CLAUDE.md`. Applied on top of the `infra` template, `layout: parts` or `layout: single` respectively.
|
Ships `backend/docker-compose.yml` with root-relative paths and profiles per service, `docker-compose.override.yml.example` (published ports and the `caddy` network aliases, copied by `make env`), a `Makefile` with `fmt`, `check`, `build` and service-specific targets, `.env.example` and `CLAUDE.md`. Applied on top of the `infra` template, `layout: parts` or `layout: single` respectively.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
copier copy --trust -d project_name="My Thing" <this-repo> <dest>
|
copier copy --trust -d project_name="My Thing" <this-repo> <dest>
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
{% if layout == 'root' %}COMPOSE_FILE=docker-compose.yml:docker-compose.local.yml
|
{%- set groups = [] -%}
|
||||||
{% endif %}RUN_ENVIRONMENT=dev
|
{%- if layout == 'root' %}{% set _ = groups.append(["COMPOSE_PROFILES=" ~ ("external," if database != 'none' or use_redis else "") ~ "services"]) %}{% endif -%}
|
||||||
LOG__LEVEL=INFO
|
{%- set _ = groups.append(["RUN_ENVIRONMENT=prod", "TZ=UTC"]) -%}
|
||||||
LOG__LEVEL_EXTERNAL=WARNING
|
{%- set _ = groups.append(["LOG__LEVEL=INFO", "LOG__LEVEL_EXTERNAL=WARNING", "LOG__SHOW_TIME=false", "LOG__CONSOLE_WIDTH=150"]) -%}
|
||||||
LOG__SHOW_TIME=false
|
{%- if database == 'mongo' %}{% set _ = groups.append(["DB__HOST=mongodb", "DB__PORT=27017", "DB__USER=" ~ project_slug, "DB__PASSWORD=" ~ project_slug, "DB__DB_NAME=" ~ project_slug]) %}{% endif -%}
|
||||||
LOG__CONSOLE_WIDTH=150
|
{%- if database == 'postgres' %}{% set _ = groups.append(["DB__HOST=postgres", "DB__PORT=5432", "DB__USER=" ~ project_slug, "DB__PASSWORD=" ~ project_slug, "DB__DB_NAME=" ~ project_slug]) %}{% endif -%}
|
||||||
{% if database == 'mongo' %}DB__HOST=mongodb
|
{%- if use_redis %}{% set _ = groups.append(["REDIS__HOST=redis", "REDIS__PORT=6379", "REDIS__CACHE_TTL=300"]) %}{% endif -%}
|
||||||
DB__PORT=27017
|
{%- if 'api' in backend_services %}{% set _ = groups.append(["API__HOST=0.0.0.0", "API__PORT=8080", "API__WORKERS=1", "API__DOCS=false"]) %}{% endif -%}
|
||||||
DB__USER={{ project_slug }}
|
{%- if 'aiogram_bot' in backend_services %}{% set _ = groups.append(["BOT__TOKEN="]) %}{% endif -%}
|
||||||
DB__PASSWORD={{ project_slug }}
|
{%- if include_payments %}{% set _ = groups.append(["CRYPTO_PAY__TOKEN="]) %}{% endif -%}
|
||||||
DB__DB_NAME={{ project_slug }}
|
{%- if use_llm %}{% set _ = groups.append(["LLM__MODEL=google-gla:gemini-2.5-flash", "LLM__GEMINI_API_KEY="]) %}{% endif -%}
|
||||||
{% endif %}{% if database == 'postgres' %}DB__HOST=postgres
|
{% for group in groups %}{{ group | join("\n") }}
|
||||||
DB__PORT=5432
|
{% if not loop.last %}
|
||||||
DB__USER={{ project_slug }}
|
{% endif %}{% endfor %}
|
||||||
DB__PASSWORD={{ project_slug }}
|
|
||||||
DB__DB_NAME={{ project_slug }}
|
|
||||||
{% endif %}{% if use_redis %}REDIS__HOST=redis
|
|
||||||
REDIS__PORT=6379
|
|
||||||
REDIS__CACHE_TTL=300
|
|
||||||
{% endif %}{% if 'api' in backend_services %}API__HOST=0.0.0.0
|
|
||||||
API__PORT=8080
|
|
||||||
API__WORKERS=1
|
|
||||||
{% endif %}{% if 'aiogram_bot' in backend_services %}BOT__TOKEN=
|
|
||||||
{% endif %}{% if include_payments %}CRYPTO_PAY__TOKEN=
|
|
||||||
{% endif %}{% if use_llm %}LLM__MODEL=google-gla:gemini-2.5-flash
|
|
||||||
LLM__GEMINI_API_KEY=
|
|
||||||
{% endif %}
|
|
||||||
@@ -10,6 +10,8 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
|||||||
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y build-essential && rm -rf /var/lib/apt/lists/*
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
FROM ghcr.io/astral-sh/uv:python3.13-alpine
|
FROM ghcr.io/astral-sh/uv:python3.13-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache tzdata
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -6,10 +6,8 @@
|
|||||||
{%- if 'scheduler' in backend_services and 'taskiq_worker' not in backend_services %}{% set _ = svc_modules.append("scheduler") %}{% endif -%}
|
{%- if 'scheduler' in backend_services and 'taskiq_worker' not in backend_services %}{% set _ = svc_modules.append("scheduler") %}{% endif -%}
|
||||||
{%- set has_script = dynamic_config and svc_modules -%}
|
{%- set has_script = dynamic_config and svc_modules -%}
|
||||||
COMPOSE := docker compose{% if layout == 'part' %} --project-directory ..{% endif %}
|
COMPOSE := docker compose{% if layout == 'part' %} --project-directory ..{% endif %}
|
||||||
{% if layout == 'root' %}SERVICES := $(COMPOSE) --profile services
|
|
||||||
EXTERNAL := $(COMPOSE) --profile external
|
.PHONY: {% if layout == 'root' %}env recreate rebuild restart logs down {% endif %}fmt check build{% if database == 'postgres' %} migrate revision{% endif %}{% if has_script %} script{% endif %}{% if 'kurigram_userbot' in backend_services %} session{% endif %}
|
||||||
{% endif %}
|
|
||||||
.PHONY: {% if layout == 'root' %}env external recreate rebuild restart logs down {% endif %}fmt check build{% if database == 'postgres' %} migrate revision{% endif %}{% if has_script %} script{% endif %}{% if 'kurigram_userbot' in backend_services %} session{% endif %}
|
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
uv run ruff format
|
uv run ruff format
|
||||||
@@ -25,23 +23,22 @@ build:
|
|||||||
{% if layout == 'root' %}
|
{% if layout == 'root' %}
|
||||||
env:
|
env:
|
||||||
@test -f .env || cp .env.example .env
|
@test -f .env || cp .env.example .env
|
||||||
|
@test -f docker-compose.override.yml || cp docker-compose.override.yml.example docker-compose.override.yml
|
||||||
|
|
||||||
external:
|
recreate:
|
||||||
$(EXTERNAL) up -d
|
$(COMPOSE) up -d --force-recreate
|
||||||
|
|
||||||
recreate: external
|
rebuild: build
|
||||||
$(SERVICES) up -d --force-recreate
|
$(COMPOSE) up -d
|
||||||
|
|
||||||
rebuild: build recreate
|
|
||||||
|
|
||||||
restart:
|
restart:
|
||||||
$(SERVICES) restart
|
$(COMPOSE) restart
|
||||||
|
|
||||||
logs:
|
logs:
|
||||||
$(SERVICES) logs -f --tail=100
|
$(COMPOSE) logs -f --tail=100
|
||||||
|
|
||||||
down:
|
down:
|
||||||
$(EXTERNAL) --profile services down
|
$(COMPOSE) down
|
||||||
{% endif %}{% if database == 'postgres' %}
|
{% endif %}{% if database == 'postgres' %}
|
||||||
migrate:
|
migrate:
|
||||||
$(COMPOSE) --profile external run --rm migrator
|
$(COMPOSE) --profile external run --rm migrator
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
{%- set has_ports = 'api' in backend_services or database != 'none' or use_redis -%}
|
|
||||||
services:{% if not has_ports %} {}{% endif %}
|
|
||||||
{% if 'api' in backend_services %} api:
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${API__PORT:-8080}:8080"
|
|
||||||
{% endif %}{% if database == 'mongo' %} mongodb:
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${DB__PORT:-27017}:${DB__PORT:-27017}"
|
|
||||||
{% endif %}{% if database == 'postgres' %} postgres:
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${DB__PORT:-5432}:5432"
|
|
||||||
{% endif %}{% if use_redis %} redis:
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${REDIS__PORT:-6379}:6379"
|
|
||||||
{% endif %}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{%- set dash = project_slug | replace('_', '-') -%}
|
||||||
|
{%- set entries = [] -%}
|
||||||
|
{%- if 'api' in backend_services %}{% set _ = entries.append('api') %}{% endif -%}
|
||||||
|
{%- if database == 'mongo' %}{% set _ = entries.append('mongodb') %}{% endif -%}
|
||||||
|
{%- if database == 'postgres' %}{% set _ = entries.append('postgres') %}{% endif -%}
|
||||||
|
{%- if use_redis %}{% set _ = entries.append('redis') %}{% endif -%}
|
||||||
|
services:{% if not entries %} {}{% endif %}
|
||||||
|
{% for entry in entries %}{% if entry == 'api' %} api:
|
||||||
|
networks:
|
||||||
|
default: {}
|
||||||
|
caddy:
|
||||||
|
aliases:
|
||||||
|
- {{ dash }}-api
|
||||||
|
{% elif entry == 'mongodb' %} mongodb:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${DB__PORT:-27017}:${DB__PORT:-27017}"
|
||||||
|
{% elif entry == 'postgres' %} postgres:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${DB__PORT:-5432}:5432"
|
||||||
|
{% elif entry == 'redis' %} redis:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${REDIS__PORT:-6379}:6379"
|
||||||
|
{% endif %}{% if not loop.last %}
|
||||||
|
{% endif %}{% endfor %}{% if 'api' in backend_services %}
|
||||||
|
networks:
|
||||||
|
caddy:
|
||||||
|
external: true
|
||||||
|
{% endif %}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{%- set dash = project_slug | replace('_', '-') -%}
|
|
||||||
{% if 'api' in backend_services -%}
|
|
||||||
services:
|
|
||||||
api:
|
|
||||||
networks:
|
|
||||||
caddy:
|
|
||||||
aliases:
|
|
||||||
- {{ dash }}-api
|
|
||||||
|
|
||||||
networks:
|
|
||||||
caddy:
|
|
||||||
external: true
|
|
||||||
{%- else -%}
|
|
||||||
services: {}
|
|
||||||
{%- endif %}
|
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
{%- set svc_networks = [] -%}
|
{%- set svc_networks = [] -%}
|
||||||
{%- if database != 'none' %}{% set _ = svc_networks.append('database') %}{% endif -%}
|
{%- if database != 'none' %}{% set _ = svc_networks.append('database') %}{% endif -%}
|
||||||
{%- if use_redis %}{% set _ = svc_networks.append('redis') %}{% endif -%}
|
{%- if use_redis %}{% set _ = svc_networks.append('redis') %}{% endif -%}
|
||||||
|
{%- set external_services = [] -%}
|
||||||
|
{%- if database == 'mongo' %}{% set _ = external_services.append('mongodb') %}{% endif -%}
|
||||||
|
{%- if database == 'postgres' %}{% set _ = external_services.append('postgres') %}{% endif -%}
|
||||||
|
{%- if use_redis %}{% set _ = external_services.append('redis') %}{% endif -%}
|
||||||
{%- set app_services = [] -%}
|
{%- set app_services = [] -%}
|
||||||
{%- if 'api' in backend_services %}{% set _ = app_services.append({'name': 'api', 'command': '[api]'}) %}{% endif -%}
|
{%- if 'api' in backend_services %}{% set _ = app_services.append({'name': 'api', 'command': '[api]'}) %}{% endif -%}
|
||||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = app_services.append({'name': 'bot', 'command': '[bot]'}) %}{% endif -%}
|
{%- if 'aiogram_bot' in backend_services %}{% set _ = app_services.append({'name': 'bot', 'command': '[bot]'}) %}{% endif -%}
|
||||||
@@ -20,14 +24,20 @@
|
|||||||
required: false
|
required: false
|
||||||
{% endif %} environment:
|
{% endif %} environment:
|
||||||
RUN_ENVIRONMENT: prod
|
RUN_ENVIRONMENT: prod
|
||||||
|
TZ: ${TZ:-UTC}
|
||||||
volumes:
|
volumes:
|
||||||
- ./{{ prefix }}src:/app/src
|
- ./{{ prefix }}src:/app/src
|
||||||
{% if has_scripts %} - ./{{ prefix }}scripts:/app/scripts
|
{% if has_scripts %} - ./{{ prefix }}scripts:/app/scripts
|
||||||
{% endif %}{% if name == 'userbot' %} - ./{{ prefix }}sessions:/app/sessions
|
{% endif %}{% if name == 'userbot' %} - ./{{ prefix }}sessions:/app/sessions
|
||||||
{% endif %}{% if name == 'api' and serve_spa %} - ./frontend/build:/app/static:ro
|
{% endif %}{% if name == 'api' and serve_spa %} - ./frontend/build:/app/static:ro
|
||||||
{% endif %} command: {{ command }}
|
{% endif %} command: {{ command }}
|
||||||
{% if svc_networks %} networks:
|
{% if external_services %} depends_on:
|
||||||
{% for net in svc_networks %} {{ net }}:
|
{% for ext in external_services %} {{ ext }}:
|
||||||
|
condition: service_healthy
|
||||||
|
required: false
|
||||||
|
{% endfor %}{% endif %}{% if svc_networks %} networks:
|
||||||
|
{% if name == 'api' %} default:
|
||||||
|
{% endif %}{% for net in svc_networks %} {{ net }}:
|
||||||
{% endfor %}{% endif %}{% endmacro -%}
|
{% endfor %}{% endif %}{% endmacro -%}
|
||||||
services:
|
services:
|
||||||
{% for svc in app_services %}{{ backend_service(svc.name, svc.command) }}{% if not loop.last %}
|
{% for svc in app_services %}{{ backend_service(svc.name, svc.command) }}{% if not loop.last %}
|
||||||
@@ -43,6 +53,11 @@ services:
|
|||||||
command: mongod --port ${DB__PORT:-27017}
|
command: mongod --port ${DB__PORT:-27017}
|
||||||
volumes:
|
volumes:
|
||||||
- database:/data/db
|
- database:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--port", "${DB__PORT:-27017}", "--eval", "db.adminCommand('ping')"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
networks:
|
networks:
|
||||||
database:
|
database:
|
||||||
aliases:
|
aliases:
|
||||||
@@ -98,6 +113,11 @@ services:
|
|||||||
profiles: [redis, external]
|
profiles: [redis, external]
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["redis-server", "--appendonly", "yes"]
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class ApiSettings(BaseSettings):
|
|||||||
host: str = "0.0.0.0" # noqa: S104
|
host: str = "0.0.0.0" # noqa: S104
|
||||||
port: int = 8080
|
port: int = 8080
|
||||||
workers: int = 1
|
workers: int = 1
|
||||||
|
docs: bool = False
|
||||||
{%- if serve_spa %}
|
{%- if serve_spa %}
|
||||||
static_dir: str = "static"
|
static_dir: str = "static"
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@@ -110,7 +111,10 @@ class Settings(BaseSettings):
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
case_sensitive=False, env_file=".env", env_nested_delimiter="__", extra="ignore"
|
case_sensitive=False,
|
||||||
|
env_file={% if layout == 'part' %}("../.env", ".env"){% else %}".env"{% endif %},
|
||||||
|
env_nested_delimiter="__",
|
||||||
|
extra="ignore",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -5,7 +5,11 @@ from utils.env import env
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"api.app:app", host=env.api.host, port=env.api.port, workers=env.api.workers
|
"api.app:app",
|
||||||
|
host=env.api.host,
|
||||||
|
port=env.api.port,
|
||||||
|
workers=env.api.workers,
|
||||||
|
forwarded_allow_ips="*",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -10,8 +10,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from api import routers
|
from api import routers
|
||||||
{% if use_dishka %}from dependencies.container import container
|
{% if use_dishka %}from dependencies.container import container
|
||||||
{% endif %}{% if database != 'none' %}from utils.db import init_db
|
{% endif %}{% if database != 'none' %}from utils.db import init_db
|
||||||
{% endif %}{% if serve_spa %}from utils.env import env
|
{% endif %}from utils.env import env
|
||||||
{% endif %}from utils.logging import setup_logging
|
from utils.logging import setup_logging
|
||||||
{% if serve_spa %}
|
{% if serve_spa %}
|
||||||
IMMUTABLE_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"}
|
IMMUTABLE_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"}
|
||||||
NO_CACHE_HEADERS = {"Cache-Control": "no-cache"}
|
NO_CACHE_HEADERS = {"Cache-Control": "no-cache"}
|
||||||
@@ -29,7 +29,11 @@ async def lifespan(app_: FastAPI) -> AsyncGenerator[None]:
|
|||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="{{ project_name }} API", lifespan=lifespan)
|
app = FastAPI(
|
||||||
|
title="{{ project_name }} API",
|
||||||
|
lifespan=lifespan,
|
||||||
|
openapi_url="/openapi.json" if env.api.docs else None,
|
||||||
|
)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
||||||
|
|||||||
+11
-5
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import signal
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import uvloop
|
import uvloop
|
||||||
@@ -11,8 +12,15 @@ import uvloop
|
|||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
|
SCAN_INTERVAL = 10
|
||||||
|
|
||||||
|
|
||||||
async def runner() -> None:
|
async def runner() -> None:
|
||||||
|
stop = asyncio.Event()
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
loop.add_signal_handler(sig, stop.set)
|
||||||
|
|
||||||
{% if database != 'none' %} await init_db()
|
{% if database != 'none' %} await init_db()
|
||||||
|
|
||||||
{% endif %} sessions_dir = anyio.Path("sessions")
|
{% endif %} sessions_dir = anyio.Path("sessions")
|
||||||
@@ -22,7 +30,7 @@ async def runner() -> None:
|
|||||||
clients: dict[int, PyroClient] = {}
|
clients: dict[int, PyroClient] = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while not stop.is_set():
|
||||||
current = {path.name async for path in sessions_dir.glob("*.session")}
|
current = {path.name async for path in sessions_dir.glob("*.session")}
|
||||||
|
|
||||||
for session_file in current - started:
|
for session_file in current - started:
|
||||||
@@ -43,7 +51,8 @@ async def runner() -> None:
|
|||||||
if me:
|
if me:
|
||||||
clients[me.id] = client
|
clients[me.id] = client
|
||||||
|
|
||||||
await asyncio.sleep(10)
|
with contextlib.suppress(TimeoutError):
|
||||||
|
await asyncio.wait_for(stop.wait(), SCAN_INTERVAL)
|
||||||
finally:
|
finally:
|
||||||
for client in clients.values():
|
for client in clients.values():
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
@@ -55,8 +64,5 @@ def main() -> None:
|
|||||||
uvloop.install()
|
uvloop.install()
|
||||||
|
|
||||||
logger.info("Starting...")
|
logger.info("Starting...")
|
||||||
|
|
||||||
with contextlib.suppress(KeyboardInterrupt):
|
|
||||||
asyncio.run(runner())
|
asyncio.run(runner())
|
||||||
|
|
||||||
logger.info("[red]Stopped.[/]")
|
logger.info("[red]Stopped.[/]")
|
||||||
|
|||||||
+7
-4
@@ -1,5 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import signal
|
||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
@@ -15,6 +15,10 @@ async def example_job() -> None:
|
|||||||
|
|
||||||
|
|
||||||
async def runner() -> None:
|
async def runner() -> None:
|
||||||
|
stop = asyncio.Event()
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
loop.add_signal_handler(sig, stop.set)
|
||||||
{%- if database != 'none' %}
|
{%- if database != 'none' %}
|
||||||
await init_db()
|
await init_db()
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
@@ -27,14 +31,13 @@ async def runner() -> None:
|
|||||||
scheduler.start()
|
scheduler.start()
|
||||||
logger.info("Scheduler started")
|
logger.info("Scheduler started")
|
||||||
|
|
||||||
with contextlib.suppress(asyncio.CancelledError):
|
await stop.wait()
|
||||||
await asyncio.Event().wait()
|
scheduler.shutdown()
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
logger.info("Starting...")
|
logger.info("Starting...")
|
||||||
with contextlib.suppress(KeyboardInterrupt):
|
|
||||||
asyncio.run(runner())
|
asyncio.run(runner())
|
||||||
logger.info("[red]Stopped.[/]")
|
logger.info("[red]Stopped.[/]")
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{%- set targets = [] -%}
|
||||||
|
{%- if database == 'postgres' %}{% set _ = targets.append("`make migrate`, `make revision m=\"message\"`") %}{% endif -%}
|
||||||
|
{%- if dynamic_config and backend_services %}{% set _ = targets.append("`make script name`") %}{% endif -%}
|
||||||
|
{%- if 'kurigram_userbot' in backend_services %}{% set _ = targets.append("`make session`") %}{% endif -%}
|
||||||
|
# {{ project_name }}
|
||||||
|
{% if project_description and project_description != project_name %}
|
||||||
|
{{ project_description }}
|
||||||
|
{% endif %}
|
||||||
|
Runs in Docker, locally the same way as in production. `COMPOSE_PROFILES` in `.env` selects the services that run on this machine. `docker-compose.override.yml` holds what differs between machines: published ports and the aliases on the external `caddy` network.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make env
|
||||||
|
make recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
`make env` creates `.env` and `docker-compose.override.yml` from their examples. `make recreate` starts the selected profiles, `make rebuild` builds the image first, `make logs`, `make restart` and `make down` do what they say.
|
||||||
|
|
||||||
|
`make fmt` formats, `make check` runs ruff and ty.{% if targets %} Also {{ targets | join(", ") }}.{% endif %}
|
||||||
Reference in New Issue
Block a user