diff --git a/Makefile b/Makefile index 6075118..09f9d46 100644 --- a/Makefile +++ b/Makefile @@ -17,9 +17,14 @@ INFRA_INIT_SERVICES ?= # ----------------------------- # Internal vars / aliases # ----------------------------- -PYTHON_BIN := python DOCKER_COMPOSE := docker compose -p $(PROJECT_NAME) -DOCKER_COMPOSE_PRUNE := scripts/makefile/docker_prune.sh +PIP_AUDIT := scripts/makefile/pip_audit.sh +SLOTSCHECK := scripts/makefile/slotscheck.sh +DOCKER_ENV := scripts/makefile/docker_env.sh +LOCAL_ENV := scripts/makefile/local_env.sh +DOCKER_PRUNE := scripts/makefile/docker_prune.sh +PYCACHE_DEL := scripts/makefile/pycache_del.sh +DISHKA_PLOT_DATA := scripts/dishka/plot_dependencies_data.py # Test stack is isolated by project name TEST_PROJECT ?= $(PROJECT_NAME)-test @@ -52,17 +57,12 @@ PYTEST_ARGS_COV_DOCKER := \ # Safety .PHONY: pip-audit pip-audit: - tmp=$$(mktemp -d); trap 'rm -rf "$$tmp"' EXIT; \ - uv -qq export --format pylock.toml -o "$$tmp/pylock.toml"; \ - pip-audit --locked "$$tmp" \ - || echo "WARNING: pip-audit found vulnerabilities (non-blocking)" >&2 + $(PIP_AUDIT) # Code quality .PHONY: slotscheck lint test check slotscheck: - slotscheck $(SLOTSCHECK_TARGET) 2>&1 | tee /dev/stderr \ - | { grep -m1 "Failed to import" || true; } | cut -d"'" -f2 \ - | xargs -r -n1 $(PYTHON_BIN) -c 'import importlib,sys; importlib.import_module(sys.argv[1])' + $(SLOTSCHECK) src lint: ruff check --fix @@ -70,7 +70,7 @@ lint: tombi format tombi lint deptry - $(MAKE) slotscheck SLOTSCHECK_TARGET=src + $(MAKE) slotscheck lint-imports mypy @@ -85,33 +85,10 @@ check: lint test # Docker compose .PHONY: docker-env local-env upd up upd-local up-local down stop-all docker-env: - { \ - echo "# This .env file is generated automatically for DOCKER environment by Makefile."; \ - echo "# Do not edit it directly; edit env.example / .secrets and Makefile instead."; \ - echo; \ - cat env.example; \ - if [ -f .secrets ]; then \ - echo; \ - echo "# --- secrets from .secrets (not committed) ---"; \ - cat .secrets; \ - fi; \ - } > .env + $(DOCKER_ENV) local-env: - { \ - echo "# This .env file is generated automatically for LOCAL environment by Makefile."; \ - echo "# Do not edit it directly; edit env.example / .secrets and Makefile instead."; \ - echo; \ - sed \ - -e 's|^EXAMPLE_SERVICE_URL=.*|EXAMPLE_SERVICE_URL=http://127.0.0.1:51999|' \ - -e 's|^POSTGRES_HOST=.*|POSTGRES_HOST=127.0.0.1|' \ - env.example; \ - if [ -f .secrets ]; then \ - echo; \ - echo "# --- secrets from .secrets (not committed) ---"; \ - cat .secrets; \ - fi; \ - } > .env + $(LOCAL_ENV) upd: docker-env $(DOCKER_COMPOSE) up -d --build --force-recreate @@ -158,18 +135,15 @@ test-docker: docker-env .PHONY: prune prune: - $(DOCKER_COMPOSE_PRUNE) + $(DOCKER_PRUNE) # Project structure visualization .PHONY: pycache-del tree plot-data -PYCACHE_DEL := scripts/makefile/pycache_del.sh -DISHKA_PLOT_DATA := scripts/dishka/plot_dependencies_data.py - pycache-del: - @$(PYCACHE_DEL) + $(PYCACHE_DEL) tree: pycache-del - @tree + tree plot-data: - @$(PYTHON_BIN) $(DISHKA_PLOT_DATA) + APP_LOGGING_LEVEL=CRITICAL python $(DISHKA_PLOT_DATA) diff --git a/README.md b/README.md index 066eb82..b8e0301 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ Stay tuned. Refactor in progress, see [`legacy-2025`](https://github.com/ivan-borovets/fastapi-clean-example/tree/legacy-2025) branch for architecture docs TODO: +- [x] Write tests - [ ] Explain code and patterns in new README - [ ] Make template project diff --git a/scripts/dishka/plot_dependencies_data.py b/scripts/dishka/plot_dependencies_data.py index 67cd9d5..22e2152 100644 --- a/scripts/dishka/plot_dependencies_data.py +++ b/scripts/dishka/plot_dependencies_data.py @@ -1,24 +1,19 @@ import asyncio import dishka.plotter -from dishka import AsyncContainer from app.main.run import make_app -def generate_d2_dependency_graph(container: AsyncContainer) -> str: +async def main() -> None: """ Generates dependency graph for container in `d2` format. See https://d2lang.com for rendering instructions. """ - return dishka.plotter.render_d2(container) - - -async def main() -> None: app = make_app() container = app.state.dishka_container try: - print(generate_d2_dependency_graph(container)) + print(dishka.plotter.render_d2(container)) finally: await container.close() diff --git a/scripts/makefile/docker_env.sh b/scripts/makefile/docker_env.sh new file mode 100755 index 0000000..30eb3ce --- /dev/null +++ b/scripts/makefile/docker_env.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# The script is called from Makefile +set -eu -o pipefail + +{ + echo "# This .env file is generated automatically for DOCKER environment by Makefile." + echo "# Do not edit it directly; edit env.example / .secrets and Makefile instead." + echo + cat env.example + if [ -f .secrets ]; then + echo + echo "# --- secrets from .secrets (not committed) ---" + cat .secrets + fi +} > .env diff --git a/scripts/makefile/docker_prune.sh b/scripts/makefile/docker_prune.sh index 037e509..62ee614 100755 --- a/scripts/makefile/docker_prune.sh +++ b/scripts/makefile/docker_prune.sh @@ -1,5 +1,7 @@ #!/bin/bash # The script is called from Makefile +set -eu -o pipefail + echo "Warning: This will remove all unused containers, networks, images, and volumes." echo "Are you sure you want to continue? [y/N]" read -r response diff --git a/scripts/makefile/local_env.sh b/scripts/makefile/local_env.sh new file mode 100755 index 0000000..c93bfbe --- /dev/null +++ b/scripts/makefile/local_env.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# The script is called from Makefile +set -eu -o pipefail + +{ + echo "# This .env file is generated automatically for LOCAL environment by Makefile." + echo "# Do not edit it directly; edit env.example / .secrets and Makefile instead." + echo + sed \ + -e 's|^EXAMPLE_SERVICE_URL=.*|EXAMPLE_SERVICE_URL=http://127.0.0.1:51999|' \ + -e 's|^POSTGRES_HOST=.*|POSTGRES_HOST=127.0.0.1|' \ + env.example + if [ -f .secrets ]; then + echo + echo "# --- secrets from .secrets (not committed) ---" + cat .secrets + fi +} > .env diff --git a/scripts/makefile/pip_audit.sh b/scripts/makefile/pip_audit.sh new file mode 100755 index 0000000..7717fcb --- /dev/null +++ b/scripts/makefile/pip_audit.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# The script is called from Makefile +set -eu -o pipefail + +tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT +uv -qq export --format pylock.toml -o "$tmp/pylock.toml" +pip-audit --locked "$tmp" \ +|| echo "WARNING: pip-audit found vulnerabilities (non-blocking)" >&2 diff --git a/scripts/makefile/pycache_del.sh b/scripts/makefile/pycache_del.sh index cf70c76..25c705f 100755 --- a/scripts/makefile/pycache_del.sh +++ b/scripts/makefile/pycache_del.sh @@ -1,3 +1,6 @@ #!/bin/bash - find . -type d -name '__pycache__' -prune -exec rm -rf {} +; \ - find . -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete +# The script is called from Makefile +set -eu -o pipefail + +find . -type d -name '__pycache__' -prune -exec rm -rf {} + +find . -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete diff --git a/scripts/makefile/slotscheck.sh b/scripts/makefile/slotscheck.sh new file mode 100755 index 0000000..2cc83dd --- /dev/null +++ b/scripts/makefile/slotscheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# The script is called from Makefile +set -eu -o pipefail + +slotscheck "$1" 2>&1 | tee /dev/stderr \ +| { grep -m1 "Failed to import" || true; } | cut -d"'" -f2 \ +| xargs -r -n1 python -c 'import importlib,sys; importlib.import_module(sys.argv[1])' diff --git a/src/app/infrastructure/persistence_sqla/alembic/env.py b/src/app/infrastructure/persistence_sqla/alembic/env.py index face40d..ff9d8db 100644 --- a/src/app/infrastructure/persistence_sqla/alembic/env.py +++ b/src/app/infrastructure/persistence_sqla/alembic/env.py @@ -18,7 +18,7 @@ # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: - fileConfig(config.config_file_name) + fileConfig(config.config_file_name, disable_existing_loggers=False) # add your model's MetaData object here # for 'autogenerate' support