added author validate

parents
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=miniominio
MINIO_URL=minio:9000
MINIO_BUCKET_NAME=fa-2
database_url=postgres://postgres:postgres@postgres/postgres
\ No newline at end of file
venv
*__pycache__*
poetry.lock
.idea/
minio/
media/
data/
\ No newline at end of file
# Наследуем образ Python
FROM python:3.10-slim-buster
# Копируем файлы проекта
WORKDIR /app
COPY reqs.txt /app/
# Устанавливаем зависимости
RUN pip3 install --upgrade pip && pip3 install -r reqs.txt
# Копируем остальные файлы проекта
COPY . .
WORKDIR /app/code
\ No newline at end of file
import io
import fastapi
from minio import Minio
import settings
class MinIOClient(Minio):
def __init__(self, *args, **kwargs):
super().__init__(
*args,
endpoint=settings.minio_url,
access_key=settings.minio_root_user,
secret_key=settings.minio_root_password,
secure=False,
cert_check=False,
**kwargs,
)
async def upload_from_bytes(self, file: fastapi.UploadFile) -> None:
file_data = await file.read()
return self.put_object(
bucket_name=settings.minio_bucket_name,
object_name=file.filename,
data=io.BytesIO(file_data),
length=len(file_data),
)
from uuid import UUID
from starlette import status
import fastapi as fa
from api import schemas
from db.repository import ArticleRepository
from exceptions import common as common_exc, http as http_exc
from .validate import is_valid_user
router = fa.APIRouter(prefix='/article', tags=['article'])
repo = ArticleRepository()
@router.get('')
async def get_articles(query: schemas.ArticleGetSchema = fa.Depends()):
return await repo.get_list(**query.dict(exclude_none=True))
@router.get('/{id}')
async def get_article(id: UUID):
try:
return await repo.get(id)
except common_exc.NotFoundExcepton as e:
raise http_exc.HTTPNotFoundException(detail=str(e))
@router.post('')
async def create_article(
body: schemas.ArticleSchema = fa.Depends(schemas.ArticleSchema),
avatar: fa.UploadFile = fa.File(...),
):
try:
is_valid_user(body.author_id)
return await repo.create(**body.dict(), avatar=avatar)
except common_exc.CreateException as e:
raise http_exc.HTTPBadRequestException(detail=str(e))
except common_exc.NotFoundExcepton as e:
raise http_exc.HTTPNotFoundException(detail=str(e))
@router.patch('/{id}')
async def update_article(id: UUID, body: schemas.ArticleUpdateSchema):
try:
return await repo.update(id, **body.dict(exclude_none=True))
except common_exc.UpdateException as e:
raise http_exc.HTTPBadRequestException(detail=str(e))
except common_exc.NotFoundExcepton as e:
raise http_exc.HTTPNotFoundException(detail=str(e))
@router.delete('/{id}')
async def delete_article(id: UUID):
try:
await repo.delete(id)
return fa.responses.Response(status_code=status.HTTP_204_NO_CONTENT)
except common_exc.DeleteException as e:
raise http_exc.HTTPBadRequestException(detail=str(e))
except common_exc.NotFoundExcepton as e:
raise http_exc.HTTPNotFoundException(detail=str(e))
import requests
from fastapi import HTTPException
def is_valid_user(author_id: str):
response = requests.get(f"http://auth-core-1:8000/api/users/{author_id}")
if response.status_code == 404:
raise HTTPException(status_code=404, detail="Author does not exist")
from datetime import timedelta
import fastapi as fa
from starlette import status
from api.article.routes import router as article_router
from adapters import MinIOClient
import settings
router = fa.APIRouter(prefix='/api')
router.include_router(article_router)
@router.get('/greetings')
def get_greetings():
return {'text': 'Greetings!'}
# @router.post('/upload')
# async def upload_file(file: fa.UploadFile = fa.File(...)):
# client = MinIOClient()
# resp = await client.upload_from_bytes(file)
# print(777, resp.location)
# url = client.get_presigned_url(
# "GET",
# settings.minio_bucket_name,
# file.filename,
# expires=timedelta(hours=2),
# )
# print(777, url)
# return fa.Response(status_code=status.HTTP_204_NO_CONTENT)
from uuid import UUID
import pydantic
class ArticleSchema(pydantic.BaseModel):
name: str
description: str
author_id: UUID
class ArticleGetSchema(pydantic.BaseModel):
id: UUID | None = pydantic.Field(None)
name: str | None = pydantic.Field(None)
description: str | None = pydantic.Field(None)
class ArticleUpdateSchema(pydantic.BaseModel):
name: str | None = pydantic.Field(None)
description: str | None = pydantic.Field(None)
from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise
from settings import database_url
TORTOISE_ORM = {
'connections': {'default': database_url},
'apps': {
'models': {
'models': ['db.models', 'aerich.models'],
'default_connection': 'default',
},
},
}
def init_db(app: FastAPI) -> None:
register_tortoise(
app,
db_url=database_url,
modules={"models": ["db.models"]},
generate_schemas=False,
add_exception_handlers=True,
)
import os
import typing
from typing import Any
import uuid
from datetime import datetime
from pathlib import Path
from starlette.datastructures import UploadFile
from tortoise.fields import TextField
def get_valid_filename(name: str):
return str(name).strip().replace(' ', '_').replace('..', '')
class FileField(TextField):
def __init__(self, *, upload_to: str, **kwargs) -> None:
super().__init__(**kwargs)
upload_to = datetime.now().strftime(upload_to)
if not os.path.exists(upload_to):
os.makedirs(upload_to, exist_ok=True)
self.upload_to = Path(upload_to)
def to_db_value(self, value: str | UploadFile | bytes, *args, **kwargs):
if isinstance(value, UploadFile):
return self.save_file(value)
elif isinstance(value, str):
return value
return value
def to_python_value(self, value: str | UploadFile | bytes, *args, **kwargs):
if isinstance(value, str):
return value
elif isinstance(value, UploadFile):
return self.save_file(value)
def save_file(self, value: UploadFile):
file = value.file
file_name = get_valid_filename(value.filename)
file_path = self.upload_to / file_name
if os.path.isfile(file_path):
file_path = self.upload_to / f'{uuid.uuid4()}-{file_name}'
with open(file_path, 'wb') as f:
f.write(file.read())
return str(file_path)
from tortoise import models, fields
from db import custom_fields
class IdMixin(models.Model):
id = fields.UUIDField(pk=True)
class Meta:
abstract = True
class TimestampMixin(models.Model):
created_at = fields.DatetimeField(auto_now=True)
updated_at = fields.DatetimeField(auto_now_add=True)
class Meta:
abstract = True
class BaseModel(IdMixin, TimestampMixin):
class Meta:
abstract = True
class Tag(BaseModel):
name = fields.CharField(max_length=100)
articles = fields.ManyToManyField('models.Article', related_name='tags')
class Article(models.Model):
id = fields.UUIDField(pk=True)
name = fields.TextField()
description = fields.TextField()
author_id = fields.UUIDField()
avatar = custom_fields.FileField(upload_to='media/avatars', null=True)
class Meta:
table = 'article'
from uuid import UUID
import tortoise
import tortoise.exceptions
from exceptions import common as common_exc
from db import models
class BaseRepo:
model: tortoise.Model
async def get_list(self, **kwargs) -> list[tortoise.Model]:
return await self.model.filter(**kwargs)
async def get(self, id: UUID) -> tortoise.Model:
try:
return await self.model.get(id=id)
except tortoise.exceptions.DoesNotExist as e:
raise common_exc.NotFoundExcepton(str(e))
async def create(self, **kwargs) -> tortoise.Model:
try:
return await self.model.create(**kwargs)
except tortoise.exceptions.IntegrityError as e:
raise common_exc.CreateException(str(e))
async def update(self, id: UUID, **kwargs) -> tortoise.Model:
try:
instance = await self.get(id=id)
await instance.update_from_dict(kwargs).save()
return instance
except tortoise.exceptions.IntegrityError as e:
raise common_exc.UpdateException(str(e))
async def delete(self, id: UUID) -> None:
try:
instance = await self.get(id=id)
await instance.delete()
except tortoise.exceptions.IntegrityError as e:
raise common_exc.DeleteException(str(e))
class ArticleRepository(BaseRepo):
model = models.Article
class NotFoundExcepton(Exception):
pass
class CreateException(Exception):
pass
class UpdateException(Exception):
pass
class DeleteException(Exception):
pass
import fastapi
import pydantic
from starlette import status
from fastapi.responses import JSONResponse
from exceptions.http import BaseHTTPException
async def query_params_exc_handler(
request: fastapi.Request, exc: pydantic.ValidationError,
) -> JSONResponse:
return JSONResponse(
{'detail': exc.errors()}, status.HTTP_422_UNPROCESSABLE_ENTITY,
)
async def request_exc_handler(
request: fastapi.Request, exc: BaseHTTPException,
) -> JSONResponse:
return JSONResponse(
{'detail': exc.detail}, exc.status_code,
)
async def internal_exc_handler(
request: fastapi.Request, exc: Exception,
) -> JSONResponse:
return JSONResponse(
{'detail': 'Internal Server Error'}, status.HTTP_500_INTERNAL_SERVER_ERROR,
)
from typing import Any, Dict, Optional
from typing_extensions import Annotated, Doc
from fastapi.exceptions import HTTPException
from starlette import status
class BaseHTTPException(HTTPException):
pass
class HTTPBadRequestException(BaseHTTPException):
def __init__(
self, status_code: int = status.HTTP_400_BAD_REQUEST, detail: Any = None,
headers: Dict[str, str] | None = None,
) -> None:
super().__init__(status_code, detail, headers)
class HTTPNotFoundException(BaseHTTPException):
def __init__(
self, status_code: int = status.HTTP_404_NOT_FOUND, detail: Any = None,
headers: Dict[str, str] | None = None,
) -> None:
super().__init__(status_code, detail, headers)
import fastapi
from fastapi.staticfiles import StaticFiles
import pydantic
from api.router import router
from db.conf import init_db
from exceptions import handlers as exc_handlers, http as http_exc
def setup():
app = fastapi.FastAPI()
app.include_router(router)
app.exception_handler(pydantic.ValidationError)(exc_handlers.query_params_exc_handler)
app.exception_handler(http_exc.BaseHTTPException)(exc_handlers.request_exc_handler)
app.exception_handler(500)(exc_handlers.internal_exc_handler)
app.mount('/media', StaticFiles(directory='media'), name='media')
return app
app = setup()
init_db(app)
if __name__ == '__main__':
import uvicorn
uvicorn.run('main:app', host='0.0.0.0', port=1026, reload=True)
from tortoise import BaseDBAsyncClient
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "article" (
"id" UUID NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,
"author_id" UUID NOT NULL,
"avatar" TEXT
);
CREATE TABLE IF NOT EXISTS "tag" (
"id" UUID NOT NULL PRIMARY KEY,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"name" VARCHAR(100) NOT NULL
);
CREATE TABLE IF NOT EXISTS "aerich" (
"id" SERIAL NOT NULL PRIMARY KEY,
"version" VARCHAR(255) NOT NULL,
"app" VARCHAR(100) NOT NULL,
"content" JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS "tag_article" (
"tag_id" UUID NOT NULL REFERENCES "tag" ("id") ON DELETE CASCADE,
"article_id" UUID NOT NULL REFERENCES "article" ("id") ON DELETE CASCADE
);"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
"""
[tool.aerich]
tortoise_orm = "db.conf.TORTOISE_ORM"
location = "migrations"
src_folder = "./."
import os
database_url = os.getenv('database_url')
minio_root_user = os.getenv('MINIO_ROOT_USER')
minio_root_password = os.getenv('MINIO_ROOT_PASSWORD')
minio_url = os.getenv('MINIO_URL')
minio_bucket_name = os.getenv('MINIO_BUCKET_NAME')
version: '3'
services:
app:
build:
context: .
env_file:
- .env
ports:
- "1026:1026"
volumes:
- .:/app
command: bash -c "python3 main.py"
depends_on:
- postgres
postgres:
image: postgres:14-alpine
environment:
- POSTGRES_PASSWORD=postgres
volumes:
- ./data/postgres:/var/lib/postgresql/data/
minio:
image: minio/minio
ports:
- "9000:9000"
- "9090:9090"
env_file:
- .env
command: server --console-address ":9090" /data
volumes:
- ./minio/data:/data
- ./minio/conf:/root/.minio
networks:
default:
external: true
name: 'fa_2'
aerich==0.7.2
aiosqlite==0.17.0
annotated-types==0.6.0
anyio==3.7.1
argon2-cffi==23.1.0
argon2-cffi-bindings==21.2.0
async-timeout==4.0.3
asyncpg==0.29.0
bcrypt==4.1.2
certifi==2023.11.17
cffi==1.16.0
charset-normalizer==3.3.2
click==8.1.7
cryptography==41.0.7
dictdiffer==0.9.0
ecdsa==0.18.0
exceptiongroup==1.2.0
fastapi==0.106.0
h11==0.14.0
idna==3.6
iso8601==1.1.0
minio==7.2.0
passlib==1.7.4
pyasn1==0.5.1
pycparser==2.21
pycryptodome==3.19.0
pydantic==2.5.3
pydantic_core==2.14.6
pypika-tortoise==0.1.6
python-jose==3.3.0
python-multipart==0.0.6
pytz==2023.3.post1
requests==2.31.0
rsa==4.9
six==1.16.0
sniffio==1.3.0
starlette==0.27.0
tomlkit==0.12.3
tortoise-orm==0.20.0
typing_extensions==4.9.0
urllib3==2.1.0
uvicorn==0.25.0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment