From 80710c87e794a51e960451b4e190db27fae1f1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=9C=D0=BE?= =?UTF-8?q?=D1=80=D0=BE=D0=B7=D0=BE=D0=B2?= Date: Mon, 30 Oct 2023 21:17:18 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20do?= =?UTF-8?q?cker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 10 ++-- README.md | 72 +++++++++++++------------- app/config.py | 47 ++--------------- app/database.py | 9 ---- app/main.py | 4 +- app/routers.py | 125 ++++++++++++++++++++++++++------------------- app/schemas.py | 1 - app/send_email.py | 50 ++++++++++-------- docker-compose.yml | 5 +- requirements.txt | 5 ++ 10 files changed, 161 insertions(+), 167 deletions(-) create mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile index 3cda2e9..8cb1ece 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,12 @@ FROM python:3.11-slim WORKDIR /app -COPY . . +COPY requirements.txt /app/ -RUN pip install -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +COPY app /app + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md index b3ec36e..b8685ec 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,43 @@ # alertify -Mикросервис уведомления пользователей +Mикросервис уведомления пользователей, кторый позволяет создавать запись уведомления в документе пользователя в MongoDB, отправлять email, а так же предоставлять листинг уведомлений из документа пользователя. +Уведомления пользователей хранятся в поле в документе пользователя их максимальное число ограничено (лимит можно установить произвольный) -# Тестовое задание. +Для создания сервиса использовались: -Написать микросервис уведомления пользователей. +- веб-фреймворк FastAPI для создания RESTful API, предоставляющего эндпоинты для создания уведомлений, получения списка уведомлений и пометки уведомлений как прочитанных. +- MongoDB для хранения данных: +- отправка электронной почты с помощью встроенного модуля Python smtplib и настройка параметров SMTP (хост, порт, логин, пароль) через переменные окружения. +- обработка возможных ошибок и возврат соответствующих HTTP-статусов. +- использование переменных окружения для конфигурации приложения, таких как порт, параметры подключения к базе данных и SMTP-серверу. -Микросервис должен представлять из себя RestAPI сервер, который позволяет создавать запись уведомления в документе пользователя в MongoDB, отправлять email, а так же предоставлять листинг уведомлений из документа пользователя. +Установка проекта из репозитория +---------- -Уведомления пользователей должны храниться в поле в документе пользователя и их максимальное кол-во должно быть ограничено (лимит можно установить произвольный) +1. Клонируйте репозиторий и перейдите в него в командной строке: +```bash +git clone git@github.com:Esposus/alertify.git +``` +2. Перейдите в папку проекта: +```bash +cd alertify +``` +3. Заполните ```.env``` файл с переменными окружения по примеру: +```bash +echo DB_URI=mongodb://mongo:27017/mydatabase >> .env +echo PORT=8000 >> .env +echo EMAIL=<почта на которую будет приходить уведомление> >> .env +echo SMTP_HOST=smtp.gmail.com >> .env +echo SMTP_PORT= >> .env +echo SMTP_LOGIN= >> .env +echo SMTP_PASSWORD= >> .env +echo SMTP_EMAIL= >> .env +echo SMTP_NAME=Ace Place Alert >> .env +``` +4. Установите и запустите приложения в контейнерах: +```bash +docker-compose build & docker-compose up +``` -При тестировании отправки Email отпраляйте key от создаваемого уведомления. #### Пример уведомления в документе пользователя @@ -27,20 +55,6 @@ Mикросервис уведомления пользователей }, ``` -Для теста в случае отсутствия пользователя следует создать новый профиль с email, который задан через параметры. - -## Переменные окружения, через которые конфигурируется сервис - -- PORT - порт на котором будет работать приложение -- EMAIL - тестовый email -- DB_URI - строка для подключения к mongoDB -- SMTP_HOST - хост smtp сервера -- SMTP_PORT - порт smtp сервера -- SMTP_LOGIN - логин пользователя -- SMTP_PASSWORD - пароль пользователя -- SMTP_EMAIL - email с которого будет отправлено сообщение -- SMTP_NAME - Имя отображаемое у получателя письма - ## API Handlers: ### [POST] /create создает новое уведомление. @@ -130,18 +144,6 @@ HTTP 200 Ok "success": true, } ``` - -## На ваше усмотрение - -Вам позволено решать указанные выше задачи тем способом, который вы сочтете наиболее подходящим. Кратко опишите свой подход в решении задач в Readme файле в репозитории. - -## Результат выполнения задания - -Данное задание будет считаться выполненным при условии размещения кода и Dockerfile'a в репозитории на github.com. - -Прошу отправить результат выполнения задания на Email: -papkovda@me.com - -в теме письма укажите: "Тестовое задание" - -в теле письма приложите ссылку на свой профиль на hh.ru и репозиторий на github с выполненым тестовым заданием. +## Автор +[Дмитрий Морозов](https://github.com/Esposus "GitHub аккаунт") +*телеграм: [@Vanadoo](https://t.me/Vanadoo "ссылка на телеграм")* \ No newline at end of file diff --git a/app/config.py b/app/config.py index 4fa8a7b..e3dea6d 100644 --- a/app/config.py +++ b/app/config.py @@ -2,10 +2,12 @@ class Settings(BaseSettings): + app_title: str = 'Alertify' + description: str = 'Написать микросервис уведомления пользователей' model_config = SettingsConfigDict(env_file='.env') - PORT: int # uvicorn по дефолту запускает на 8000 порту, не будем ничего менять # noqa - EMAIL: str # тестовый емейл на который отправляется сообщение взят у тг бота Senthy Email https://t.me/SenthyBot # noqa + PORT: int + EMAIL: str DB_URI: str SMTP_HOST: str SMTP_PORT: int @@ -16,44 +18,3 @@ class Settings(BaseSettings): settings = Settings() - -# from fastapi import FastAPI -# from starlette.responses import JSONResponse -# from fastapi_mail import FastMail, MessageSchema, ConnectionConfig, MessageType -# from pydantic import EmailStr, BaseModel -# from typing import List - -# class EmailSchema(BaseModel): -# email: List[EmailStr] - - -# conf = ConnectionConfig( -# MAIL_USERNAME = "username", -# MAIL_PASSWORD = "**********", -# MAIL_FROM = "test@email.com", -# MAIL_PORT = 587, -# MAIL_SERVER = "mail server", -# MAIL_FROM_NAME="Desired Name", -# MAIL_STARTTLS = True, -# MAIL_SSL_TLS = False, -# USE_CREDENTIALS = True, -# VALIDATE_CERTS = True -# ) - -# app = FastAPI() - - - -# @app.post("/email") -# async def simple_send(email: EmailSchema) -> JSONResponse: -# html = """

Hi this test mail, thanks for using Fastapi-mail

""" - -# message = MessageSchema( -# subject="Fastapi-Mail module", -# recipients=email.dict().get("email"), -# body=html, -# subtype=MessageType.html) - -# fm = FastMail(conf) -# await fm.send_message(message) -# return JSONResponse(status_code=200, content={"message": "email has been sent"}) diff --git a/app/database.py b/app/database.py index 5e08d8a..1145654 100644 --- a/app/database.py +++ b/app/database.py @@ -1,4 +1,3 @@ -import asyncio from motor.motor_asyncio import AsyncIOMotorClient import config @@ -7,11 +6,3 @@ client = AsyncIOMotorClient(config.settings.DB_URI) db = client["alertify"] notifications_collection = db["notifications"] - - -async def main(): - data = notifications_collection.find({}) - print(data) - - -asyncio.run(main()) diff --git a/app/main.py b/app/main.py index 9516de8..c12260c 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,9 @@ from fastapi import FastAPI + +from config import settings from routers import router as api_router -app = FastAPI() +app = FastAPI(title=settings.app_title, description=settings.description) app.include_router(api_router) diff --git a/app/routers.py b/app/routers.py index 9c7b13c..ca5f4e3 100644 --- a/app/routers.py +++ b/app/routers.py @@ -1,4 +1,3 @@ -# API Handlers from datetime import datetime from fastapi import APIRouter, HTTPException, Query, status @@ -7,7 +6,7 @@ from database import notifications_collection from schemas import ( - Notification, ListNotificationParams, ReadNotificationParams + Notification, ReadNotificationParams ) from send_email import send_email @@ -17,62 +16,84 @@ @router.post("/create", response_model=dict) async def create_notification(notification: Notification): - notification_id = str(datetime.utcnow().timestamp()) - notification_dict = notification.model_dump() # .dict() заменил на .model_dump() # noqa - notification_dict.update( - { - "id": notification_id, - "timestamp": datetime.utcnow().timestamp(), - "is_new": True, - } - ) - notifications_collection.update_one( - {"user_id": notification.user_id}, - {"$push": {"notifications": notification_dict}}, - upsert=True - ) - - if notification.key == "registration" or notification.key == "new_login": - send_email(notification.key) - return JSONResponse(content=jsonable_encoder( - {"success": True}), status_code=status.HTTP_201_CREATED - ) - -@router.get("/list", response_model=dict) -async def list_notifications(user_id: str, skip: int = Query(0, ge=0), limit: int = Query(10, ge=1)): - user_notifications = await notifications_collection.find_one({"user_id": user_id}, {"_id": 0, "notifications": 1}) - - if not user_notifications or not user_notifications.get("notifications"): - return JSONResponse(content=jsonable_encoder({"success": True, "data": {"elements": 0, "new": 0, "list": []}})) + try: + notification_id = str(datetime.utcnow().timestamp()) + notification_dict = notification.model_dump() # .dict() заменил на .model_dump() # noqa + notification_dict.update( + { + "id": notification_id, + "timestamp": datetime.utcnow().timestamp(), + "is_new": True, + } + ) + notifications_collection.update_one( + {"user_id": notification.user_id}, + {"$push": {"notifications": notification_dict}}, + upsert=True + ) - notifications = user_notifications["notifications"] - total_elements = len(notifications) - new_elements = sum(1 for notification in notifications if notification["is_new"]) + if notification.key == "registration" or notification.key == "new_login": + send_email(notification.key) + return JSONResponse(content=jsonable_encoder( + {"success": True}), status_code=status.HTTP_201_CREATED + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Внутренняя ошибка сервера: {str(e)}' + ) - response_data = { - "elements": total_elements, - "new": new_elements, - "request": {"user_id": user_id, "skip": skip, "limit": limit}, - "list": notifications[skip:skip + limit] - } - return JSONResponse(content=jsonable_encoder({"success": True, "data": response_data})) +@router.get("/list", response_model=dict) +async def list_notifications( + user_id: str, + skip: int = Query(0, ge=0), + limit: int = Query(10, ge=1) +): + try: + user_notifications = await notifications_collection.find_one( + {"user_id": user_id}, {"_id": 0, "notifications": 1} + ) + if not user_notifications or not user_notifications.get("notifications"): + return JSONResponse( + content=jsonable_encoder( + {"success": True, "data": {"elements": 0, "new": 0, "list": []}} + ) + ) + notifications = user_notifications["notifications"] + total_elements = len(notifications) + new_elements = sum(1 for notification in notifications if notification["is_new"]) + response_data = { + "elements": total_elements, + "new": new_elements, + "request": {"user_id": user_id, "skip": skip, "limit": limit}, + "list": notifications[skip:skip + limit] + } + return JSONResponse(content=jsonable_encoder({"success": True, "data": response_data})) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Внутренняя ошибка сервера: {str(e)}' + ) @router.post("/read") async def read_notification(params: ReadNotificationParams) -> JSONResponse: - user_id = params.user_id - notification_id = params.notification_id - - result = await notifications_collection.update_one( - {"user_id": user_id, "notifications.id": notification_id}, - {"$set": {"notifications.$.is_new": False}} - ) - - if result.matched_count == 0: + try: + user_id = params.user_id + notification_id = params.notification_id + result = await notifications_collection.update_one( + {"user_id": user_id, "notifications.id": notification_id}, + {"$set": {"notifications.$.is_new": False}} + ) + if result.matched_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Сейчас уведомлений нет" + ) + return JSONResponse(content=jsonable_encoder({"success": True})) + except Exception as e: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Сейчас уведомлений нет" + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Внутренняя ошибка сервера: {str(e)}' ) - - return JSONResponse(content=jsonable_encoder({"success": True})) diff --git a/app/schemas.py b/app/schemas.py index b5b831e..208390b 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -2,7 +2,6 @@ from fastapi import Query from pydantic import BaseModel, Field, validator -from beanie import Document class NotificationKeyEnum(str, Enum): diff --git a/app/send_email.py b/app/send_email.py index 778037a..e2795bf 100644 --- a/app/send_email.py +++ b/app/send_email.py @@ -1,38 +1,44 @@ import smtplib from email.message import EmailMessage -# from celery import Celery +from fastapi import HTTPException, status from config import settings -SMTP_HOST = "smtp.gmail.com" -SMTP_PORT = 465 - -# celery = Celery('tasks', broker='redis://localhost:6379') - -# async def send_email(): -# pass - - -# email, "Notification", f"You have a new notification: {notification.key}" def set_email_content(notification: str): email = EmailMessage() - email['Subject'] = 'Notification' + email['Subject'] = settings.SMTP_NAME email['From'] = settings.SMTP_LOGIN email['To'] = settings.EMAIL - email.set_content( - '
' - f'

Вы получили новое уведомление: {notification}

' - '
', - subtype='html' - ) + if notification == 'registration': + email.set_content( + '
' + '

Регистрация прошла успешно

' + f'статус: {notification}' + '
', + subtype='html' + ) + elif notification == 'new_login': + email.set_content( + '
' + '

Ваш логин обновлен!

' + f'статус: {notification}' + '
', + subtype='html' + ) return email def send_email(notification: str): - email = set_email_content(notification) - with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as server: - server.login(settings.SMTP_LOGIN, settings.SMTP_PASSWORD) - server.send_message(email) \ No newline at end of file + try: + email = set_email_content(notification) + with smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT) as server: + server.login(settings.SMTP_LOGIN, settings.SMTP_PASSWORD) + server.send_message(email) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f'Ошибка при отправке электронной почты: {str(e)}' + ) diff --git a/docker-compose.yml b/docker-compose.yml index 03a5308..c42ad47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: environment: MONGO_INITDB_DATABASE: alertifyprojectdb volumes: - - mongo-data:/data/db + - mongo:/data/db app: build: @@ -21,3 +21,6 @@ services: - mongo env_file: - .env + +volumes: + mongo: \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6527d71 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi[all]==0.104.0 +uvicorn[standard]==0.23.2 +pymongo==4.5.0 +motor==3.3.1 +