From 5e990a615e9e320015d2ee78f0ca24f578511039 Mon Sep 17 00:00:00 2001 From: Conrad Date: Thu, 10 Oct 2024 15:51:41 +0200 Subject: [PATCH] feat: added initial config --- .gitignore | 12 +++-- .vscode/launch.json | 35 +++++++++++++ .vscode/settings.json | 42 +++++++++++++++ Dockerfile | 29 +++++++++++ README.md | 4 +- alembic.ini | 61 ++++++++++++++++++++++ alembic/env.py | 43 ++++++++++++++++ alembic/script.py.mako | 26 ++++++++++ alembic/versions/95201f00f6b9_.py | 38 ++++++++++++++ alembic/versions/e253d9799d38_.py | 38 ++++++++++++++ app/__init__.py | 0 app/main.py | 86 +++++++++++++++++++++++++++++++ app/models/app.py | 6 +++ app/models/auth.py | 6 +++ app/models/entry.py | 0 app/routes/app.py | 65 +++++++++++++++++++++++ app/schema/app.py | 9 ++++ app/schema/common.py | 10 ++++ app/services/auth.py | 24 +++++++++ app/services/db/models.py | 6 +++ app/services/db/session.py | 8 +++ app/setup.py | 20 +++++++ app/test_main.py | 51 ++++++++++++++++++ docker-compose.yml | 36 +++++++++++++ pytest.ini | 9 ++++ requirements.txt | 34 ++++++++++++ 26 files changed, 694 insertions(+), 4 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 Dockerfile create mode 100644 alembic.ini create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/95201f00f6b9_.py create mode 100644 alembic/versions/e253d9799d38_.py create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 app/models/app.py create mode 100644 app/models/auth.py create mode 100644 app/models/entry.py create mode 100644 app/routes/app.py create mode 100644 app/schema/app.py create mode 100644 app/schema/common.py create mode 100644 app/services/auth.py create mode 100644 app/services/db/models.py create mode 100644 app/services/db/session.py create mode 100644 app/setup.py create mode 100644 app/test_main.py create mode 100644 docker-compose.yml create mode 100644 pytest.ini create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 82f9275..ca80eeb 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,10 @@ cover/ local_settings.py db.sqlite3 db.sqlite3-journal +dev.sqlite3 +TEST_DB/ +files/ +DOCKER/ # Flask stuff: instance/ @@ -77,6 +81,7 @@ target/ # Jupyter Notebook .ipynb_checkpoints +.virtual_documents # IPython profile_default/ @@ -106,10 +111,8 @@ ipython_config.py #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +# https://pdm.fming.dev/#use-with-ide .pdm.toml -.pdm-python -.pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ @@ -160,3 +163,6 @@ cython_debug/ # 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/ + +# MacOS +.DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4936f9e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run", + "type": "python", + "request": "launch", + "module": "uvicorn", + "args": ["app.main:app", "--reload", "--port", "9000"], + "jinja": true, + "justMyCode": true, + "env": { + "PYDEVD_DISABLE_FILE_VALIDATION": "1" + } + }, + { + "name": "Migrate", + "type": "python", + "request": "launch", + "module": "alembic", + "args": ["upgrade", "head"], + "jinja": true, + "justMyCode": true + }, + { + "name": "Make Migrations", + "type": "python", + "request": "launch", + "module": "alembic", + "args": ["revision", "--autogenerate"], + "jinja": true, + "justMyCode": true + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de8d7cb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,42 @@ +{ + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python3", + "python.globalModuleInstallation": false, + "python.terminal.activateEnvironment": true, + "python.analysis.typeCheckingMode": "basic", + "[python]": { + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "black-formatter.args": ["--line-length", "100"], + "isort.args": ["--profile", "black"], + // Editor General + "files.insertFinalNewline": true, + "editor.fontSize": 15, + "editor.formatOnSave": true, + "editor.rulers": [100], + "editor.minimap.enabled": true, + "files.exclude": { + "**/.git": true, + "**/.pytest_cache": true, + "**/.venv": true, + "**/.svn": true, + "**/.hg": true, + "**/db.sqlite3": true, + "**/.DS_Store": true, + "**/*.pyc": true, + "**/__pycache__/": true + }, + "search.exclude": { + "**/.git": true, + "**/.venv": true, + "**/tmp": true, + "htmlcov/*": true, + "docs/*": true, + ".venv/*": true + }, + "python.testing.pytestArgs": ["app"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2c007ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM python:3.12-slim AS builder +WORKDIR /build +COPY requirements.txt . +RUN pip wheel --no-cache-dir --wheel-dir=/build/wheels \ + -r requirements.txt \ + +FROM python:3.12-slim + +WORKDIR /app +COPY . . + +RUN apt-get update && apt-get install -y curl git + +COPY --from=builder /build/wheels /wheels +RUN pip install --no-cache /wheels/* + +# Remove the wheels directory after installation to save space +RUN rm -rf /wheels +# Python setup +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 +ARG VERSION=unknown +ENV VERSION=${VERSION} + +CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "-w", "6" , "-b", "0.0.0.0:9000","app.main:app"] +EXPOSE 9000 + +HEALTHCHECK --interval=30s --timeout=10s --retries=5 \ + CMD curl --fail http://localhost:9000/openapi.json || exit 1 diff --git a/README.md b/README.md index a8520f0..d9569d8 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# apilog \ No newline at end of file +# apilog + +Tiny logging API server, for taking logs via HTTP POST requests. diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..758c208 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,61 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +[post_write_hooks] +; This will work from alembic version 1.12 https://alembic.sqlalchemy.org/en/latest/autogenerate.html#basic-post-processor-configuration +; hooks = black +; black.type = exec +; black.executable = black +; black.options = -l 100 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..51f8c91 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,43 @@ +from logging.config import fileConfig + +from dotenv import load_dotenv +from sqlalchemy import create_engine + +from alembic import context + +load_dotenv() + + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# 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) + + +# Our models +from app.services.db.models import Base + +target_metadata = Base.metadata + + +def run_migrations() -> None: + from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, name + + with create_engine(SQLALCHEMY_DATABASE_URL + name, pool_pre_ping=True).connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True, + compare_server_default=True, + render_as_batch=True, + ) + + with context.begin_transaction(): + context.run_migrations() + + +run_migrations() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/95201f00f6b9_.py b/alembic/versions/95201f00f6b9_.py new file mode 100644 index 0000000..297b007 --- /dev/null +++ b/alembic/versions/95201f00f6b9_.py @@ -0,0 +1,38 @@ +"""empty message + +Revision ID: 95201f00f6b9 +Revises: e253d9799d38 +Create Date: 2024-10-10 15:45:50.089915 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "95201f00f6b9" +down_revision: Union[str, None] = "e253d9799d38" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "application", + sa.Column("name", sa.String(length=512), nullable=False), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column( + "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True + ), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.Column("created_by_id", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id", name="pk_application"), + sa.UniqueConstraint("name", name="uq_application_name"), + ) + + +def downgrade() -> None: + op.drop_table("application") diff --git a/alembic/versions/e253d9799d38_.py b/alembic/versions/e253d9799d38_.py new file mode 100644 index 0000000..8ca0939 --- /dev/null +++ b/alembic/versions/e253d9799d38_.py @@ -0,0 +1,38 @@ +"""empty message + +Revision ID: e253d9799d38 +Revises: +Create Date: 2024-10-10 15:23:32.339647 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "e253d9799d38" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "apikey", + sa.Column("note", sa.String(length=512), nullable=False), + sa.Column("id", sa.UUID(), nullable=False), + sa.Column( + "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=True + ), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.Column("created_by_id", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("id", name="pk_apikey"), + sa.UniqueConstraint("note", name="uq_apikey_note"), + ) + + +def downgrade() -> None: + op.drop_table("apikey") diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..feacddd --- /dev/null +++ b/app/main.py @@ -0,0 +1,86 @@ +import logging +import os +from contextlib import asynccontextmanager + +from creyPY.fastapi.app import generate_unique_id +from dotenv import load_dotenv +from fastapi import FastAPI, Security +from fastapi.middleware.cors import CORSMiddleware +from fastapi_pagination import add_pagination + +from app.services.auth import verify + +load_dotenv() + +ENV = os.getenv("ENV", "local").lower() +VERSION = os.getenv("VERSION", "Alpha") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + from app.setup import setup + + setup() + + # Create initial API key + from creyPY.fastapi.db.session import get_db + from sqlalchemy.orm import Session + + from app.models.auth import APIKey + + db: Session = next(get_db()) + key_obj = db.query(APIKey).filter(APIKey.note == "local_key").one_or_none() + if not key_obj: + db.add(APIKey(note="local_key")) # type: ignore + db.commit() + key_obj = db.query(APIKey).filter(APIKey.note == "local_key").one() + print(f"Local API key: {key_obj.id}") + yield + + +# App Setup +app = FastAPI( + title="ApiLog API", + description="Tiny service for ingesting logs via POST and querying them via GET.", + version=VERSION, + docs_url="/", + redoc_url=None, + debug=ENV != "prod", + swagger_ui_parameters={ + "docExpansion": "list", + "displayOperationId": True, + "defaultModelsExpandDepth": 5, + "defaultModelExpandDepth": 5, + "filter": True, + "displayRequestDuration": True, + "defaultModelRendering": "model", + "persistAuthorization": True, + }, + generate_unique_id_function=generate_unique_id, + dependencies=[Security(verify)], + lifespan=lifespan, +) + +origins = [ + "http://localhost:3000", + "http://localhost:5173", + "http://localhost:4200", +] + +# CORS Setup +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# App Routers +from app.routes.app import router as app_router + +app.include_router(app_router) + + +# Pagination +add_pagination(app) diff --git a/app/models/app.py b/app/models/app.py new file mode 100644 index 0000000..45ccdab --- /dev/null +++ b/app/models/app.py @@ -0,0 +1,6 @@ +from creyPY.fastapi.models.base import Base +from sqlalchemy import Column, String + + +class Application(Base): + name = Column(String(512), nullable=False, unique=True) diff --git a/app/models/auth.py b/app/models/auth.py new file mode 100644 index 0000000..57f2d78 --- /dev/null +++ b/app/models/auth.py @@ -0,0 +1,6 @@ +from creyPY.fastapi.models.base import Base +from sqlalchemy import Column, String + + +class APIKey(Base): + note = Column(String(512), nullable=False, unique=True) diff --git a/app/models/entry.py b/app/models/entry.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/app.py b/app/routes/app.py new file mode 100644 index 0000000..0e3ca5e --- /dev/null +++ b/app/routes/app.py @@ -0,0 +1,65 @@ +from creyPY.fastapi.crud import ( + create_obj_from_data, +) +from creyPY.fastapi.db.session import get_db +from fastapi import APIRouter, Depends, Security, HTTPException +from sqlalchemy.orm import Session + +from app.services.auth import verify +from app.schema.app import AppIN, AppOUT +from app.models.app import Application +from fastapi_pagination.ext.sqlalchemy import paginate +from creyPY.fastapi.pagination import Page +from uuid import UUID + +router = APIRouter(prefix="/app", tags=["apps"]) + + +@router.post("/", status_code=201) +async def create_app( + data: AppIN, + sub: str = Security(verify), + db: Session = Depends(get_db), +) -> AppOUT: + obj = create_obj_from_data( + data, + Application, + db, + additonal_data={"created_by_id": sub}, + ) + return AppOUT.model_validate(obj) + + +@router.delete("/{app_id}", status_code=204) +async def delete_app( + app_id: UUID, + sub: str = Security(verify), + db: Session = Depends(get_db), +) -> None: + obj = db.query(Application).filter_by(id=app_id, created_by_id=sub).one_or_none() + if obj is None: + raise HTTPException(status_code=404, detail="Item not found") + db.delete(obj) + db.commit() + return None + + +@router.get("/") +async def get_apps( + sub: str = Security(verify), + db: Session = Depends(get_db), +) -> Page[AppOUT]: + the_select = db.query(Application).filter_by(created_by_id=sub) + return paginate(the_select) + + +@router.get("/{app_id}") +async def get_app( + app_id: UUID, + sub: str = Security(verify), + db: Session = Depends(get_db), +) -> AppOUT: + obj = db.query(Application).filter_by(id=app_id, created_by_id=sub).one_or_none() + if obj is None: + raise HTTPException(status_code=404, detail="Item not found") + return AppOUT.model_validate(obj) diff --git a/app/schema/app.py b/app/schema/app.py new file mode 100644 index 0000000..4bdb5c1 --- /dev/null +++ b/app/schema/app.py @@ -0,0 +1,9 @@ +from app.schema.common import BaseSchemaModelIN, BaseSchemaModelOUT + + +class AppIN(BaseSchemaModelIN): + name: str + + +class AppOUT(BaseSchemaModelOUT, AppIN): + pass diff --git a/app/schema/common.py b/app/schema/common.py new file mode 100644 index 0000000..32ff7ad --- /dev/null +++ b/app/schema/common.py @@ -0,0 +1,10 @@ +from creyPY.fastapi.schemas.base import BaseSchemaModelOUT as TemplateOUT +from pydantic import BaseModel, ConfigDict + + +class BaseSchemaModelIN(BaseModel): + model_config = ConfigDict(from_attributes=True) + + +class BaseSchemaModelOUT(BaseSchemaModelIN, TemplateOUT): + pass diff --git a/app/services/auth.py b/app/services/auth.py new file mode 100644 index 0000000..b53f53c --- /dev/null +++ b/app/services/auth.py @@ -0,0 +1,24 @@ +from uuid import UUID + +from creyPY.fastapi.db.session import get_db +from dotenv import load_dotenv +from fastapi import Depends, HTTPException, Request, Security +from fastapi.security import APIKeyQuery +from sqlalchemy.orm import Session + +from app.models.auth import APIKey + +load_dotenv() + + +async def verify( + request: Request, + api_key_query: str = Security(APIKeyQuery(name="api-key", auto_error=False)), + db: Session = Depends(get_db), +) -> str: + if api_key_query: + key_info = db.query(APIKey).filter_by(id=UUID(api_key_query)).one_or_none() + if key_info is None: + raise HTTPException(status_code=401, detail="Invalid API key.") + return f"API-KEY: {key_info.note}" + raise HTTPException(status_code=401, detail="No API key.") diff --git a/app/services/db/models.py b/app/services/db/models.py new file mode 100644 index 0000000..d8e26da --- /dev/null +++ b/app/services/db/models.py @@ -0,0 +1,6 @@ +from creyPY.fastapi.models.base import Base # noqa, isort:skip + +# custom models from all apps +from app.models.entry import * # noqa, isort:skip +from app.models.auth import * # noqa, isort:skip +from app.models.app import * # noqa, isort:skip diff --git a/app/services/db/session.py b/app/services/db/session.py new file mode 100644 index 0000000..ddf7a0c --- /dev/null +++ b/app/services/db/session.py @@ -0,0 +1,8 @@ +from sqlalchemy_utils import create_database, database_exists + + +def create_if_not_exists(db_name: str): + from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL + + if not database_exists(SQLALCHEMY_DATABASE_URL + db_name): + create_database(SQLALCHEMY_DATABASE_URL + db_name) diff --git a/app/setup.py b/app/setup.py new file mode 100644 index 0000000..959368f --- /dev/null +++ b/app/setup.py @@ -0,0 +1,20 @@ +import os + +from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, name + +from alembic import command +from alembic.config import Config +from app.services.db.session import create_if_not_exists + + +def setup(db_name=name): + # Create Database + create_if_not_exists(db_name) + + # Make alembic migrations + config = Config() + config.set_main_option("sqlalchemy.url", SQLALCHEMY_DATABASE_URL + db_name) + config.set_main_option( + "script_location", os.path.join(os.path.dirname(os.path.dirname(__file__)), "alembic") + ) + command.upgrade(config, "head") diff --git a/app/test_main.py b/app/test_main.py new file mode 100644 index 0000000..5b85d5d --- /dev/null +++ b/app/test_main.py @@ -0,0 +1,51 @@ +from creyPY.fastapi.db.session import SQLALCHEMY_DATABASE_URL, get_db +from creyPY.fastapi.models.base import Base +from creyPY.fastapi.testing import GenericClient +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy_utils import create_database, database_exists, drop_database + +from app.services.auth import verify + +from .main import app + +CURRENT_USER = "api-key|testing" + + +class TestAPI: + def setup_class(self): + self.engine = create_engine(SQLALCHEMY_DATABASE_URL + "test", pool_pre_ping=True) + if database_exists(self.engine.url): + drop_database(self.engine.url) + create_database(self.engine.url) + + Base.metadata.create_all(self.engine) + + def get_db_test(): + db = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)() + try: + yield db + finally: + db.close() + + def get_test_sub(): + global CURRENT_USER + return CURRENT_USER + + app.dependency_overrides[get_db] = get_db_test + app.dependency_overrides[verify] = get_test_sub + + self.c = GenericClient(app) + + def teardown_class(self): + drop_database(self.engine.url) + + def test_swagger_gen(self): + re = self.c.get("/openapi.json") + assert re["info"]["title"] == "ApiLog API" + + def test_health_check(self): + self.c.get("/", parse_json=False) + + def test_application_api(self): + self.c.obj_lifecycle({"name": "Testing"}, "/app/") diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..94726d5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +--- +x-restart-policy: &restart_policy + restart: unless-stopped + +services: + # apilog_worker: + # build: + # context: . + # dockerfile: Dockerfile + # <<: *restart_policy + # container_name: api_worker + # environment: + # - POSTGRES_HOST=apilog_db + # - POSTGRES_PORT=5432 + # - POSTGRES_USER=root + # - POSTGRES_PASSWORD=password + # - POSTGRES_DB=apilog + # depends_on: + # apilog_db: + # condition: service_healthy + + apilog_db: + image: postgres + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: root + POSTGRES_DB: fastapi + <<: *restart_policy + container_name: apilog_db + healthcheck: + test: ["CMD-SHELL", "pg_isready -U root"] + interval: 10s + timeout: 5s + retries: 5 + ports: + - "5432:5432" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..4b337d9 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,9 @@ +[pytest] +filterwarnings = + default:::app.* + ignore +addopts = -sl +log_cli = 1 +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)s] %(message)s (%(filename)s:%(lineno)s) +log_cli_date_format=%H:%M:%S diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3d7a964 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,34 @@ +annotated-types==0.7.0 +anyio==4.6.0 +certifi==2024.8.30 +creyPY==1.2.5 +fastapi==0.115.0 +fastapi-pagination==0.12.31 +h11==0.14.0 +httpcore==1.0.6 +httpx==0.27.2 +idna==3.10 +psycopg==3.2.3 +psycopg-binary==3.2.3 +psycopg-pool==3.2.3 +pydantic==2.9.2 +pydantic_core==2.23.4 +python-dotenv==1.0.1 +sniffio==1.3.1 +SQLAlchemy==2.0.35 +starlette==0.38.6 +typing_extensions==4.12.2 + +Mako==1.3.5 # Alembic +MarkupSafe==3.0.1 # Alembic +alembic==1.13.3 # Alembic + +SQLAlchemy-Utils==0.41.2 # SQLAlchemy + +click==8.1.7 # Uvicorn +uvicorn==0.31.1 # Uvicorn + +iniconfig==2.0.0 # pytest +packaging==24.1 # pytest +pluggy==1.5.0 # pytest +pytest==8.3.3 # pytest