Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ repos:
- id: check-yaml

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.2
rev: v0.11.12
hooks:
- id: ruff-format

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.2
rev: v0.11.12
hooks:
- id: ruff
args: [--fix]
31 changes: 18 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
FROM python:3.13.3-slim-bookworm as base
FROM python:3.13.3-slim-bookworm AS base

ENV PYTHONUNBUFFERED 1
ENV PYTHONUNBUFFERED=1
WORKDIR /build

# Create requirements.txt file
FROM base as poetry
RUN pip install poetry==1.8.2
FROM base AS poetry
RUN pip install poetry==2.1.3
RUN poetry self add poetry-plugin-export
COPY poetry.lock pyproject.toml ./
RUN poetry export -o /requirements.txt --without-hashes

FROM base as common
FROM base AS final
COPY --from=poetry /requirements.txt .

# Create venv, add it to path and install requirements
RUN python -m venv /venv
ENV PATH="/venv/bin:$PATH"
Expand All @@ -26,12 +28,15 @@ COPY alembic.ini .
COPY pyproject.toml .
COPY init.sh .

# Create new user to run app process as unprivilaged user
RUN addgroup --gid 1001 --system uvicorn && \
adduser --gid 1001 --shell /bin/false --disabled-password --uid 1001 uvicorn
# Expose port
EXPOSE 8000

# Make the init script executable
RUN chmod +x ./init.sh

# Set ENTRYPOINT to always run init.sh
ENTRYPOINT ["./init.sh"]

# Run init.sh script then start uvicorn
RUN chown -R uvicorn:uvicorn /build
CMD bash init.sh && \
runuser -u uvicorn -- /venv/bin/uvicorn app.main:app --app-dir /build --host 0.0.0.0 --port 8000 --workers 2 --loop uvloop
EXPOSE 8000
# Set CMD to uvicorn
# /venv/bin/uvicorn is used because from entrypoint script PATH is new
CMD ["/venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2", "--loop", "uvloop"]
2 changes: 1 addition & 1 deletion alembic.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ script_location = alembic

# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
file_template = %%(year)d%%(month).2d%%(day).2d%%(minute).2d_%%(slug)s_%%(rev)s
file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(slug)s_%%(rev)s

# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""init user and refresh token
"""initial_migration

Revision ID: c79b0938ea4b
Revision ID: be24780c0da0
Revises:
Create Date: 2024-03-03 11:45:21.361225
Create Date: 2025-06-02 21:42:16.031375

"""

Expand All @@ -11,7 +11,7 @@
from alembic import op

# revision identifiers, used by Alembic.
revision = "c79b0938ea4b"
revision = "be24780c0da0"
down_revision = None
branch_labels = None
depends_on = None
Expand Down
45 changes: 39 additions & 6 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
# Note, complex types like lists are read as json-encoded strings.


import logging.config
from functools import lru_cache
from pathlib import Path

from pydantic import AnyHttpUrl, BaseModel, SecretStr, computed_field
from pydantic import AnyHttpUrl, BaseModel, Field, SecretStr, computed_field
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy.engine.url import URL

Expand All @@ -26,7 +27,7 @@

class Security(BaseModel):
jwt_issuer: str = "my-app"
jwt_secret_key: SecretStr
jwt_secret_key: SecretStr = SecretStr("sk-change-me")
jwt_access_token_expire_secs: int = 24 * 3600 # 1d
refresh_token_expire_secs: int = 28 * 24 * 3600 # 28d
password_bcrypt_rounds: int = 12
Expand All @@ -37,14 +38,15 @@ class Security(BaseModel):
class Database(BaseModel):
hostname: str = "postgres"
username: str = "postgres"
password: SecretStr
password: SecretStr = SecretStr("passwd-change-me")
port: int = 5432
db: str = "postgres"


class Settings(BaseSettings):
security: Security
database: Database
security: Security = Field(default_factory=Security)
database: Database = Field(default_factory=Database)
log_level: str = "INFO"

@computed_field # type: ignore[prop-decorator]
@property
Expand All @@ -67,4 +69,35 @@ def sqlalchemy_database_uri(self) -> URL:

@lru_cache(maxsize=1)
def get_settings() -> Settings:
return Settings() # type: ignore
return Settings()


def logging_config(log_level: str) -> None:
conf = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{asctime} [{levelname}] {name}: {message}",
"style": "{",
},
},
"handlers": {
"stream": {
"class": "logging.StreamHandler",
"formatter": "verbose",
"level": "DEBUG",
},
},
"loggers": {
"": {
"level": log_level,
"handlers": ["stream"],
"propagate": True,
},
},
}
logging.config.dictConfig(conf)


logging_config(log_level=get_settings().log_level)
15 changes: 1 addition & 14 deletions app/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,6 @@
default_user_access_token = create_jwt_token(default_user_id).access_token


# @pytest.fixture(scope="session")
# def event_loop_policy():
# return uvloop.EventLoopPolicy()


# @pytest.fixture(scope="session")
# def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
# loop = asyncio.new_event_loop()
# asyncio.set_event_loop(loop)
# yield loop
# loop.close()


@pytest_asyncio.fixture(scope="session", autouse=True)
async def fixture_setup_new_test_database() -> None:
worker_name = os.getenv("PYTEST_XDIST_WORKER", "gw0")
Expand Down Expand Up @@ -139,5 +126,5 @@ async def fixture_default_user(


@pytest_asyncio.fixture(name="default_user_headers", scope="function")
def fixture_default_user_headers(default_user: User) -> dict[str, str]:
async def fixture_default_user_headers(default_user: User) -> dict[str, str]:
return {"Authorization": f"Bearer {default_user_access_token}"}
4 changes: 4 additions & 0 deletions init.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
#!/bin/bash
set -e

echo "Run migrations"
alembic upgrade head

# Run whatever CMD was passed
exec "$@"
Loading