diff --git a/.gitignore b/.gitignore index 4bb1c71..3e3b8af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,31 @@ -# Byte-compiled / optimized / DLL files + +# Environment variables and secrets +.env +.env.* +!.env.example +.envrc +*.pem +*.key +*.p12 +*.pfx + +# Python bytecode __pycache__/ -*.py[codz] +*.py[cod] *$py.class - -# C extensions *.so -# Distribution / packaging +# Virtual environments +.venv/ +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# Python packaging .Python build/ -develop-eggs/ dist/ downloads/ eggs/ @@ -18,7 +34,6 @@ lib/ lib64/ parts/ sdist/ -var/ wheels/ share/python-wheels/ *.egg-info/ @@ -26,199 +41,99 @@ share/python-wheels/ *.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/ +# Testing and coverage +.pytest_cache/ .tox/ .nox/ .coverage .coverage.* -.cache -nosetests.xml +htmlcov/ coverage.xml +nosetests.xml *.cover -*.py.cover .hypothesis/ -.pytest_cache/ -cover/ -# Translations -*.mo -*.pot +# Type checking and linting +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ +.ruff_cache/ -# Django stuff: +# Logs *.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy +logs/ +log/ + +# Databases and local data +*.sqlite +*.sqlite3 +*.db +*.db-journal + +# Alembic +# Keep migration scripts tracked. +# Ignore only local temporary files. +alembic/__pycache__/ + +# Temporary files +*.tmp +*.temp +temp/ +tmp/ +tempCodeRunnerFile.py -# Sphinx documentation -docs/_build/ +# IDEs and editors +.vscode/ +.idea/ +.spyderproject +.spyproject +.ropeproject -# PyBuilder -.pybuilder/ -target/ +# OS files +.DS_Store +._* +Thumbs.db +Desktop.ini -# Jupyter Notebook +# Jupyter .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 - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -# poetry.lock -# poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -# pdm.lock -# pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -# pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff +# Node.js (if frontend tooling is added) +node_modules/ +npm-debug.log* +yarn-debug.log* +pnpm-debug.log* + +# Build and distribution +/site +target/ + +# Celery celerybeat-schedule celerybeat.pid # Redis *.rdb *.aof -*.pid - -# RabbitMQ -mnesia/ -rabbitmq/ -rabbitmq-data/ - -# ActiveMQ -activemq-data/ - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.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/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -# .idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ -# Temporary file for partial code execution -tempCodeRunnerFile.py +# Application runtime data +instance/ +.webassets-cache/ +uploads/ +storage/ -# Ruff stuff: -.ruff_cache/ +# Security scan outputs +.bandit +.safety +pip-audit-report.json -# PyPI configuration file +# Local configuration +local_settings.py .pypirc -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ - -# Streamlit -.streamlit/secrets.toml - -# macOS / Windows junk -.DS_Store -._* -Thumbs.db -Desktop.ini +# Generated documentation +docs/_build/ diff --git a/alembic/env.py b/alembic/env.py index 0a321c3..35ef554 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -28,7 +28,10 @@ target_metadata = Base.metadata -config.set_main_option("sqlalchemy.url", get_settings().database_url) +config.set_main_option( + "sqlalchemy.url", + get_settings().database_url.replace("%", "%%") +) def run_migrations_offline() -> None: diff --git a/app/main.py b/app/main.py index 0405c90..6534024 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,5 @@ import asyncio +import logging from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Request, Response, status @@ -10,6 +11,8 @@ from app.db.migrate import run_migrations from app.db.session import engine +logger = logging.getLogger(__name__) + @asynccontextmanager async def lifespan(_app: FastAPI): @@ -71,7 +74,6 @@ async def security_headers(request: Request, call_next) -> Response: app.include_router(shares.router) app.include_router(sync.router) - @app.get("/health", tags=["health"]) async def health() -> dict[str, str]: """Liveness + DB readiness. Returns 503 when Postgres is unreachable.""" @@ -79,8 +81,10 @@ async def health() -> dict[str, str]: async with engine.connect() as conn: await conn.execute(text("SELECT 1")) except Exception as exc: + logger.exception("Database health check failed") raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Database unavailable", ) from exc - return {"status": "ok", "database": "ok"} + + return {"status": "ok", "database": "ok"} \ No newline at end of file diff --git a/app/schemas/collection.py b/app/schemas/collection.py index d25ae36..ecec907 100644 --- a/app/schemas/collection.py +++ b/app/schemas/collection.py @@ -1,14 +1,18 @@ from datetime import datetime from typing import Annotated -from pydantic import BaseModel, ConfigDict, Field, PlainValidator +from pydantic import ( + BeforeValidator, + BaseModel, + ConfigDict, + Field, +) from app.schemas.vault_ids import MAX_COLLECTION_ICON_CHARS, normalize_folder_id FolderId = Annotated[ - str | None, - PlainValidator(normalize_folder_id), - Field(max_length=64), + Annotated[str, Field(max_length=64)] | None, + BeforeValidator(normalize_folder_id), ]