makeup readme and clean unnecessary files
This commit is contained in:
parent
03c4941af0
commit
9b096057c0
100
.gitlab-ci.yml
100
.gitlab-ci.yml
@ -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"
|
39
README.md
39
README.md
@ -1,15 +1,50 @@
|
||||
# python_dev
|
||||
|
||||
# launch
|
||||
# Запуск
|
||||
```cp .env.example .env```
|
||||
```docker compose up -d```
|
||||
|
||||
# migrations
|
||||
# миграции БД
|
||||
```cp .env.example .env```
|
||||
```cd alembic/db1 && 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 сделаны через отдельные таблицы
|
||||
- потому что работать с этой таблицей будет бэкенд, и у нас есть различные API хэндлеры, которым, чтобы создать запись в БД нужно
|
||||
сходить на дочерние таблицы, найти нужный тип (например event_type - login), взять от него id, прийти назад и создать запись с нужным id, при этом это ещё будет не надёжным (кто-то удалит тип, поменяет название, всё поляжет) + а зачем нам отдельная таблица? (я в том смысле, что над этими типа у нас есть все операции круд, но без изменения кода бэкенда - это либо бесполезно, либо опасно)
|
||||
- я заменил на более удобные Enum
|
||||
|
||||
- (не знаю успею ли я закончить формирование датасета comments после ответа на вопрос)
|
||||
- в ТЗ было сказано, что мне нужно сделать датасет в котором должна быть информация о том, кто кому какой коммент оставил, хотя упоминание комментов было только в логах. Если бы мне такое попалось как реальное задание, то я бы пошёл к аналитикам, и стал настаивать на создании отдельной таблицы комментариев, где было бы указано - от кого, на какой пост, текст комментария
|
||||
|
@ -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): ...
|
@ -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()
|
Loading…
x
Reference in New Issue
Block a user