Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c19cdc4e11 | ||
|
|
97fdafa092 | ||
|
|
4d2ed061ea | ||
|
|
85ce05a55e | ||
|
|
47debebc0c |
No files matched your search
@@ -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.
|
||||
|
||||
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.
|
||||
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` (ruff, ty and `scripts/check_imports.py`, which keeps each service from importing the others' SDKs at startup), `build` and service-specific targets, `.env.example` and `CLAUDE.md`. Applied on top of the `infra` template, `layout: parts` or `layout: single` respectively.
|
||||
|
||||
`DB__HOST`/`DB__PORT` and `REDIS__HOST`/`REDIS__PORT` say where the service really is. The bundled container takes the host as its network alias and listens on the port, the override publishes it on the machine. For an external database, point them at it and leave its profile out of `COMPOSE_PROFILES`.
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ check:
|
||||
uv run ruff format --check
|
||||
uv run ruff check
|
||||
uv run ty check
|
||||
{% if svc_modules %}
|
||||
{% if svc_modules %} uv run python scripts/check_imports.py
|
||||
{% endif %}{% if svc_modules %}
|
||||
build:
|
||||
$(COMPOSE) build {{ svc_modules[0] }}
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import asyncio
|
||||
|
||||
import anyio
|
||||
|
||||
from userbot import PyroClient
|
||||
from userbot.modules.client import PyroClient
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HEAVY = ["aiogram", "pyrogram", "fastapi", "google.genai"]
|
||||
SERVICES = {
|
||||
{%- if 'api' in backend_services %}
|
||||
"api": (["api.app"], {"fastapi"}),
|
||||
{%- endif %}
|
||||
{%- if 'aiogram_bot' in backend_services %}
|
||||
"bot": ({% if include_payments %}["bot"]{% else %}["bot.common", "bot.handlers"]{% endif %}, {"aiogram"}),
|
||||
{%- endif %}
|
||||
{%- if 'kurigram_userbot' in backend_services %}
|
||||
"userbot": (["userbot.runner", "userbot.handlers"], {"pyrogram"}),
|
||||
{%- endif %}
|
||||
{%- if 'taskiq_worker' in backend_services %}
|
||||
"worker": (["worker.broker", "worker.tasks"], {% if 'api' in backend_services %}{"fastapi"}{% else %}set(){% endif %}),
|
||||
{%- elif 'scheduler' in backend_services %}
|
||||
"scheduler": (["scheduler"], set()),
|
||||
{%- endif %}
|
||||
}
|
||||
PROBE = """
|
||||
import importlib, sys
|
||||
for module in sys.argv[1].split(","):
|
||||
importlib.import_module(module)
|
||||
print(" ".join(name for name in sys.argv[2:] if name in sys.modules))
|
||||
"""
|
||||
{%- if 'aiogram_bot' in backend_services %}
|
||||
ENV = {**os.environ, "BOT__TOKEN": "1:check"}
|
||||
{%- else %}
|
||||
ENV = dict(os.environ)
|
||||
{%- endif %}
|
||||
|
||||
|
||||
def loaded(modules: list[str]) -> tuple[set[str], str]:
|
||||
result = subprocess.run( # noqa: S603
|
||||
[sys.executable, "-c", PROBE, ",".join(modules), *HEAVY],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd="src",
|
||||
env=ENV,
|
||||
check=False,
|
||||
)
|
||||
error = (result.stderr.strip().splitlines() or ["exit code"])[-1]
|
||||
return set(result.stdout.split()) & set(HEAVY), error if result.returncode else ""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
failed = False
|
||||
for service, (modules, allowed) in SERVICES.items():
|
||||
found, error = loaded(modules)
|
||||
extra = sorted(found - allowed)
|
||||
status = error or ("imports " + ", ".join(extra) if extra else "")
|
||||
print(f"{service:10s} {'FAIL: ' + status if status else 'ok'}")
|
||||
failed |= bool(status)
|
||||
return int(failed)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,6 @@
|
||||
import asyncio
|
||||
from importlib import import_module
|
||||
|
||||
|
||||
async def preload(module: str) -> None:
|
||||
await asyncio.to_thread(import_module, module)
|
||||
@@ -1,5 +1,7 @@
|
||||
import logging
|
||||
|
||||
{% if 'aiogram_bot' in backend_services %}from importlib.util import find_spec
|
||||
from pathlib import Path
|
||||
{% endif %}
|
||||
from rich.console import Console
|
||||
from rich.logging import RichHandler
|
||||
from rich.traceback import install
|
||||
@@ -10,8 +12,12 @@ console = Console(width=env.log.console_width, color_system="auto", force_termin
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
{% if 'aiogram_bot' in backend_services %} from aiogram.dispatcher import router # noqa: PLC0415
|
||||
|
||||
{% if 'aiogram_bot' in backend_services %} aiogram = find_spec("aiogram")
|
||||
suppress = (
|
||||
[str(Path(aiogram.origin).parent / "dispatcher")]
|
||||
if aiogram and aiogram.origin
|
||||
else []
|
||||
)
|
||||
{% endif %} logging.basicConfig(
|
||||
level=env.log.level_external,
|
||||
format="",
|
||||
@@ -26,7 +32,7 @@ def setup_logging() -> None:
|
||||
omit_repeated_times=False,
|
||||
show_time=env.log.show_time,
|
||||
{%- if 'aiogram_bot' in backend_services %}
|
||||
tracebacks_suppress=[router],
|
||||
tracebacks_suppress=suppress,
|
||||
{%- endif %}
|
||||
)
|
||||
],
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
from .modules.client import DEVICE_MODEL, PyroClient
|
||||
|
||||
__all__ = ["DEVICE_MODEL", "PyroClient"]
|
||||
-4
@@ -4,11 +4,7 @@
|
||||
{%- if database == 'postgres' %}{% set _ = providers.append('RepositoryProvider()') %}{% endif -%}
|
||||
{%- if use_redis %}{% set _ = providers.append('RedisProvider()') %}{% endif -%}
|
||||
{%- if use_llm %}{% set _ = providers.append('LlmProvider()') %}{% endif -%}
|
||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = providers.append('AiogramProvider()') %}{% endif -%}
|
||||
{%- if 'api' in backend_services %}{% set _ = providers.append('FastapiProvider()') %}{% endif -%}
|
||||
{%- set third = ['from dishka import make_async_container'] -%}
|
||||
{%- if 'aiogram_bot' in backend_services %}{% set _ = third.append('from dishka.integrations.aiogram import AiogramProvider') %}{% endif -%}
|
||||
{%- if 'api' in backend_services %}{% set _ = third.append('from dishka.integrations.fastapi import FastapiProvider') %}{% endif -%}
|
||||
{%- set first = [] -%}
|
||||
{%- if dynamic_config %}{% set _ = first.append('from dependencies.providers.config import ConfigProvider') %}{% endif -%}
|
||||
{%- if database != 'none' %}{% set _ = first.append('from dependencies.providers.db import DbProvider') %}{% endif -%}
|
||||
|
||||
+9
-3
@@ -1,14 +1,20 @@
|
||||
from dishka import Provider, Scope, provide
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.models.google import GoogleModel, GoogleModelSettings
|
||||
from pydantic_ai.providers.google import GoogleProvider
|
||||
|
||||
from utils.env import env
|
||||
from utils.imports import preload
|
||||
|
||||
|
||||
class LlmProvider(Provider):
|
||||
@provide(scope=Scope.APP)
|
||||
def agent(self) -> Agent:
|
||||
async def agent(self) -> Agent:
|
||||
await preload("pydantic_ai.models.google")
|
||||
from pydantic_ai.models.google import ( # noqa: PLC0415
|
||||
GoogleModel,
|
||||
GoogleModelSettings,
|
||||
)
|
||||
from pydantic_ai.providers.google import GoogleProvider # noqa: PLC0415
|
||||
|
||||
model = GoogleModel(
|
||||
env.llm.model.split(":", 1)[-1],
|
||||
provider=GoogleProvider(api_key=env.llm.gemini_api_key.get_secret_value()),
|
||||
|
||||
@@ -5,3 +5,9 @@ ruff format
|
||||
ruff check --fix
|
||||
ty check
|
||||
```
|
||||
|
||||
## Imports
|
||||
Every service imports only what it needs to start. `make check` runs `scripts/check_imports.py`, which fails when a service loads a heavy package it is not allowed; `SERVICES` there is the contract, extend it on purpose.
|
||||
- Package `__init__` files don't re-export clients or SDKs.
|
||||
- An SDK used by one function is imported inside it. On an async path load it first with `await preload("<module>")` from `utils.imports`, so the event loop never waits on the import.
|
||||
- Types used in Dishka provider signatures and `FromDishka[...]` stay real top-level imports.
|
||||
Reference in new issue
Block a user