Skip to content

Commit

Permalink
добавил docker
Browse files Browse the repository at this point in the history
  • Loading branch information
Esposus committed Oct 30, 2023
1 parent c389158 commit 80710c8
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 167 deletions.
10 changes: 7 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
72 changes: 37 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 [email protected]: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 от создаваемого уведомления.

#### Пример уведомления в документе пользователя

Expand All @@ -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 создает новое уведомление.
Expand Down Expand Up @@ -130,18 +144,6 @@ HTTP 200 Ok
"success": true,
}
```

## На ваше усмотрение

Вам позволено решать указанные выше задачи тем способом, который вы сочтете наиболее подходящим. Кратко опишите свой подход в решении задач в Readme файле в репозитории.

## Результат выполнения задания

Данное задание будет считаться выполненным при условии размещения кода и Dockerfile'a в репозитории на github.com.

Прошу отправить результат выполнения задания на Email:
[email protected]

в теме письма укажите: "Тестовое задание"

в теле письма приложите ссылку на свой профиль на hh.ru и репозиторий на github с выполненым тестовым заданием.
## Автор
[Дмитрий Морозов](https://github.com/Esposus "GitHub аккаунт")
*телеграм: [@Vanadoo](https://t.me/Vanadoo "ссылка на телеграм")*
47 changes: 4 additions & 43 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = "[email protected]",
# 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 = """<p>Hi this test mail, thanks for using Fastapi-mail</p> """

# 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"})
9 changes: 0 additions & 9 deletions app/database.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import asyncio
from motor.motor_asyncio import AsyncIOMotorClient

import config
Expand All @@ -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())
4 changes: 3 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
@@ -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)


Expand Down
125 changes: 73 additions & 52 deletions app/routers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# API Handlers
from datetime import datetime

from fastapi import APIRouter, HTTPException, Query, status
Expand All @@ -7,7 +6,7 @@

from database import notifications_collection
from schemas import (
Notification, ListNotificationParams, ReadNotificationParams
Notification, ReadNotificationParams
)
from send_email import send_email

Expand All @@ -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}))
1 change: 0 additions & 1 deletion app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from fastapi import Query
from pydantic import BaseModel, Field, validator
from beanie import Document


class NotificationKeyEnum(str, Enum):
Expand Down
Loading

0 comments on commit 80710c8

Please sign in to comment.