Improve template (#1)

- Created a README file
- Added PyCharm/IntelliJ IDE run configuration
- Squashed bugs related to:
  - Code style
  - Default Postgre database URL schema
- Updated project dependencies

Reviewed-on: #1
Co-authored-by: Ēriks K <git@72.lv>
Co-committed-by: Ēriks K <git@72.lv>
This commit is contained in:
Ēriks K 2025-04-05 13:15:41 +03:00 committed by eriks
parent 0f01088e4f
commit ef5da867a6
21 changed files with 210 additions and 46 deletions

View File

@ -176,3 +176,4 @@ dmypy.json
.pytype/ .pytype/
cython_debug/ cython_debug/
cookiecutter.zip cookiecutter.zip
envs.yml

8
{{ cookiecutter.project_slug }}/.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3 ({{ cookiecutter.project_slug }})" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3 ({{ cookiecutter.project_slug }})" project-jdk-type="Python SDK" />
</project>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/{{ cookiecutter.project_slug }}.iml" filepath="$PROJECT_DIR$/.idea/{{ cookiecutter.project_slug }}.iml" />
</modules>
</component>
</project>

View File

@ -0,0 +1,26 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Create migration" type="PythonConfigurationType" factoryName="Python">
<module name="{{ cookiecutter.project_slug }}" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3 ({{ cookiecutter.project_slug }})" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/venv/bin/aerich" />
<option name="PARAMETERS" value="migrate" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run" type="Python.FastAPI">
<option name="file" value="$PROJECT_DIR$/service/api/main.py" />
<module name="{{ cookiecutter.project_slug }}" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="$PROJECT_DIR$/venv/bin/python" />
<option name="SDK_NAME" value="Python 3 ({{ cookiecutter.project_slug }})" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="launchJavascriptDebuger" value="false" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,26 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Migrations" type="PythonConfigurationType" factoryName="Python">
<module name="{{ cookiecutter.project_slug }}" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="SDK_NAME" value="Python 3 ({{ cookiecutter.project_slug }})" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="false" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/venv/bin/aerich" />
<option name="PARAMETERS" value="upgrade" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (project_slug)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

View File

@ -1,13 +1,13 @@
FROM python:3.12-alpine as python FROM python:3.12-alpine AS python
WORKDIR /app WORKDIR /app
ENV PYTHONUNBUFFERED 1 ARG ENVIRONMENT=local
ENV PYTHONUNBUFFERED=1
ENV TZ="Europe/Riga" ENV TZ="Europe/Riga"
COPY requirements /app/requirements COPY requirements /app/requirements
RUN apk add libpq \ RUN apk add libpq \
poppler-utils zlib-dev \ poppler-utils zlib-dev \
# curl \ && pip install --no-cache-dir --upgrade -r requirements/${ENVIRONMENT}.txt \
&& pip install --no-cache-dir --upgrade -r requirements/production.txt \
&& rm -rf /var/cache/apk/* && rm -rf /var/cache/apk/*
COPY entrypoint.sh /entrypoint COPY entrypoint.sh /entrypoint
@ -19,4 +19,4 @@ RUN chmod +x /entrypoint \
EXPOSE 5000 EXPOSE 5000
ENTRYPOINT ["/entrypoint"] ENTRYPOINT ["/entrypoint"]
CMD ["uvicorn", "--workers", "2", "--proxy-headers", "--host", "0.0.0.0", "--port", "5000", "--forwarded-allow-ips=*", "service.main:app"] CMD ["uvicorn", "--workers", "2", "--proxy-headers", "--host", "0.0.0.0", "--port", "5000", "--forwarded-allow-ips=*", "service.api.main:app"]

View File

@ -48,7 +48,7 @@
# `SERVICE_DATABASE_URL` # `SERVICE_DATABASE_URL`
*Optional*, default value: `psql://{{ cookiecutter.project_slug }}:{{ cookiecutter.project_slug }}@postgres:5432/{{ cookiecutter.project_slug }}` *Optional*, default value: `asyncpg://{{ cookiecutter.project_slug }}:{{ cookiecutter.project_slug }}@postgres:5432/{{ cookiecutter.project_slug }}`
# `SERVICE_TIMEZONE` # `SERVICE_TIMEZONE`

View File

@ -0,0 +1,37 @@
# Welcome to {{ cookiecutter.project_name }}
## Setup
### 1. Install dependencies
#### Local virtual environment
```shell
python3 -m venv venv
source venv/bin/activate
pip install -r requirements/local.txt
```
#### Docker image
```shell
docker build -f Dockerfile --tag {{ cookiecutter.project_slug }} .
```
### 2. Initial setup
#### Environment variables
Environment variables for the project are being read from the env.yml file:
- Copy example yml:
`cp envs.example.yml envs.yml`
- Adjust values as necessary
#### Initialize database
```shell
aerich init-db
```
#### Migrate database
```shell
aerich upgrade
```
#### Create new database migration
```shell
aerich migrate
```

View File

@ -7,13 +7,13 @@ service:
token_expiration_days: 28 token_expiration_days: 28
cors_origins: cors_origins:
- "http://localhost:5000" - "http://localhost:5000"
- "https://example.com" - "https://{{ cookiecutter.project_slug }}.xyz"
environment: "local" environment: "local"
log_level: "DEBUG" log_level: "DEBUG"
redis_url: "redis://redis:6379" redis_url: "redis://redis:6379"
database_url: "psql://{{ cookiecutter.project_slug }}:{{ cookiecutter.project_slug }}@postgres:5432/{{ cookiecutter.project_slug }}" database_url: "asyncpg://{{ cookiecutter.project_slug }}:{{ cookiecutter.project_slug }}@postgres:5432/{{ cookiecutter.project_slug }}"
timezone: "UTC" timezone: "UTC"
sentry_url: null sentry_url: null

View File

@ -1,26 +1,26 @@
fastapi==0.115.5 # https://github.com/tiangolo/fastapi fastapi==0.115.12 # https://github.com/tiangolo/fastapi
uvicorn==0.32.1 # https://pypi.org/project/uvicorn/ uvicorn==0.34.0 # https://pypi.org/project/uvicorn/
pydantic[email]==2.10.2 # https://github.com/pydantic/pydantic pydantic[email]==2.11.2 # https://github.com/pydantic/pydantic
pydantic-settings[yaml]==2.6.1 # https://github.com/pydantic/pydantic-settings/ pydantic-settings[yaml]==2.8.1 # https://github.com/pydantic/pydantic-settings/
tortoise-orm[accel,asyncpg]==0.22.1 # https://pypi.org/project/tortoise-orm/ tortoise-orm[accel,asyncpg]==0.24.2 # https://pypi.org/project/tortoise-orm/
aerich==0.7.2 # https://pypi.org/project/aerich/ aerich==0.8.2 # https://pypi.org/project/aerich/
setech==1.4.2 # https://pypi.org/project/setech/ setech==1.4.2 # https://pypi.org/project/setech/
python-multipart==0.0.18 # https://pypi.org/project/python-multipart/ python-multipart==0.0.20 # https://pypi.org/project/python-multipart/
email-validator==2.2.0 # https://pypi.org/project/email-validator/ email-validator==2.2.0 # https://pypi.org/project/email-validator/
tenacity==9.0.0 # https://pypi.org/project/tenacity/ tenacity==9.1.2 # https://pypi.org/project/tenacity/
pydantic==2.10.2 # https://pypi.org/project/pydantic/ pydantic==2.11.2 # https://pypi.org/project/pydantic/
#emails==0.6 # https://pypi.org/project/emails/ #emails==0.6 # https://pypi.org/project/emails/
python-jose[cryptography]==3.3 # https://pypi.org/project/python-jose/ python-jose[cryptography]==3.4.0 # https://pypi.org/project/python-jose/
passlib[bcrypt]==1.7.4 # https://pypi.org/project/passlib/ passlib[bcrypt]==1.7.4 # https://pypi.org/project/passlib/
bcrypt==4.2.1 # https://pypi.org/project/bcrypt/ bcrypt==4.3.0 # https://pypi.org/project/bcrypt/
# Pin bcrypt until passlib supports the latest # Pin bcrypt until passlib supports the latest
pydantic-settings==2.6.1 # https://pypi.org/project/pydantic-settings/ pydantic-settings==2.8.1 # https://pypi.org/project/pydantic-settings/
asyncio==3.4.3 asyncio==3.4.3

View File

@ -1,25 +1,25 @@
-r base.txt -r base.txt
uvicorn[standard]==0.32.1 # https://pypi.org/project/uvicorn/ uvicorn[standard]==0.34.0 # https://pypi.org/project/uvicorn/
black==24.10.0 # https://pypi.org/project/black/ black==25.1.0 # https://pypi.org/project/black/
isort==5.13.2 # https://pypi.org/project/isort/ isort==6.0.1 # https://pypi.org/project/isort/
pur==7.3.2 # https://pypi.org/project/pur/ pur==7.3.3 # https://pypi.org/project/pur/
pre-commit==4.0.1 # https://pypi.org/project/pre-commit/ pre-commit==4.2.0 # https://pypi.org/project/pre-commit/
flake8==7.1.1 flake8==7.2.0
pytest==8.3.3 # https://pypi.org/project/pytest/ pytest==8.3.5 # https://pypi.org/project/pytest/
coverage==7.6.8 # https://pypi.org/project/coverage/ coverage==7.8.0 # https://pypi.org/project/coverage/
mypy==1.13.0 # https://pypi.org/project/mypy/ mypy==1.15.0 # https://pypi.org/project/mypy/
types-python-jose==3.3.4.20240106 # https://pypi.org/project/types-python-jose/ types-python-jose==3.4.0.20250224 # https://pypi.org/project/types-python-jose/
types-passlib==1.7.7.20240819 # https://pypi.org/project/types-passlib/ types-passlib==1.7.7.20250401 # https://pypi.org/project/types-passlib/
types-PyYAML==6.0.12.20240917 types-PyYAML==6.0.12.20250402
types-Pygments==2.18.0.20240506 types-Pygments==2.19.0.20250305
types-colorama==0.4.15.20240311 types-colorama==0.4.15.20240311
types-decorator==5.1.8.20240310 types-decorator==5.2.0.20250324
types-six==1.16.21.20241105 types-six==1.17.0.20250403
types-ujson==5.10.0.20240515 types-ujson==5.10.0.20250326
settings-doc==4.3.1 # https://github.com/radeklat/settings-doc settings-doc==4.3.2 # https://github.com/radeklat/settings-doc
ipython==8.30.0 ipython==9.0.2

View File

@ -1,3 +1,3 @@
-r base.txt -r base.txt
sentry-sdk[fastapi]==2.19.0 # https://pypi.org/project/sentry-sdk/ sentry-sdk[fastapi]==2.25.1 # https://pypi.org/project/sentry-sdk/

View File

@ -24,7 +24,7 @@ class ProjectSettings(ProjectBaseSettings):
# Background task config # Background task config
redis_url: RedisDsn = Field(RedisDsn(url="redis://redis:6379")) redis_url: RedisDsn = Field(RedisDsn(url="redis://redis:6379"))
database_url: str = Field("psql://{{ cookiecutter.project_slug }}:{{ cookiecutter.project_slug }}@postgres:5432/{{ cookiecutter.project_slug }}") database_url: str = Field("asyncpg://{{ cookiecutter.project_slug }}:{{ cookiecutter.project_slug }}@postgres:5432/{{ cookiecutter.project_slug }}")
# Various # Various
timezone: str = Field( timezone: str = Field(

View File

@ -1,9 +1,8 @@
from service.api.dependencies import QueryParams
from service.core.security import verify_password from service.core.security import verify_password
from service.crud.user.models import UserPublic, UserRegister
from service.crud.utils import order_queryset
from service.database.models import User from service.database.models import User
from ..utils import order_queryset
from ...api.dependencies import QueryParams
from .models import UserPublic, UserRegister
async def authenticate(*, username: str, password: str) -> User | None: async def authenticate(*, username: str, password: str) -> User | None:

View File

@ -4,5 +4,7 @@ from service.constants.types import PaginationParams
def order_queryset(qs: QuerySet, filters: PaginationParams, default: str) -> QuerySet: def order_queryset(qs: QuerySet, filters: PaginationParams, default: str) -> QuerySet:
ordering = None
if filters.order:
ordering = [f for f in filters.order.split(",") if f.split("-")[-1] in qs.fields] ordering = [f for f in filters.order.split(",") if f.split("-")[-1] in qs.fields]
return qs.order_by(*(ordering or (default, ))) return qs.order_by(*(ordering or (default, )))