Compare commits

...

3 Commits

10 changed files with 145 additions and 62 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# DEFAULT_ADMIN_EMAIL = "admin@example.com" # DEFAULT_ADMIN_EMAIL = "admin@example.com"
# DEFAULT_ADMIN_PASSWORD = "password-is-generated-randomly-on-first-launch" # DEFAULT_ADMIN_PASSWORD = "password-is-generated-randomly-on-first-launch"
POSTGRES_HOST = "localhost" POSTGRES_HOST = "db"
POSTGRES_PORT = "5432" POSTGRES_PORT = "5432"
POSTGRES_USER = "postgres" POSTGRES_USER = "postgres"
POSTGRES_PASSWORD = "postgres" POSTGRES_PASSWORD = "postgres"
Vendored
+2
View File
@@ -1,3 +1,5 @@
.env
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
+24 -17
View File
@@ -1,22 +1,29 @@
FROM python:3.13 as requirements-stage FROM python:3.12-slim
WORKDIR /tmp
RUN pip install poetry
RUN poetry self add poetry-plugin-export
COPY ./pyproject.toml ./poetry.lock* /tmp/
RUN poetry export -f requirements.txt --output requirements.txt --with dev --without-hashes
FROM python:3.13
ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
RUN apt-get update \
&& apt-get -y install gcc postgresql \
&& apt-get clean # netcat
RUN pip install --upgrade pip
WORKDIR /code WORKDIR /code
COPY --from=requirements-stage /tmp/requirements.txt ./requirements.txt
RUN pip install --no-cache-dir --upgrade -r ./requirements.txt RUN apt-get update \
COPY . . && apt-get -y install gcc libpq-dev \
ENV PYTHONPATH=. && apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN pip install poetry
RUN poetry config virtualenvs.create false
COPY ./pyproject.toml ./poetry.lock* /code/
RUN poetry install --with dev --no-root --no-interaction
COPY ./library_service /code/library_service
COPY ./alembic.ini /code/
COPY ./data.py /code/
RUN useradd app && chown -R app:app /code
USER app
ENV PYTHONPATH=/code
CMD ["uvicorn", "library_service.main:app", "--host", "0.0.0.0", "--port", "8000", "--forwarded-allow-ips=\"*\""]
+52 -18
View File
@@ -37,12 +37,12 @@
5. Запустите приложение: 5. Запустите приложение:
```bash ```bash
docker compose up api docker compose up api -d
``` ```
Для создания новых миграций: Для создания новых миграций:
```bash ```bash
docker compose run --rm -T api alembic revision --autogenerate -m "Migration name" alembic revision --autogenerate -m "Migration name"
``` ```
Для запуска тестов: Для запуска тестов:
@@ -50,6 +50,11 @@
docker compose up test docker compose up test
``` ```
Для добавление данных для примера используйте:
```bash
python data.py
```
### **Эндпоинты API** ### **Эндпоинты API**
**Авторы** **Авторы**
@@ -100,37 +105,66 @@
```mermaid ```mermaid
erDiagram erDiagram
AUTHOR { USER {
int id PK "ID автора" int id PK
string name "Имя автора" string username UK
string email UK
string full_name
string password
boolean is_active
boolean is_verified
}
USER_ROLE {
int user_id FK
string role
}
LOAN {
int id PK
int book_id FK
int user_id FK
datetime borrowed_at
datetime due_date
datetime returned_at
} }
BOOK { BOOK {
int id PK "ID книги" int id PK
string title "Название книги" string title
string description "Описание книги" string description
}
AUTHOR {
int id PK
string name
string bio
} }
GENRE { GENRE {
int id PK "ID жанра" int id PK
string name "Название жанра" string name
} }
AUTHOR_BOOK { AUTHOR_BOOK {
int author_id FK "ID автора" int author_id FK
int book_id FK "ID книги" int book_id FK
} }
GENRE_BOOK { GENRE_BOOK {
int genre_id FK "ID жанра" int genre_id FK
int book_id FK "ID книги" int book_id FK
} }
AUTHOR ||--o{ AUTHOR_BOOK : "писал" USER ||--o{ USER_ROLE : "имеет роли"
BOOK ||--o{ AUTHOR_BOOK : "написан" USER ||--o{ LOAN : "берёт книги"
LOAN }o--|| BOOK : "выдача"
BOOK ||--o{ GENRE_BOOK : "принадлежит" AUTHOR ||--o{ AUTHOR_BOOK : "пишет"
GENRE ||--o{ GENRE_BOOK : "содержит" AUTHOR_BOOK }o--|| BOOK : "авторство"
GENRE ||--o{ GENRE_BOOK : "содержит"
GENRE_BOOK }o--|| BOOK : "жанр"
``` ```
+1 -1
View File
@@ -3,7 +3,7 @@ from typing import Optional
# Конфигурация # Конфигурация
USERNAME = "admin" USERNAME = "admin"
PASSWORD = "GzwQMe3j2DsPRKpL2DVw6A" PASSWORD = "your-password-here"
BASE_URL = "http://localhost:8000" BASE_URL = "http://localhost:8000"
+46 -11
View File
@@ -1,28 +1,63 @@
services: services:
db: db:
container_name: db
image: postgres:17 image: postgres:17
ports: container_name: db
- 5432:5432 restart: unless-stopped
# volumes: logging:
# - ./data/db:/var/lib/postgresql/data options:
max-size: "10m"
max-file: "3"
volumes:
- ./data/db:/var/lib/postgresql/data
networks:
- proxy
env_file: env_file:
- ./.env - ./.env
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
api: api:
container_name: api
build: . build: .
command: bash -c "alembic upgrade head && uvicorn library_service.main:app --host 0.0.0.0 --port 8000 --reload" container_name: api
restart: unless-stopped
command: bash -c "uvicorn library_service.main:app --host 0.0.0.0 --port 8000 --forwarded-allow-ips=\"*\""
logging:
options:
max-size: "10m"
max-file: "3"
networks:
- proxy
env_file:
- ./.env
volumes: volumes:
- .:/code - .:/code
ports: depends_on:
- "8000:8000" db:
# depends_on: condition: service_healthy
# - db
tests: tests:
container_name: tests container_name: tests
build: . build: .
command: bash -c "pytest tests" command: bash -c "pytest tests"
restart: unless-stopped
logging:
options:
max-size: "10m"
max-file: "3"
networks:
- proxy
env_file:
- ./.env
volumes: volumes:
- .:/code - .:/code
depends_on:
db:
condition: service_healthy
networks:
proxy: # Рекомендуется использовать через реверс-прокси
name: proxy
external: true
+4 -4
View File
@@ -96,10 +96,10 @@ async def book(request: Request, book_id: int):
return templates.TemplateResponse(request, "book.html") return templates.TemplateResponse(request, "book.html")
@router.get("/auth", include_in_schema=False) @router.get("/auth", include_in_schema=False)
async def auth(request: Request): async def auth(request: Request):
"""Эндпоинт страницы авторизации""" """Эндпоинт страницы авторизации"""
return templates.TemplateResponse(request, "auth.html") return templates.TemplateResponse(request, "auth.html")
@router.get("/profile", include_in_schema=False) @router.get("/profile", include_in_schema=False)
+7 -2
View File
@@ -53,7 +53,12 @@ const Utils = {
}; };
const Api = { const Api = {
getBaseUrl() {
return window.location.origin;
},
async request(endpoint, options = {}) { async request(endpoint, options = {}) {
const fullUrl = this.getBaseUrl() + endpoint;
const token = localStorage.getItem("access_token"); const token = localStorage.getItem("access_token");
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -67,14 +72,14 @@ const Api = {
const config = { ...options, headers }; const config = { ...options, headers };
try { try {
const response = await fetch(endpoint, config); const response = await fetch(fullUrl, config);
if (response.status === 401) { if (response.status === 401) {
const refreshed = await Auth.tryRefresh(); const refreshed = await Auth.tryRefresh();
if (refreshed) { if (refreshed) {
headers["Authorization"] = headers["Authorization"] =
`Bearer ${localStorage.getItem("access_token")}`; `Bearer ${localStorage.getItem("access_token")}`;
const retryResponse = await fetch(endpoint, { ...options, headers }); const retryResponse = await fetch(fullUrl, { ...options, headers });
if (retryResponse.ok) { if (retryResponse.ok) {
return retryResponse.json(); return retryResponse.json();
} }
Generated
+6 -6
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. # This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]] [[package]]
name = "aiofiles" name = "aiofiles"
@@ -1969,14 +1969,14 @@ files = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.34.3" version = "0.40.0"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.10"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885"}, {file = "uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee"},
{file = "uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a"}, {file = "uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea"},
] ]
[package.dependencies] [package.dependencies]
@@ -2247,4 +2247,4 @@ files = [
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "a8d44f0decfa3ba437e998207c16ca7429ee42e930e8aa1d40f87231e71f219f" content-hash = "6048c7b120fbeb4533a1e858a87eb09aec68e5da569ca821b9e41ecabf220a9e"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "LibraryAPI" name = "LibraryAPI"
version = "0.2.0" version = "0.3.0"
description = "Это простое API для управления авторами, книгами и их жанрами." description = "Это простое API для управления авторами, книгами и их жанрами."
authors = ["wowlikon"] authors = ["wowlikon"]
readme = "README.md" readme = "README.md"
@@ -9,11 +9,11 @@ packages = [{ include = "library_service" }]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.12" python = "^3.12"
fastapi = { extras = ["all"], version = "^0.115.12" } fastapi = { extras = ["all"], version = "^0.115.12" }
uvicorn = { extras = ["standard"], version = "^0.40.0" }
psycopg2-binary = "^2.9.10" psycopg2-binary = "^2.9.10"
alembic = "^1.16.1" alembic = "^1.16.1"
python-dotenv = "^0.21.0" python-dotenv = "^0.21.0"
sqlmodel = "^0.0.24" sqlmodel = "^0.0.24"
uvicorn = "^0.34.3"
jinja2 = "^3.1.6" jinja2 = "^3.1.6"
toml = "^0.10.2" toml = "^0.10.2"
python-jose = {extras = ["cryptography"], version = "^3.5.0"} python-jose = {extras = ["cryptography"], version = "^3.5.0"}