Initial commit
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
### Python template
|
||||
|
||||
deploy/
|
||||
.idea/
|
||||
.vscode/
|
||||
.git/
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
@@ -0,0 +1,31 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
tab_width = 4
|
||||
end_of_line = lf
|
||||
max_line_length = 88
|
||||
ij_visual_guides = 88
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{js,py,html}]
|
||||
charset = utf-8
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[.flake8]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.py]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
ij_python_from_import_parentheses_force_if_multiline = true
|
||||
@@ -0,0 +1,41 @@
|
||||
name: Testing zangramru
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
strategy:
|
||||
matrix:
|
||||
cmd:
|
||||
- ruff-format
|
||||
- ruff
|
||||
- mypy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Set up UV
|
||||
uses: astral-sh/setup-uv@v6
|
||||
- name: Install deps
|
||||
run: uv sync
|
||||
- name: Run lint check
|
||||
run: uv run pre-commit run -a ${{ matrix.cmd }}
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Create .env
|
||||
run: touch .env
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Update docker-compose
|
||||
uses: KengoTODA/actions-setup-docker-compose@v1
|
||||
with:
|
||||
version: "2.28.0"
|
||||
- name: run tests
|
||||
run: docker-compose run --rm api pytest -vv
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
### Python template
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
*.sqlite3
|
||||
*.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
|
||||
- id: ruff-format
|
||||
name: Format with Ruff
|
||||
entry: uv run ruff format
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
|
||||
- id: ruff
|
||||
name: Check with Ruff
|
||||
entry: uv run ruff
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
args: ["check", "zangramru", "tests", "--fix"]
|
||||
|
||||
- id: mypy
|
||||
name: Validate types with MyPy
|
||||
entry: uv run mypy
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
args:
|
||||
- "zangramru"
|
||||
- "tests"
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
FROM ghcr.io/astral-sh/uv:0.11.7-python3.13-trixie AS uv
|
||||
|
||||
# -----------------------------------
|
||||
# STAGE 1: prod stage
|
||||
# Only install main dependencies
|
||||
# -----------------------------------
|
||||
FROM python:3.13-slim-trixie AS prod
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
ENV UV_LINK_MODE=copy
|
||||
ENV UV_PROJECT_ENVIRONMENT=/usr/local
|
||||
ENV UV_PYTHON_DOWNLOADS=never
|
||||
ENV UV_NO_MANAGED_PYTHON=1
|
||||
ENV PICCOLO_CONF="zangramru.piccolo_conf"
|
||||
|
||||
WORKDIR /app/src
|
||||
|
||||
RUN --mount=from=uv,source=/usr/local/bin/uv,target=/bin/uv \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --locked --no-install-project --no-dev
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN --mount=from=uv,source=/usr/local/bin/uv,target=/bin/uv \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --no-dev
|
||||
|
||||
CMD ["/usr/local/bin/python", "-m", "zangramru"]
|
||||
|
||||
# -----------------------------------
|
||||
# STAGE 3: development build
|
||||
# Includes dev dependencies
|
||||
# -----------------------------------
|
||||
FROM prod AS dev
|
||||
|
||||
RUN --mount=from=uv,source=/usr/local/bin/uv,target=/bin/uv \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --all-groups
|
||||
@@ -0,0 +1,145 @@
|
||||
# zangramru
|
||||
|
||||
This project was generated using fastapi_template.
|
||||
|
||||
## UV
|
||||
|
||||
This project uses uv. It's a modern dependency management
|
||||
tool.
|
||||
|
||||
To run the project use this set of commands:
|
||||
|
||||
```bash
|
||||
uv sync --locked
|
||||
uv run -m zangramru
|
||||
```
|
||||
|
||||
This will start the server on the configured host.
|
||||
|
||||
You can find swagger documentation at `/api/docs`.
|
||||
|
||||
You can read more about uv here: https://docs.astral.sh/ruff/
|
||||
|
||||
## Docker
|
||||
|
||||
You can start the project with docker using this command:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
This command exposes the web application on port 8000, mounts current directory and enables autoreload.
|
||||
|
||||
But you have to rebuild image every time you modify `uv.lock` or `pyproject.toml` with this command:
|
||||
|
||||
```bash
|
||||
docker compose build
|
||||
```
|
||||
|
||||
## Project structure
|
||||
|
||||
```bash
|
||||
$ tree "zangramru"
|
||||
zangramru
|
||||
├── conftest.py # Fixtures for all tests.
|
||||
├── db # module contains db configurations
|
||||
│ ├── dao # Data Access Objects. Contains different classes to interact with database.
|
||||
│ └── models # Package contains different models for ORMs.
|
||||
├── __main__.py # Startup script. Starts uvicorn.
|
||||
├── services # Package for different external services such as rabbit or redis etc.
|
||||
├── settings.py # Main configuration settings for project.
|
||||
├── static # Static content.
|
||||
├── tests # Tests for project.
|
||||
└── web # Package contains web server. Handlers, startup config.
|
||||
├── api # Package with all handlers.
|
||||
│ └── router.py # Main router.
|
||||
├── application.py # FastAPI application configuration.
|
||||
└── lifespan.py # Contains actions to perform on startup and shutdown.
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
This application can be configured with environment variables.
|
||||
|
||||
You can create `.env` file in the root directory and place all
|
||||
environment variables here.
|
||||
|
||||
All environment variables should start with "ZANGRAMRU_" prefix.
|
||||
|
||||
For example if you see in your "zangramru/settings.py" a variable named like
|
||||
`random_parameter`, you should provide the "ZANGRAMRU_RANDOM_PARAMETER"
|
||||
variable to configure the value. This behaviour can be changed by overriding `env_prefix` property
|
||||
in `zangramru.settings.Settings.Config`.
|
||||
|
||||
An example of .env file:
|
||||
```bash
|
||||
ZANGRAMRU_RELOAD="True"
|
||||
ZANGRAMRU_PORT="8000"
|
||||
ZANGRAMRU_ENVIRONMENT="dev"
|
||||
```
|
||||
|
||||
You can read more about BaseSettings class here: https://pydantic-docs.helpmanual.io/usage/settings/
|
||||
|
||||
## Pre-commit
|
||||
|
||||
To install pre-commit simply run inside the shell:
|
||||
```bash
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
pre-commit is very useful to check your code before publishing it.
|
||||
It's configured using .pre-commit-config.yaml file.
|
||||
|
||||
By default it runs:
|
||||
* mypy (validates types);
|
||||
* ruff (spots possible bugs);
|
||||
|
||||
|
||||
You can read more about pre-commit here: https://pre-commit.com/
|
||||
|
||||
## Migrations
|
||||
|
||||
If you want to migrate your database, you should run following commands:
|
||||
```bash
|
||||
# You have to set a PICCOLO_CONF variable
|
||||
export PICCOLO_CONF="zangramru.piccolo_conf"
|
||||
# Now you can easily run migrations using
|
||||
piccolo migrations forwards all
|
||||
```
|
||||
|
||||
### Reverting migrations
|
||||
|
||||
If you want to revert migrations, you should run:
|
||||
```bash
|
||||
```
|
||||
|
||||
### Migration generation
|
||||
|
||||
To generate migrations you should run:
|
||||
```bash
|
||||
```
|
||||
|
||||
|
||||
## Running tests
|
||||
|
||||
If you want to run it in docker, simply run:
|
||||
|
||||
```bash
|
||||
docker compose run --build --rm api pytest -vv .
|
||||
docker compose down
|
||||
```
|
||||
|
||||
For running tests on your local machine.
|
||||
1. you need to start all aux services.
|
||||
|
||||
We can do so by using our `docker-compose.yaml` configuration. It already has everything we need.
|
||||
|
||||
```bash
|
||||
docker compose up -d --wait db
|
||||
```
|
||||
|
||||
2. Run tests.
|
||||
```bash
|
||||
pytest -vv .
|
||||
```
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
services:
|
||||
api: &main_app
|
||||
build:
|
||||
context: .
|
||||
target: dev
|
||||
dockerfile: ./Dockerfile
|
||||
image: zangramru:${ZANGRAMRU_VERSION:-latest}
|
||||
restart: always
|
||||
ports:
|
||||
# Exposes application port.
|
||||
- "8000:8000"
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
migrator:
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
ZANGRAMRU_HOST: 0.0.0.0
|
||||
ZANGRAMRU_RELOAD: "True"
|
||||
ZANGRAMRU_DB_HOST: zangramru-db
|
||||
ZANGRAMRU_DB_PORT: 5432
|
||||
ZANGRAMRU_DB_USER: zangramru
|
||||
ZANGRAMRU_DB_PASS: zangramru
|
||||
ZANGRAMRU_DB_BASE: zangramru
|
||||
volumes:
|
||||
- .:/app/src/
|
||||
|
||||
db:
|
||||
image: postgres:18.3-trixie
|
||||
hostname: zangramru-db
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "zangramru"
|
||||
POSTGRES_USER: "zangramru"
|
||||
POSTGRES_DB: "zangramru"
|
||||
volumes:
|
||||
- zangramru-db-data:/var/lib/postgresql
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: pg_isready -U zangramru
|
||||
interval: 2s
|
||||
timeout: 3s
|
||||
retries: 40
|
||||
|
||||
migrator:
|
||||
<<: *main_app
|
||||
restart: "no"
|
||||
command: piccolo migrations forwards all
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
|
||||
|
||||
volumes:
|
||||
zangramru-db-data:
|
||||
name: zangramru-db-data
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
[project]
|
||||
name = "zangramru"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12,<4.0"
|
||||
description = ""
|
||||
authors = []
|
||||
maintainers = []
|
||||
readme = "README.md"
|
||||
dependencies = [
|
||||
"fastapi >=0.122.0,<1",
|
||||
"uvicorn[standard] >=0.38.0,<1",
|
||||
"pydantic >=2.12.5,<3",
|
||||
"pydantic-settings >=2.12.0,<3",
|
||||
"yarl >=1.22.0,<2",
|
||||
"ujson >=5.11.0,<6",
|
||||
"piccolo[postgres] >=1.30.0,<2",
|
||||
"httptools >=0.7.1,<1",
|
||||
"pymongo >=4.15.4,<5",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest >=9.0.1,<10",
|
||||
"ruff >=0.14.6,<1",
|
||||
"mypy >=1.19.0,<2",
|
||||
"pre-commit >=4.5.0,<5",
|
||||
"pytest-cov >=7.0.0,<8",
|
||||
"anyio >=4.11.0,<5",
|
||||
"pytest-env >=1.2.0,<2",
|
||||
"httpx >=0.28.1,<1",
|
||||
]
|
||||
|
||||
|
||||
[tool.uv]
|
||||
default-groups = ["dev"]
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
src_paths = ["zangramru",]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.13"
|
||||
strict = true
|
||||
ignore_missing_imports = true
|
||||
allow_subclassing_any = true
|
||||
allow_untyped_calls = true
|
||||
pretty = true
|
||||
show_error_codes = true
|
||||
implicit_reexport = true
|
||||
allow_untyped_decorators = true
|
||||
warn_unused_ignores = false
|
||||
warn_return_any = false
|
||||
namespace_packages = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-p no:unraisableexception"
|
||||
filterwarnings = [
|
||||
"error",
|
||||
"ignore::DeprecationWarning",
|
||||
"ignore:.*unclosed.*:ResourceWarning",
|
||||
]
|
||||
anyio_mode = "auto"
|
||||
env = [
|
||||
"ZANGRAMRU_ENVIRONMENT=pytest",
|
||||
"ZANGRAMRU_DB_BASE=zangramru_test",
|
||||
"PICCOLO_CONF=zangramru.piccolo_conf",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py313"
|
||||
# List of enabled rulsets.
|
||||
# See https://docs.astral.sh/ruff/rules/ for more information.
|
||||
lint.select = [ "E", # Error
|
||||
"F", # Pyflakes
|
||||
"W", # Pycodestyle
|
||||
"C90", # McCabe complexity
|
||||
"I", # Isort
|
||||
"N", # pep8-naming
|
||||
"D", # Pydocstyle
|
||||
"ANN", # Pytype annotations
|
||||
"S", # Bandit
|
||||
"B", # Bugbear
|
||||
"C4", # Comprehensions
|
||||
"ISC", # Implicit string concat
|
||||
"PIE", # Unnecessary code
|
||||
"T20", # Catch prints
|
||||
"PYI", # validate pyi files
|
||||
"Q", # Checks for quotes
|
||||
"RSE", # Checks raise statements
|
||||
"RET", # Checks return statements
|
||||
"SLF", # Self checks
|
||||
"SIM", # Simplificator
|
||||
"PTH", # Pathlib checks
|
||||
"ERA", # Checks for commented out code
|
||||
"PL", # PyLint checks
|
||||
"RUF", # Specific to Ruff checks
|
||||
"FA102", # Future annotations
|
||||
"UP", # Python updates
|
||||
]
|
||||
lint.ignore = [
|
||||
"D105", # Missing docstring in magic method
|
||||
"D107", # Missing docstring in __init__
|
||||
"B008", # Do not perform function calls in argument defaults
|
||||
"D211", # No blank lines allowed before class docstring
|
||||
"D212", # Multi-line docstring summary should start at the first line
|
||||
"D401", # First line should be in imperative mood
|
||||
"D104", # Missing docstring in public package
|
||||
"D100", # Missing docstring in public module
|
||||
"D202", # No blank lines allowed after function docstring
|
||||
"ANN401", # typing.Any are disallowed in `**kwargs
|
||||
"PLR0913", # Too many arguments for function call
|
||||
"D106", # Missing docstring in public nested class
|
||||
"UP043", # Unnecessary default argument specified (conflicts with mypy).
|
||||
]
|
||||
exclude = [
|
||||
"zangramru/db/migrations",
|
||||
".venv/"
|
||||
]
|
||||
lint.mccabe = { max-complexity = 10 }
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = [
|
||||
"S101", # Use of assert detected
|
||||
"PLC0415", # Import outside toplevel
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "pep257"
|
||||
ignore-decorators = ["typing.overload"]
|
||||
|
||||
[tool.ruff.lint.pylint]
|
||||
allow-magic-value-types = ["int", "str", "float", "bytes"]
|
||||
|
||||
[tool.ruff.lint.flake8-bugbear]
|
||||
extend-immutable-calls = [
|
||||
"fastapi.Depends",
|
||||
"fastapi.Query",
|
||||
]
|
||||
|
||||
[fastapi-template.options]
|
||||
project_name = "zangramru"
|
||||
api_type = "rest"
|
||||
enable_redis = "None"
|
||||
enable_rmq = "None"
|
||||
ci_type = "github"
|
||||
enable_migrations = "True"
|
||||
enable_taskiq = "None"
|
||||
enable_routers = "True"
|
||||
enable_kafka = "None"
|
||||
enable_nats = "None"
|
||||
enable_loguru = "None"
|
||||
traefik_labels = "None"
|
||||
add_dummy = "None"
|
||||
orm = "piccolo"
|
||||
self_hosted_swagger = "None"
|
||||
prometheus_enabled = "None"
|
||||
sentry_enabled = "None"
|
||||
otlp_enabled = "None"
|
||||
gunicorn = "None"
|
||||
add_users = "None"
|
||||
cookie_auth = "None"
|
||||
jwt_auth = "None"
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for zangramru."""
|
||||
@@ -0,0 +1,99 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from piccolo.conf.apps import Finder
|
||||
from piccolo.engine.postgres import PostgresEngine
|
||||
from piccolo.table import create_tables, drop_tables
|
||||
|
||||
from zangramru.settings import settings
|
||||
from zangramru.web.application import get_app
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def anyio_backend() -> str:
|
||||
"""
|
||||
Backend for anyio pytest plugin.
|
||||
|
||||
:return: backend name.
|
||||
"""
|
||||
return "asyncio"
|
||||
|
||||
|
||||
async def drop_database(engine: PostgresEngine) -> None:
|
||||
"""
|
||||
Drops test database.
|
||||
|
||||
:param engine: engine connected to postgres database.
|
||||
"""
|
||||
await engine.run_ddl(
|
||||
"SELECT pg_terminate_backend(pg_stat_activity.pid) " # noqa: S608
|
||||
"FROM pg_stat_activity "
|
||||
f"WHERE pg_stat_activity.datname = '{settings.db_base}' "
|
||||
"AND pid <> pg_backend_pid();",
|
||||
)
|
||||
await engine.run_ddl(
|
||||
f"DROP DATABASE {settings.db_base};",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_db() -> AsyncGenerator[None, None]:
|
||||
"""
|
||||
Fixture to create all tables before test and drop them after.
|
||||
|
||||
:yield: nothing.
|
||||
"""
|
||||
engine = PostgresEngine(
|
||||
config={
|
||||
"database": "postgres",
|
||||
"user": settings.db_user,
|
||||
"password": settings.db_pass,
|
||||
"host": settings.db_host,
|
||||
"port": settings.db_port,
|
||||
},
|
||||
)
|
||||
await engine.start_connection_pool()
|
||||
|
||||
db_exists = await engine.run_ddl(
|
||||
f"SELECT 1 FROM pg_database WHERE datname='{settings.db_base}'" # noqa: S608
|
||||
)
|
||||
if db_exists:
|
||||
await drop_database(engine)
|
||||
await engine.run_ddl(f"CREATE DATABASE {settings.db_base}")
|
||||
tables = Finder().get_table_classes()
|
||||
create_tables(*tables, if_not_exists=True)
|
||||
|
||||
yield
|
||||
|
||||
drop_tables(*tables)
|
||||
await drop_database(engine)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fastapi_app() -> FastAPI:
|
||||
"""
|
||||
Fixture for creating FastAPI app.
|
||||
|
||||
:return: fastapi app with mocked dependencies.
|
||||
"""
|
||||
application = get_app()
|
||||
return application # noqa: RET504
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(
|
||||
fastapi_app: FastAPI, anyio_backend: Any
|
||||
) -> AsyncGenerator[AsyncClient, None]:
|
||||
"""
|
||||
Fixture that creates client for requesting server.
|
||||
|
||||
:param fastapi_app: the application.
|
||||
:yield: client for the app.
|
||||
"""
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(fastapi_app), base_url="http://test", timeout=2.0
|
||||
) as ac:
|
||||
yield ac
|
||||
@@ -0,0 +1,19 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import FastAPI
|
||||
from httpx import AsyncClient
|
||||
from starlette import status
|
||||
|
||||
|
||||
async def test_echo(fastapi_app: FastAPI, client: AsyncClient) -> None:
|
||||
"""
|
||||
Tests that echo route works.
|
||||
|
||||
:param fastapi_app: current application.
|
||||
:param client: client for the app.
|
||||
"""
|
||||
url = fastapi_app.url_path_for("send_echo_message")
|
||||
message = uuid.uuid4().hex
|
||||
response = await client.post(url, json={"message": message})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["message"] == message
|
||||
@@ -0,0 +1,15 @@
|
||||
from fastapi import FastAPI
|
||||
from httpx import AsyncClient
|
||||
from starlette import status
|
||||
|
||||
|
||||
async def test_health(client: AsyncClient, fastapi_app: FastAPI) -> None:
|
||||
"""
|
||||
Checks the health endpoint.
|
||||
|
||||
:param client: client for the app.
|
||||
:param fastapi_app: current FastAPI application.
|
||||
"""
|
||||
url = fastapi_app.url_path_for("health_check")
|
||||
response = await client.get(url)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -0,0 +1 @@
|
||||
"""zangramru package."""
|
||||
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
|
||||
import uvicorn
|
||||
|
||||
from zangramru.settings import settings
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Entrypoint of the application."""
|
||||
os.environ["PICCOLO_CONF"] = "zangramru.piccolo_conf"
|
||||
uvicorn.run(
|
||||
"zangramru.web.application:get_app",
|
||||
workers=settings.workers_count,
|
||||
host=settings.host,
|
||||
port=settings.port,
|
||||
reload=settings.reload,
|
||||
log_level=settings.log_level.value.lower(),
|
||||
access_log=True,
|
||||
factory=True,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
from piccolo.conf.apps import AppConfig, table_finder
|
||||
|
||||
CURRENT_DIRECTORY = Path(__file__).parent
|
||||
|
||||
|
||||
APP_CONFIG = AppConfig(
|
||||
app_name="zangramru_db",
|
||||
migrations_folder_path=str(CURRENT_DIRECTORY / "migrations"),
|
||||
table_classes=table_finder(modules=[]),
|
||||
migration_dependencies=[],
|
||||
commands=[],
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""zangramru models."""
|
||||
@@ -0,0 +1,62 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from zangramru.settings import settings
|
||||
|
||||
|
||||
class InterceptHandler(logging.Handler):
|
||||
"""
|
||||
Default handler from examples in loguru documentation.
|
||||
|
||||
This handler intercepts all log requests and
|
||||
passes them to loguru.
|
||||
|
||||
For more info see:
|
||||
https://loguru.readthedocs.io/en/stable/overview.html#entirely-compatible-with-standard-logging
|
||||
"""
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None: # pragma: no cover
|
||||
"""
|
||||
Propagates logs to loguru.
|
||||
|
||||
:param record: record to log.
|
||||
"""
|
||||
try:
|
||||
level: str | int = logger.level(record.levelname).name
|
||||
except ValueError:
|
||||
level = record.levelno
|
||||
|
||||
# Find caller from where originated the logged message
|
||||
frame, depth = logging.currentframe(), 2
|
||||
while frame.f_code.co_filename == logging.__file__:
|
||||
frame = frame.f_back # type: ignore
|
||||
depth += 1
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(
|
||||
level,
|
||||
record.getMessage(),
|
||||
)
|
||||
|
||||
|
||||
def configure_logging() -> None: # pragma: no cover
|
||||
"""Configures logging."""
|
||||
intercept_handler = InterceptHandler()
|
||||
|
||||
logging.basicConfig(handlers=[intercept_handler], level=logging.NOTSET)
|
||||
|
||||
for logger_name in logging.root.manager.loggerDict:
|
||||
if logger_name.startswith("uvicorn."):
|
||||
logging.getLogger(logger_name).handlers = []
|
||||
|
||||
# change handler for default uvicorn logger
|
||||
logging.getLogger("uvicorn").handlers = [intercept_handler]
|
||||
logging.getLogger("uvicorn.access").handlers = [intercept_handler]
|
||||
|
||||
# set logs output, level and format
|
||||
logger.remove()
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level=settings.log_level.value,
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
from piccolo.conf.apps import AppRegistry
|
||||
from piccolo.engine.postgres import PostgresEngine
|
||||
|
||||
from zangramru.settings import settings
|
||||
|
||||
DB = PostgresEngine(
|
||||
config={
|
||||
"database": settings.db_base,
|
||||
"user": settings.db_user,
|
||||
"password": settings.db_pass,
|
||||
"host": settings.db_host,
|
||||
"port": settings.db_port,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
APP_REGISTRY = AppRegistry(apps=["zangramru.db.app_conf"])
|
||||
@@ -0,0 +1 @@
|
||||
"""Services for zangramru."""
|
||||
@@ -0,0 +1,72 @@
|
||||
import enum
|
||||
from pathlib import Path
|
||||
from tempfile import gettempdir
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from yarl import URL
|
||||
|
||||
TEMP_DIR = Path(gettempdir())
|
||||
|
||||
|
||||
class LogLevel(enum.StrEnum):
|
||||
"""Possible log levels."""
|
||||
|
||||
NOTSET = "NOTSET"
|
||||
DEBUG = "DEBUG"
|
||||
INFO = "INFO"
|
||||
WARNING = "WARNING"
|
||||
ERROR = "ERROR"
|
||||
FATAL = "FATAL"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""
|
||||
Application settings.
|
||||
|
||||
These parameters can be configured
|
||||
with environment variables.
|
||||
"""
|
||||
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 8000
|
||||
# quantity of workers for uvicorn
|
||||
workers_count: int = 1
|
||||
# Enable uvicorn reloading
|
||||
reload: bool = False
|
||||
|
||||
# Current environment
|
||||
environment: str = "dev"
|
||||
|
||||
log_level: LogLevel = LogLevel.INFO
|
||||
# Variables for the database
|
||||
db_host: str = "localhost"
|
||||
db_port: int = 5432
|
||||
db_user: str = "zangramru"
|
||||
db_pass: str = "zangramru" # noqa: S105
|
||||
db_base: str = "zangramru"
|
||||
db_echo: bool = False
|
||||
|
||||
@property
|
||||
def db_url(self) -> URL:
|
||||
"""
|
||||
Assemble database URL from settings.
|
||||
|
||||
:return: database URL.
|
||||
"""
|
||||
return URL.build(
|
||||
scheme="postgresql",
|
||||
host=self.db_host,
|
||||
port=self.db_port,
|
||||
user=self.db_user,
|
||||
password=self.db_pass,
|
||||
path=f"/{self.db_base}",
|
||||
)
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_prefix="ZANGRAMRU_",
|
||||
env_file_encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1 @@
|
||||
"""WEB API for zangramru."""
|
||||
@@ -0,0 +1 @@
|
||||
"""zangramru API package."""
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Echo API."""
|
||||
|
||||
from .views import router
|
||||
|
||||
__all__ = ["router"]
|
||||
@@ -0,0 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""Simple message model."""
|
||||
|
||||
message: str
|
||||
@@ -0,0 +1,18 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from zangramru.web.api.echo.schema import Message
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=Message)
|
||||
async def send_echo_message(
|
||||
incoming_message: Message,
|
||||
) -> Message:
|
||||
"""
|
||||
Sends echo back to user.
|
||||
|
||||
:param incoming_message: incoming message.
|
||||
:returns: message same as the incoming.
|
||||
"""
|
||||
return incoming_message
|
||||
@@ -0,0 +1,5 @@
|
||||
"""API for checking project status."""
|
||||
|
||||
from zangramru.web.api.monitoring.views import router
|
||||
|
||||
__all__ = ["router"]
|
||||
@@ -0,0 +1,12 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health_check() -> None:
|
||||
"""
|
||||
Checks the health of a project.
|
||||
|
||||
It returns 200 if the project is healthy.
|
||||
"""
|
||||
@@ -0,0 +1,7 @@
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from zangramru.web.api import echo, monitoring
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(monitoring.router)
|
||||
api_router.include_router(echo.router, prefix="/echo", tags=["echo"])
|
||||
@@ -0,0 +1,26 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from zangramru.web.api.router import api_router
|
||||
from zangramru.web.lifespan import lifespan_setup
|
||||
|
||||
|
||||
def get_app() -> FastAPI:
|
||||
"""
|
||||
Get FastAPI application.
|
||||
|
||||
This is the main constructor of an application.
|
||||
|
||||
:return: application.
|
||||
"""
|
||||
app = FastAPI(
|
||||
title="zangramru",
|
||||
lifespan=lifespan_setup,
|
||||
docs_url="/api/docs",
|
||||
redoc_url="/api/redoc",
|
||||
openapi_url="/api/openapi.json",
|
||||
)
|
||||
|
||||
# Main router for the API.
|
||||
app.include_router(router=api_router, prefix="/api")
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,24 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan_setup(
|
||||
app: FastAPI,
|
||||
) -> AsyncGenerator[None, None]: # pragma: no cover
|
||||
"""
|
||||
Actions to run on application startup.
|
||||
|
||||
This function uses fastAPI app to store data
|
||||
in the state, such as db_engine.
|
||||
|
||||
:param app: the fastAPI application.
|
||||
:return: function that actually performs actions.
|
||||
"""
|
||||
|
||||
app.middleware_stack = None
|
||||
app.middleware_stack = app.build_middleware_stack()
|
||||
|
||||
yield
|
||||
Reference in New Issue
Block a user