Compare commits
11
Commits
v0.1.0
..
b4125407bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4125407bf | ||
|
|
e0d5defa97 | ||
|
|
dae9532ef6 | ||
|
|
e316bc92a5 | ||
|
|
3878837d25 | ||
|
|
b1db7c274f | ||
|
|
ac0ccf47e5 | ||
|
|
1f416dbe60 | ||
|
|
9afa7da9c4 | ||
|
|
aac037676c | ||
|
|
321e4b6b35 |
No files matched your search
@@ -1,10 +1,10 @@
|
||||
# backend-python
|
||||
|
||||
Copier template for a `backend/` part: 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, 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`. Meant to be applied on top of the `infra` template.
|
||||
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
|
||||
copier copy --trust -d project_name="My Thing" <this-repo> <dest>
|
||||
copier update -a .copier-answers.backend.yml
|
||||
copier update -a .copier/backend.yml
|
||||
```
|
||||
+25
-4
@@ -1,6 +1,6 @@
|
||||
_subdirectory: template
|
||||
_templates_suffix: .jinja
|
||||
_answers_file: .copier-answers.backend.yml
|
||||
_answers_file: .copier/backend.yml
|
||||
_envops:
|
||||
keep_trailing_newline: true
|
||||
|
||||
@@ -25,6 +25,14 @@ author_email:
|
||||
type: str
|
||||
default: ""
|
||||
|
||||
layout:
|
||||
type: str
|
||||
help: Where the backend lives
|
||||
choices:
|
||||
In backend/ next to other parts: part
|
||||
At the root of the project: root
|
||||
default: part
|
||||
|
||||
backend_services:
|
||||
type: str
|
||||
multiselect: true
|
||||
@@ -63,6 +71,9 @@ taskiq_broker:
|
||||
choices:
|
||||
in-memory: memory
|
||||
Redis: redis
|
||||
PostgreSQL (taskiq-pg):
|
||||
value: postgres
|
||||
validator: "{% if database != 'postgres' %}Requires the PostgreSQL database{% endif %}"
|
||||
default: memory
|
||||
|
||||
include_payments:
|
||||
@@ -84,7 +95,7 @@ dynamic_config:
|
||||
|
||||
serve_spa:
|
||||
type: bool
|
||||
when: "{{ 'api' in backend_services }}"
|
||||
when: "{{ layout == 'part' and 'api' in backend_services }}"
|
||||
help: Serve the frontend build from the api as a SPA
|
||||
default: false
|
||||
|
||||
@@ -93,6 +104,16 @@ install_deps:
|
||||
help: uv sync after generation
|
||||
default: true
|
||||
|
||||
part_dir:
|
||||
type: str
|
||||
when: false
|
||||
default: "{{ 'backend' if layout == 'part' else '.' }}"
|
||||
|
||||
prefix:
|
||||
type: str
|
||||
when: false
|
||||
default: "{{ 'backend/' if layout == 'part' else '' }}"
|
||||
|
||||
_tasks:
|
||||
- "{% if _copier_operation == 'copy' and install_deps %}sh -c 'cd backend && uv sync'{% else %}true{% endif %}"
|
||||
- "{% if install_deps %}sh -c 'cd backend && uv run ruff format -q'{% else %}true{% endif %}"
|
||||
- "{% if _copier_operation == 'copy' and install_deps %}sh -c 'cd {{ part_dir }} && uv sync'{% else %}true{% endif %}"
|
||||
- "{% if install_deps %}sh -c 'cd {{ part_dir }} && uv run ruff format -q'{% else %}true{% endif %}"
|
||||
@@ -1,26 +0,0 @@
|
||||
RUN_ENVIRONMENT=dev
|
||||
LOG__LEVEL=INFO
|
||||
LOG__LEVEL_EXTERNAL=WARNING
|
||||
LOG__SHOW_TIME=false
|
||||
LOG__CONSOLE_WIDTH=150
|
||||
{% if database == 'mongo' %}DB__HOST=mongodb
|
||||
DB__PORT=27017
|
||||
DB__USER={{ project_slug }}
|
||||
DB__PASSWORD={{ project_slug }}
|
||||
DB__DB_NAME={{ project_slug }}
|
||||
{% endif %}{% if database == 'postgres' %}DB__HOST=postgres
|
||||
DB__PORT=5432
|
||||
DB__USER={{ project_slug }}
|
||||
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 %}
|
||||
@@ -1,38 +0,0 @@
|
||||
{%- set svc_modules = [] -%}
|
||||
{%- if 'api' in backend_services %}{% set _ = svc_modules.append("api") %}{% endif -%}
|
||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = svc_modules.append("bot") %}{% endif -%}
|
||||
{%- if 'taskiq_worker' in backend_services %}{% set _ = svc_modules.append("worker") %}{% endif -%}
|
||||
{%- if 'kurigram_userbot' in backend_services %}{% set _ = svc_modules.append("userbot") %}{% 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 -%}
|
||||
COMPOSE := docker compose --project-directory ..
|
||||
|
||||
.PHONY: fmt check build{% if database == 'postgres' %} migrate revision{% endif %}{% if has_script %} script{% endif %}{% if 'kurigram_userbot' in backend_services %} session{% endif %}
|
||||
|
||||
fmt:
|
||||
uv run ruff format
|
||||
uv run ruff check --fix
|
||||
|
||||
check:
|
||||
uv run ruff format --check
|
||||
uv run ruff check
|
||||
uv run ty check
|
||||
|
||||
build:
|
||||
$(COMPOSE) build {{ svc_modules[0] }}
|
||||
{% if database == 'postgres' %}
|
||||
migrate:
|
||||
$(COMPOSE) --profile external run --rm migrator
|
||||
|
||||
revision:
|
||||
$(COMPOSE) --profile external run --rm migrator revision --autogenerate -m "$(m)"
|
||||
{% endif %}{% if has_script %}
|
||||
script:
|
||||
@$(COMPOSE) run --rm {{ svc_modules[0] }} scripts.$(word 2,$(MAKECMDGOALS)) $(wordlist 3,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
|
||||
%:
|
||||
@:
|
||||
{% endif %}{% if 'kurigram_userbot' in backend_services %}
|
||||
session:
|
||||
uv run python scripts/session/create.py
|
||||
{% endif %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import uvicorn
|
||||
|
||||
from utils.env import env
|
||||
|
||||
|
||||
def main() -> None:
|
||||
uvicorn.run(
|
||||
"api.app:app", host=env.api.host, port=env.api.port, workers=env.api.workers
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
from .modules.client import PyroClient
|
||||
|
||||
__all__ = ["PyroClient"]
|
||||
@@ -1,5 +0,0 @@
|
||||
[environment]
|
||||
python = "backend/.venv"
|
||||
|
||||
[src]
|
||||
exclude = ["backend/migrations"]
|
||||
@@ -0,0 +1,5 @@
|
||||
[environment]
|
||||
python = "{{ prefix }}.venv"
|
||||
|
||||
[src]
|
||||
exclude = ["{{ prefix }}migrations"]
|
||||
File renamed without changes.
@@ -0,0 +1,14 @@
|
||||
{%- set groups = [] -%}
|
||||
{%- if layout == 'root' %}{% set _ = groups.append(["COMPOSE_PROFILES=" ~ ("external," if database != 'none' or use_redis else "") ~ "services"]) %}{% endif -%}
|
||||
{%- set _ = groups.append(["RUN_ENVIRONMENT=prod", "TZ=UTC"]) -%}
|
||||
{%- set _ = groups.append(["LOG__LEVEL=INFO", "LOG__LEVEL_EXTERNAL=WARNING", "LOG__SHOW_TIME=false", "LOG__CONSOLE_WIDTH=150"]) -%}
|
||||
{%- 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 -%}
|
||||
{%- 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 use_redis %}{% set _ = groups.append(["REDIS__HOST=redis", "REDIS__PORT=6379", "REDIS__CACHE_TTL=300"]) %}{% endif -%}
|
||||
{%- if 'api' in backend_services %}{% set _ = groups.append(["API__HOST=0.0.0.0", "API__PORT=8080", "API__WORKERS=1", "API__DOCS=false"]) %}{% endif -%}
|
||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = groups.append(["BOT__TOKEN="]) %}{% endif -%}
|
||||
{%- if include_payments %}{% set _ = groups.append(["CRYPTO_PAY__TOKEN="]) %}{% endif -%}
|
||||
{%- if use_llm %}{% set _ = groups.append(["LLM__MODEL=google-gla:gemini-2.5-flash", "LLM__GEMINI_API_KEY="]) %}{% endif -%}
|
||||
{% for group in groups %}{{ group | join("\n") }}
|
||||
{% if not loop.last %}
|
||||
{% endif %}{% endfor %}
|
||||
File renamed without changes.
@@ -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/*
|
||||
{%- else -%}
|
||||
FROM ghcr.io/astral-sh/uv:python3.13-alpine
|
||||
|
||||
RUN apk add --no-cache tzdata
|
||||
{%- endif %}
|
||||
|
||||
WORKDIR /app
|
||||
@@ -0,0 +1,88 @@
|
||||
{%- set svc_modules = [] -%}
|
||||
{%- if 'api' in backend_services %}{% set _ = svc_modules.append("api") %}{% endif -%}
|
||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = svc_modules.append("bot") %}{% endif -%}
|
||||
{%- if 'taskiq_worker' in backend_services %}{% set _ = svc_modules.append("worker") %}{% endif -%}
|
||||
{%- if 'kurigram_userbot' in backend_services %}{% set _ = svc_modules.append("userbot") %}{% endif -%}
|
||||
{%- if 'scheduler' in backend_services and 'taskiq_worker' not in backend_services %}{% set _ = svc_modules.append("scheduler") %}{% endif -%}
|
||||
{%- set app_services = [] -%}
|
||||
{%- if 'api' in backend_services %}{% set _ = app_services.append("api") %}{% endif -%}
|
||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = app_services.append("bot") %}{% endif -%}
|
||||
{%- if 'taskiq_worker' in backend_services %}{% set _ = app_services.append("worker") %}{% endif -%}
|
||||
{%- if 'kurigram_userbot' in backend_services %}{% set _ = app_services.append("userbot") %}{% endif -%}
|
||||
{%- if 'scheduler' in backend_services %}{% set _ = app_services.append("scheduler") %}{% endif -%}
|
||||
{%- set has_script = dynamic_config and svc_modules -%}
|
||||
{%- set has_scripts = dynamic_config or 'kurigram_userbot' in backend_services -%}
|
||||
COMPOSE := {% if layout == 'part' %}cd .. && {% endif %}docker compose
|
||||
{% if svc_modules %}IMAGE := {{ project_slug }}/backend
|
||||
IMAGE_INPUTS := pyproject.toml uv.lock Dockerfile
|
||||
CODE := src{% if has_scripts %} scripts{% endif %}{% if serve_spa %} ../frontend{% endif %}
|
||||
SERVICES := {{ app_services | join(' ') }}
|
||||
changed = ! git diff --quiet $(1) -- $(2) 2>/dev/null
|
||||
stamp = { git update-ref $(1) HEAD 2>/dev/null || true; }
|
||||
{% endif %}
|
||||
.PHONY: {% if layout == 'root' %}env recreate rebuild restart logs down deploy {% endif %}fmt check{% if svc_modules %} build pre-deploy post-deploy{% endif %}{% if database == 'postgres' %} migrate revision{% endif %}{% if has_script %} script{% endif %}{% if 'kurigram_userbot' in backend_services %} session{% endif %}
|
||||
|
||||
fmt:
|
||||
uv run ruff format
|
||||
uv run ruff check --fix
|
||||
|
||||
check:
|
||||
uv run ruff format --check
|
||||
uv run ruff check
|
||||
uv run ty check
|
||||
{% if svc_modules %}
|
||||
build:
|
||||
$(COMPOSE) build {{ svc_modules[0] }}
|
||||
|
||||
pre-deploy:
|
||||
@if $(call changed,refs/deploy/backend,$(IMAGE_INPUTS)) || ! docker image inspect $(IMAGE) >/dev/null 2>&1; then \
|
||||
$(MAKE) build && $(call stamp,refs/deploy/backend) && $(call stamp,refs/deploy/backend-code); \
|
||||
fi
|
||||
{% if database == 'postgres' %} @$(COMPOSE) config --services | grep -qx postgres && $(COMPOSE) up -d postgres || true
|
||||
$(MAKE) migrate
|
||||
{% endif %}
|
||||
post-deploy:
|
||||
@if $(call changed,refs/deploy/backend-code,$(CODE)); then \
|
||||
running=$$($(COMPOSE) ps --services --status running | grep -x $(addprefix -e ,$(SERVICES))); \
|
||||
[ -z "$$running" ] || $(COMPOSE) restart $$running; \
|
||||
$(call stamp,refs/deploy/backend-code); \
|
||||
fi
|
||||
{% endif %}{% if layout == 'root' %}
|
||||
env:
|
||||
@test -f .env || cp .env.example .env
|
||||
@test -f docker-compose.override.yml || cp docker-compose.override.yml.example docker-compose.override.yml
|
||||
|
||||
recreate:
|
||||
$(COMPOSE) up -d --force-recreate
|
||||
|
||||
rebuild: build
|
||||
$(COMPOSE) up -d
|
||||
|
||||
restart:
|
||||
$(COMPOSE) restart
|
||||
|
||||
logs:
|
||||
$(COMPOSE) logs -f --tail=100
|
||||
|
||||
down:
|
||||
$(COMPOSE) down
|
||||
{% if svc_modules %}
|
||||
deploy: pre-deploy
|
||||
$(COMPOSE) up -d
|
||||
$(MAKE) post-deploy
|
||||
{% endif %}{% endif %}{% if database == 'postgres' %}
|
||||
migrate:
|
||||
$(COMPOSE) run --rm migrator $(or $(filter-out $@,$(MAKECMDGOALS)),upgrade head)
|
||||
|
||||
revision:
|
||||
$(COMPOSE) run --rm migrator revision --autogenerate -m "$(m)"
|
||||
{% endif %}{% if has_script %}
|
||||
script:
|
||||
@$(COMPOSE) run --rm {{ svc_modules[0] }} scripts.$(word 2,$(MAKECMDGOALS)) $(wordlist 3,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
{% endif %}{% if database == 'postgres' or has_script %}
|
||||
%:
|
||||
@:
|
||||
{% endif %}{% if 'kurigram_userbot' in backend_services %}
|
||||
session:
|
||||
uv run python scripts/session/create.py
|
||||
{% 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 %}
|
||||
+36
-14
@@ -2,6 +2,10 @@
|
||||
{%- set svc_networks = [] -%}
|
||||
{%- if database != 'none' %}{% set _ = svc_networks.append('database') %}{% 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 = [] -%}
|
||||
{%- 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 -%}
|
||||
@@ -9,25 +13,31 @@
|
||||
{%- if 'kurigram_userbot' in backend_services %}{% set _ = app_services.append({'name': 'userbot', 'command': '[userbot]'}) %}{% endif -%}
|
||||
{%- if 'scheduler' in backend_services %}{% set _ = app_services.append({'name': 'scheduler', 'command': ('[worker, scheduler]' if 'taskiq_worker' in backend_services else '[scheduler]')}) %}{% endif -%}
|
||||
{%- macro backend_service(name, command) %} {{ name }}:
|
||||
build: backend
|
||||
build: {{ part_dir }}
|
||||
image: {{ project_slug }}/backend
|
||||
profiles: [{{ name }}, services]
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
- path: backend/.env
|
||||
{% if layout == 'part' %} - path: backend/.env
|
||||
required: false
|
||||
environment:
|
||||
{% endif %} environment:
|
||||
RUN_ENVIRONMENT: prod
|
||||
TZ: ${TZ:-UTC}
|
||||
volumes:
|
||||
- ./backend/src:/app/src
|
||||
{% if has_scripts %} - ./backend/scripts:/app/scripts
|
||||
{% endif %}{% if name == 'userbot' %} - ./backend/sessions:/app/sessions
|
||||
- ./{{ prefix }}src:/app/src
|
||||
{% if has_scripts %} - ./{{ prefix }}scripts:/app/scripts
|
||||
{% endif %}{% if name == 'userbot' %} - ./{{ prefix }}sessions:/app/sessions
|
||||
{% endif %}{% if name == 'api' and serve_spa %} - ./frontend/build:/app/static:ro
|
||||
{% endif %} command: {{ command }}
|
||||
{% if svc_networks %} networks:
|
||||
{% for net in svc_networks %} {{ net }}:
|
||||
{% if external_services %} depends_on:
|
||||
{% 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 -%}
|
||||
services:
|
||||
{% 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}
|
||||
volumes:
|
||||
- database:/data/db
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--quiet", "--port", "${DB__PORT:-27017}", "--eval", "db.adminCommand('ping')"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
database:
|
||||
aliases:
|
||||
@@ -70,25 +85,27 @@ services:
|
||||
- ${DB__HOST:-postgres}
|
||||
|
||||
migrator:
|
||||
build: backend
|
||||
build: {{ part_dir }}
|
||||
image: {{ project_slug }}/backend
|
||||
profiles: [migrate]
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
- path: backend/.env
|
||||
{% if layout == 'part' %} - path: backend/.env
|
||||
required: false
|
||||
environment:
|
||||
{% endif %} environment:
|
||||
RUN_ENVIRONMENT: prod
|
||||
TZ: ${TZ:-UTC}
|
||||
volumes:
|
||||
- ./backend/src:/app/src
|
||||
- ./backend/migrations:/app/migrations
|
||||
- ./backend/alembic.ini:/app/alembic.ini
|
||||
- ./{{ prefix }}src:/app/src
|
||||
- ./{{ prefix }}migrations:/app/migrations
|
||||
- ./{{ prefix }}alembic.ini:/app/alembic.ini
|
||||
entrypoint: [alembic]
|
||||
command: [upgrade, head]
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
required: false
|
||||
networks:
|
||||
database:
|
||||
{% endif %}
|
||||
@@ -98,6 +115,11 @@ services:
|
||||
profiles: [redis, external]
|
||||
restart: unless-stopped
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = deps.append("aiogram>=3.29.0") %}{% endif -%}
|
||||
{%- if include_payments %}{% set _ = deps.append("aiosend>=3.0.7") %}{% endif -%}
|
||||
{%- if 'kurigram_userbot' in backend_services %}{% set _ = deps.append("anyio>=4.0.0") %}{% set _ = deps.append("kurigram>=2.2.7") %}{% set _ = deps.append("tgcrypto>=1.2.5") %}{% set _ = deps.append("uvloop>=0.21.0") %}{% endif -%}
|
||||
{%- if 'taskiq_worker' in backend_services %}{% set _ = deps.append("taskiq>=0.12.4") %}{% if taskiq_broker == 'redis' %}{% set _ = deps.append("taskiq-redis>=1.2.3") %}{% endif %}{% endif -%}
|
||||
{%- if 'taskiq_worker' in backend_services %}{% set _ = deps.append("taskiq>=0.12.4") %}{% if taskiq_broker == 'redis' %}{% set _ = deps.append("taskiq-redis>=1.2.3") %}{% endif %}{% if taskiq_broker == 'postgres' %}{% set _ = deps.append("taskiq-pg>=0.2.0") %}{% endif %}{% endif -%}
|
||||
{%- if 'scheduler' in backend_services %}{% set _ = deps.append("apscheduler>=3.11.0") %}{% endif -%}
|
||||
{%- if database == 'mongo' %}{% set _ = deps.append("beanie>=2.0.1") %}{% endif -%}
|
||||
{%- if database == 'postgres' %}{% set _ = deps.append("sqlmodel>=0.0.38") %}{% set _ = deps.append("asyncpg>=0.31.0") %}{% set _ = deps.append("alembic>=1.18.4") %}{% set _ = deps.append("greenlet>=3.1.0") %}{% endif -%}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+5
-1
@@ -69,6 +69,7 @@ class ApiSettings(BaseSettings):
|
||||
host: str = "0.0.0.0" # noqa: S104
|
||||
port: int = 8080
|
||||
workers: int = 1
|
||||
docs: bool = False
|
||||
{%- if serve_spa %}
|
||||
static_dir: str = "static"
|
||||
{%- endif %}
|
||||
@@ -110,7 +111,10 @@ class Settings(BaseSettings):
|
||||
{%- endif %}
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+17
@@ -0,0 +1,17 @@
|
||||
import uvicorn
|
||||
|
||||
from utils.env import env
|
||||
|
||||
|
||||
def main() -> None:
|
||||
uvicorn.run(
|
||||
"api.app:app",
|
||||
host=env.api.host,
|
||||
port=env.api.port,
|
||||
workers=env.api.workers,
|
||||
forwarded_allow_ips="*",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+7
-3
@@ -10,8 +10,8 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from api import routers
|
||||
{% if use_dishka %}from dependencies.container import container
|
||||
{% endif %}{% if database != 'none' %}from utils.db import init_db
|
||||
{% endif %}{% if serve_spa %}from utils.env import env
|
||||
{% endif %}from utils.logging import setup_logging
|
||||
{% endif %}from utils.env import env
|
||||
from utils.logging import setup_logging
|
||||
{% if serve_spa %}
|
||||
IMMUTABLE_HEADERS = {"Cache-Control": "public, max-age=31536000, immutable"}
|
||||
NO_CACHE_HEADERS = {"Cache-Control": "no-cache"}
|
||||
@@ -29,7 +29,11 @@ async def lifespan(app_: FastAPI) -> AsyncGenerator[None]:
|
||||
{%- 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(
|
||||
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
@@ -0,0 +1,3 @@
|
||||
from .modules.client import DEVICE_MODEL, PyroClient
|
||||
|
||||
__all__ = ["DEVICE_MODEL", "PyroClient"]
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+14
-4
@@ -1,20 +1,30 @@
|
||||
from pyrogram import Client, enums
|
||||
|
||||
DEVICE_MODEL = "{{ project_name[:32] }}"
|
||||
|
||||
|
||||
class PyroClient(Client):
|
||||
def __init__(
|
||||
self, name: str, *, workdir: str = "sessions", load_handlers: bool = True
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
workdir: str = "sessions",
|
||||
device_model: str | None = None,
|
||||
load_handlers: bool = True,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name,
|
||||
workdir=workdir,
|
||||
api_id=2040,
|
||||
api_hash="b18441a1ff607e10a989891a5462e627",
|
||||
device_model="Desktop",
|
||||
device_model=device_model or DEVICE_MODEL,
|
||||
system_version="Windows 11 x64",
|
||||
app_version="6.2.4 x64",
|
||||
app_version="7.0.8 x64",
|
||||
lang_pack="tdesktop",
|
||||
lang_code="en",
|
||||
system_lang_code="en-US",
|
||||
client_platform=enums.ClientPlatform.DESKTOP,
|
||||
max_concurrent_transmissions=4,
|
||||
)
|
||||
|
||||
if load_handlers:
|
||||
@@ -24,4 +34,4 @@ class PyroClient(Client):
|
||||
self.add_handler(*handler)
|
||||
|
||||
|
||||
__all__ = ["PyroClient"]
|
||||
__all__ = ["DEVICE_MODEL", "PyroClient"]
|
||||
+11
-5
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import signal
|
||||
|
||||
import anyio
|
||||
import uvloop
|
||||
@@ -11,8 +12,15 @@ import uvloop
|
||||
|
||||
setup_logging()
|
||||
|
||||
SCAN_INTERVAL = 10
|
||||
|
||||
|
||||
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()
|
||||
|
||||
{% endif %} sessions_dir = anyio.Path("sessions")
|
||||
@@ -22,7 +30,7 @@ async def runner() -> None:
|
||||
clients: dict[int, PyroClient] = {}
|
||||
|
||||
try:
|
||||
while True:
|
||||
while not stop.is_set():
|
||||
current = {path.name async for path in sessions_dir.glob("*.session")}
|
||||
|
||||
for session_file in current - started:
|
||||
@@ -43,7 +51,8 @@ async def runner() -> None:
|
||||
if me:
|
||||
clients[me.id] = client
|
||||
|
||||
await asyncio.sleep(10)
|
||||
with contextlib.suppress(TimeoutError):
|
||||
await asyncio.wait_for(stop.wait(), SCAN_INTERVAL)
|
||||
finally:
|
||||
for client in clients.values():
|
||||
with contextlib.suppress(Exception):
|
||||
@@ -55,8 +64,5 @@ def main() -> None:
|
||||
uvloop.install()
|
||||
|
||||
logger.info("Starting...")
|
||||
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
asyncio.run(runner())
|
||||
|
||||
logger.info("[red]Stopped.[/]")
|
||||
+7
-4
@@ -1,5 +1,5 @@
|
||||
import asyncio
|
||||
import contextlib
|
||||
import signal
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
@@ -15,6 +15,10 @@ async def example_job() -> 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()
|
||||
{%- endif %}
|
||||
@@ -27,14 +31,13 @@ async def runner() -> None:
|
||||
scheduler.start()
|
||||
logger.info("Scheduler started")
|
||||
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await asyncio.Event().wait()
|
||||
await stop.wait()
|
||||
scheduler.shutdown()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
setup_logging()
|
||||
|
||||
logger.info("Starting...")
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
asyncio.run(runner())
|
||||
logger.info("[red]Stopped.[/]")
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+16
@@ -39,6 +39,22 @@ broker = ResilientListQueueBroker(env.redis.url).with_result_backend(
|
||||
{% endif %}{% if use_dishka %}
|
||||
setup_dishka(container, broker)
|
||||
{% endif %}
|
||||
{%- elif taskiq_broker == 'postgres' -%}
|
||||
{% if use_dishka %}from dishka.integrations.taskiq import setup_dishka
|
||||
{% endif %}{% if 'scheduler' in backend_services %}from taskiq import TaskiqScheduler
|
||||
from taskiq.schedule_sources import LabelScheduleSource
|
||||
{% endif %}from taskiq_pg import AsyncpgBroker, AsyncpgResultBackend
|
||||
|
||||
{% if use_dishka %}from dependencies.container import container
|
||||
{% endif %}from utils.env import env
|
||||
|
||||
broker = AsyncpgBroker(env.db.connection_url).with_result_backend(
|
||||
AsyncpgResultBackend(env.db.connection_url)
|
||||
)
|
||||
{% if 'scheduler' in backend_services %}scheduler = TaskiqScheduler(broker, sources=[LabelScheduleSource(broker)])
|
||||
{% endif %}{% if use_dishka %}
|
||||
setup_dishka(container, broker)
|
||||
{% endif %}
|
||||
{%- else -%}
|
||||
{% if use_dishka %}from dishka.integrations.taskiq import setup_dishka
|
||||
{% endif %}from taskiq import InMemoryBroker{% if 'scheduler' in backend_services %}, TaskiqScheduler
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,20 @@
|
||||
{%- set targets = [] -%}
|
||||
{%- if database == 'postgres' %}{% set _ = targets.append("`make migrate` (`make migrate downgrade -1` to roll back), `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.
|
||||
|
||||
{% if backend_services %}To deploy, `git pull && make deploy`. It runs `make pre-deploy`, which builds the image only when `pyproject.toml`, `uv.lock` or `Dockerfile` changed{% if database == 'postgres' %} and applies migrations{% endif %}, then `docker compose up -d`, then `make post-deploy`, which restarts the services whose mounted code changed. A deploy system with its own `up` step calls the two hooks around it. What was built and started is remembered as `refs/deploy/*` in the clone.
|
||||
|
||||
{% endif %}`make fmt` formats, `make check` runs ruff and ty.{% if targets %} Also {{ targets | join(", ") }}.{% endif %}
|
||||
Reference in new issue
Block a user