makeup readme and clean unnecessary files

This commit is contained in:
Ivanov Matvey 2025-03-14 02:45:33 +10:00
parent 03c4941af0
commit 9b096057c0
5 changed files with 37 additions and 296 deletions

View File

@ -1,100 +0,0 @@
stages:
- lint
- build
- backup
- deploy
.configure_ssh:
before_script:
# Run ssh-agent for keys management
- 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'
- eval $(ssh-agent -s)
# Add place for ssh related files
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
# Initialize token
- chmod 400 "$SSH_PRIVATE_KEY"
- ssh-add "$SSH_PRIVATE_KEY"
# Add server fingerprint to known hosts
- ssh-keyscan "$SSH_HOST" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
.on_merge_request:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- when: never
lint-ruff:
stage: lint
image: registry.gitlab.com/pipeline-components/ruff:latest
rules:
- !reference [.on_merge_request, rules]
script:
- echo "☕ Linting with ruff"
- ruff check --output-format=gitlab src/
- echo "✅ Passed"
lint-mypy:
stage: lint
image: python:3.12
rules:
- !reference [.on_merge_request, rules]
before_script:
- pip install mypy
- apt install make
- make deps
script:
- echo "🐍 Typechecking with mypy"
- mypy src
- echo "✅ Passed"
build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
script:
- docker pull $CI_REGISTRY_IMAGE:latest || true
- docker build --target prod --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
database-backup:
stage: backup
image: ubuntu:latest
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
before_script:
- !reference [.configure_ssh, before_script]
script:
- echo "💾 backuping database"
- ssh $SSH_USER@$SSH_HOST "docker exec database pg_dump --column-inserts udom >> pre_deploy.sql"
- echo "✅ Passed"
deploy-dev:
stage: deploy
image: ubuntu:latest
rules:
- if: '$CI_COMMIT_BRANCH == "dev"'
before_script:
- !reference [.configure_ssh, before_script]
script:
- echo "🚀🧨 Deploing dev changes"
- ssh $SSH_USER@$SSH_HOST "cd /root/udom_dev/ && git pull && docker compose -f compose-dev.yaml up -d --build --remove-orphans"
- echo "✅ Passed"
deploy-main:
stage: deploy
image: ubuntu:latest
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
before_script:
- !reference [.configure_ssh, before_script]
script:
- echo "🚀 Deploing changes"
- ssh $SSH_USER@$SSH_HOST "cd /root/udom/ && git pull && echo $SERVER_TOKEN | docker login registry.gitlab.com -u 'Server' --password-stdin && docker compose pull && docker compose up -d --build --remove-orphans"
- echo "✅ Passed"

View File

@ -1,15 +1,50 @@
# python_dev # python_dev
# launch # Запуск
```cp .env.example .env```
```docker compose up -d``` ```docker compose up -d```
# migrations # миграции БД
```cp .env.example .env```
```cd alembic/db1 && alembic revision --autogenerate && alembic upgrade head``` ```cd alembic/db1 && alembic revision --autogenerate && alembic upgrade head```
```cd alembic/db2 && alembic revision --autogenerate && alembic upgrade head``` ```cd alembic/db2 && alembic revision --autogenerate && alembic upgrade head```
# режим разработки
```cp .env.example .env```
```rye sync```
```granian src/app.py --reload```
# структура проекта
📦python_dev_farpost
┣ 📂alembic
┃ ┣ 📂db1 - вспомогательные инструменты alembic для миграций первой БД
┃ ┗ 📂db2 - вспомогательные инструменты alembic для миграций второй БД
┣ 📂database
┃ ┣ 📂dumps - дампы БД
┃ ┗ 📜....sh - скрипт, для автоматизации создания и импорта дампов в multiDB
┣ 📂src
┃ ┣ 📂adapters
┃ ┃ ┗ 📂database - модели sqlAlchemy и инструменты для подключения к БД
┃ ┣ 📂api - эндпоинты fastapi + формирование датасета (не делил ввиду отстутствия необходимости)
┃ ┣ 📂schemas - схемы для обработки входных и выходных данных (в тч валидации данных для БД)
┃ ┣ 📜app.py - приложение для запуска
┃ ┣ 📜settings.py - валидация настроек из .env, которые в дальнейшем удобно брать
┣ 📜.env.example -
┣ 📜.gitignore
┣ 📜compose.yaml
┣ 📜Dockerfile
┣ 📜Makefile
┣ 📜pyproject.toml - данные проекта для запуска rye
┣ 📜README.md
┣ 📜requirements-dev.lock
┗ 📜requirements.lock
# что не по ТЗ # что не по ТЗ
- мне не очень понравилось, что space_type и event_type сделаны через отдельные таблицы - мне не очень понравилось, что space_type и event_type сделаны через отдельные таблицы
- потому что работать с этой таблицей будет бэкенд, и у нас есть различные API хэндлеры, которым, чтобы создать запись в БД нужно - потому что работать с этой таблицей будет бэкенд, и у нас есть различные API хэндлеры, которым, чтобы создать запись в БД нужно
сходить на дочерние таблицы, найти нужный тип (например event_type - login), взять от него id, прийти назад и создать запись с нужным id, при этом это ещё будет не надёжным (кто-то удалит тип, поменяет название, всё поляжет) + а зачем нам отдельная таблица? (я в том смысле, что над этими типа у нас есть все операции круд, но без изменения кода бэкенда - это либо бесполезно, либо опасно) сходить на дочерние таблицы, найти нужный тип (например event_type - login), взять от него id, прийти назад и создать запись с нужным id, при этом это ещё будет не надёжным (кто-то удалит тип, поменяет название, всё поляжет) + а зачем нам отдельная таблица? (я в том смысле, что над этими типа у нас есть все операции круд, но без изменения кода бэкенда - это либо бесполезно, либо опасно)
- я заменил на более удобные Enum - я заменил на более удобные Enum
- (не знаю успею ли я закончить формирование датасета comments после ответа на вопрос)
- в ТЗ было сказано, что мне нужно сделать датасет в котором должна быть информация о том, кто кому какой коммент оставил, хотя упоминание комментов было только в логах. Если бы мне такое попалось как реальное задание, то я бы пошёл к аналитикам, и стал настаивать на создании отдельной таблицы комментариев, где было бы указано - от кого, на какой пост, текст комментария

View File

View File

@ -1,52 +0,0 @@
class RepositoryException(Exception): ...
class AccessDenied(Exception): ...
class ResultNotFound(RepositoryException): ...
class AuthorizationException(Exception): ...
class WrongCredentials(RepositoryException): ...
class ForeignKeyError(RepositoryException): ...
class JwtException(AuthorizationException): ...
class JwtExpired(JwtException): ...
class JwtInvalid(JwtException): ...
class RefreshException(AuthorizationException): ...
class RefreshExpired(RefreshException): ...
class RefreshInvalid(RefreshException): ...
class VerificationException(Exception): ...
class RefreshClientInfoIncorrect(RefreshException): ...
class FileNotFound(Exception): ...
class UserNotRegistered(Exception): ...
class PlaceOrderForeignKeyError(Exception): ...
class UserAlreadyExist(Exception): ...

View File

@ -1,142 +0,0 @@
from abc import abstractmethod
from typing import Any, Optional, Protocol
from sqlalchemy import func, insert, select, update
from sqlalchemy.exc import IntegrityError, NoResultFound
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.sql.base import ExecutableOption
from src.utils.exceptions import ForeignKeyError, ResultNotFound
_sentinel: Any = object()
class AbstractRepository(Protocol):
@abstractmethod
async def add_one(self, **data):
raise NotImplementedError
class SQLAlchemyRepository(AbstractRepository):
model = _sentinel
def __init__(self, session: AsyncSession):
self.session = session
async def add_one(self, **data):
stmt = insert(self.model).values(**data).returning(self.model)
try:
res = await self.session.execute(stmt)
return res.scalar_one()
except IntegrityError:
raise ForeignKeyError
async def edit_one(self, id: int, **data):
stmt = update(self.model).values(**data).filter_by(id=id).returning(self.model)
try:
res = await self.session.execute(stmt)
return res.unique().scalar_one()
except NoResultFound:
raise ResultNotFound
except IntegrityError:
raise ForeignKeyError
async def find_all(self):
stmt = select(self.model).options(*self.get_select_options())
res = await self.session.execute(stmt)
return res.unique().scalars().fetchall()
async def find_filtered(self, sort_by: str = "", **filter_by):
stmt = (
select(self.model)
.options(*self.get_select_options())
.filter_by(**filter_by)
.order_by(getattr(self.model, sort_by, None))
)
res = await self.session.execute(stmt)
return res.unique().scalars().fetchall()
async def find_filtered_and_paginated(self, page: int, limit: int, **filter_by):
stmt = (
select(self.model)
.options(*self.get_select_options())
.filter_by(**filter_by)
.offset((page - 1) * limit)
.limit(limit)
)
res = await self.session.execute(stmt)
return res.unique().scalars().fetchall()
async def find_one(self, **filter_by):
stmt = (
select(self.model)
.options(*self.get_select_options())
.filter_by(**filter_by)
)
res = await self.session.execute(stmt)
try:
return res.scalar_one()
except NoResultFound:
raise ResultNotFound
async def count_filtered(self, **filter_by):
stmt = (
select(func.count())
.select_from(self.model)
.options(*self.get_select_options())
.filter_by(**filter_by)
)
res = await self.session.execute(stmt)
return res.unique().scalar_one()
async def find_filtered_in(self, column_name: str, values: list):
stmt = (
select(self.model)
.options(*self.get_select_options())
.filter(getattr(self.model, column_name).in_(values))
)
res = await self.session.execute(stmt)
return res.unique().scalars().fetchall()
# this find operation in delete methods help us to raise 404 error instead of 50x
async def delete_one(self, id: int) -> None:
await self.session.delete((await self.find_one(id=id)))
async def delete_filtered(self, **filter_by) -> None:
for cart_item in await self.find_filtered(**filter_by):
await self.session.delete(cart_item)
def get_select_options(self) -> list[ExecutableOption]:
return []
async def count_filtered_by_fastadmin(
self,
joins: list[Any],
filters: list[Any],
):
stmt = select(func.count()).select_from(self.model).filter(*filters)
for join in joins:
stmt = stmt.join(join)
res = await self.session.execute(stmt)
return res.unique().scalar_one()
async def find_filtered_by_fastadmin(
self,
options: list[Any],
joins: list[Any],
filters: list[Any],
sort_by: Optional[Any],
offset: int,
limit: int,
):
stmt = select(self.model).filter(*filters).offset(offset).limit(limit)
if sort_by is not None:
stmt = stmt.order_by(sort_by)
for join in joins:
stmt = stmt.join(join)
stmt = stmt.options(*options)
res = await self.session.execute(stmt)
return res.scalars()