diff --git a/.env b/.env new file mode 100644 index 0000000..1e2f09a --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +DISABLE_DOCS=true + +MONGO_HOST=127.0.0.1 +MONGO_PORT=27017 +MONGO_USER=user +MONGO_PASS=pass \ No newline at end of file diff --git a/.github/workflows/app-actions.yml b/.github/workflows/app-actions.yml new file mode 100644 index 0000000..9af1516 --- /dev/null +++ b/.github/workflows/app-actions.yml @@ -0,0 +1,67 @@ +name: app-actions + +on: + workflow_dispatch: + push: + paths: + - '**.py' + branches: + - main + - app + pull_request: + branches: + - main + + +jobs: + mypy: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.9 + architecture: x64 + + - name: Install dependencies + run: | + pip install mypy + pip install -r app/requirements.txt + + - name: Run mypy + run: mypy --ignore-missing-imports ./app + + pytest: + runs-on: ubuntu-latest + env: + MONGO_HOST: ${{ secrets.MONGO_HOST }} + MONGO_PORT: ${{ secrets.MONGO_PORT }} + MONGO_USER: ${{ secrets.MONGO_USER }} + MONGO_PASS: ${{ secrets.MONGO_PASS }} + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.9 + architecture: x64 + + - name: Setup MongoDB + run: docker run --name mongodb -d -p ${{ secrets.MONGO_PORT }}:27017 -e MONGO_INITDB_ROOT_USERNAME=${{ secrets.MONGO_USER }} -e MONGO_INITDB_ROOT_PASSWORD=${{ secrets.MONGO_PASS }} mongodb/mongodb-community-server + + - name: Install dependencies + run: | + pip install pytest==8.3.4 pytest-asyncio==0.25.3 httpx==0.28.1 + pip install -r app/requirements.txt + - name: Setup moderated base cards + + working-directory: ./app/tools + run: python3 _add_base_cards.py -a 2 -f data/base_cards.json + - name: Run pytest + run: pytest -vs \ No newline at end of file diff --git a/.github/workflows/mypy-test.yml b/.github/workflows/mypy-test.yml deleted file mode 100644 index 0ac5f6c..0000000 --- a/.github/workflows/mypy-test.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: mypy-test - -on: - push: - paths: - - '**.py' - -jobs: - mypy: - runs-on: ubuntu-latest - steps: - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: 3.9 - architecture: x64 - - name: Checkout - uses: actions/checkout@v3 - - name: Install mypy - run: pip install mypy - - name: Run mypy - run: mypy --ignore-missing-imports ./app diff --git a/.gitignore b/.gitignore index a3c2255..e07ccd3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.py[cod] *$py.class +.idea/ # C extensions *.so @@ -170,28 +171,4 @@ cython_debug/ # PyPI configuration file .pypirc - - - -# React -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* +app/.env diff --git a/README.md b/README.md index 1b148f7..b75646a 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,23 @@ Telegram mini-app where you have to choose one of two things. git clone https://github.com/IgorVolochay/thisORthat ``` +### Manual setup: + 2. The project is written in Python3.9. Make sure you have it on your system. Go to the project folder, create a virtual environment and download pip requirements: ```bash cd ./thisORthat python3.9 -m venv venv source ./venv/bin/activate -pip3 install -r requirements.txt +pip3 install -r ./app/requirements.txt ``` 3. Installing MongoDB database. You can use the [official manual](https://www.mongodb.com/docs/manual/installation/) to install MongoDB manually, or use a [Docker image](https://hub.docker.com/r/mongodb/mongodb-community-server) to run the container: ```bash -docker run --name mongodb -d -p 27017:27017 mongodb/mongodb-community-server +docker run --name mongodb -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=user -e MONGO_INITDB_ROOT_PASSWORD=pass mongodb/mongodb-community-server ``` +### Docker Compose setup: + +2. Use docker-compose to automatically build the entire project. For correct build, it is better to use docker-compose version 1.29.2: +```bash +docker-compose up --build +``` \ No newline at end of file diff --git a/app/dockerfile b/app/dockerfile new file mode 100644 index 0000000..281fd13 --- /dev/null +++ b/app/dockerfile @@ -0,0 +1,11 @@ +FROM python:3.9.21-alpine + +WORKDIR /app + +COPY . . + +RUN pip3 install -r requirements.txt + +EXPOSE 5000 + +CMD ["python3", "main.py"] \ No newline at end of file diff --git a/app/main.py b/app/main.py index 5a11942..e5cbb3e 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,191 @@ +import os + +import uvicorn +import asyncio + +from dotenv import load_dotenv +from fastapi import FastAPI, Depends, Response, status + +from schemas.api_schemas import * +from schemas.base_schemas import * from mongo_worker import MongoWorker +from tools.base_moderation import moderate_text + + +load_dotenv() +disable_docs = os.getenv("DISABLE_DOCS", "true").lower() == "true" + +app: FastAPI = FastAPI(title="This OR That", + summary="OpenAPI schema for \"This OR That\" project!", + version="0.1", + contact={"GitHub": "https://github.com/IgorVolochay/thisORthat"}, + docs_url=None if disable_docs else "/docs", + redoc_url=None if disable_docs else "/redoc", + openapi_url=None if disable_docs else "/openapi.json") +mongo_worker = MongoWorker() + + +@app.get("/check_user", status_code=200) +async def check_user(user_id: NonNegativeInt, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + result = mongo.check_user(user_id) + return BaseResponse(result=result) + +@app.get("/get_user", status_code=200) +async def get_user(user_id: NonNegativeInt, + response: Response, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + if mongo.check_user(user_id): + result = mongo.get_user(user_id) + return BaseResponse(result=result) + else: + response.status_code = status.HTTP_404_NOT_FOUND + return BaseResponse(result="User doesn't exist", error=True) + +@app.post("/add_user", status_code=201) +async def add_user(new_user: AddUserBody, + response: Response, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + if not mongo.check_user(new_user.user_id): + result = mongo.add_user(new_user.user_id, + new_user.username, + new_user.first_name, + new_user.last_name, + new_user.photo_url) + return BaseResponse(result=result) + else: + response.status_code = status.HTTP_409_CONFLICT + return BaseResponse(result="User already exist", error=True) + + +@app.get("/get_card", status_code=200) +async def get_card(card_id: NonNegativeInt, + response: Response, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + card = mongo.get_card(card_id) + if card: + return BaseResponse(result=card) + else: + response.status_code = status.HTTP_404_NOT_FOUND + return BaseResponse(result="There is no card with this card_id", error=True) + +@app.get("/get_random_cards", status_code=200) +async def get_random_cards(user_id: NonNegativeInt, + response: Response, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + cards_visited = mongo.get_visited_cards(user_id) + + if cards_visited.error: + response.status_code = status.HTTP_401_UNAUTHORIZED + return cards_visited + elif not cards_visited.result.cards_visited: + random_cards = mongo.get_random_cards(10, True) + if random_cards: + return BaseResponse(result=random_cards) + else: + response.status_code = status.HTTP_404_NOT_FOUND + return BaseResponse(result="No active cards", error=True) + + result: list[Card] = list() + trys = 3 + while len(result) < 10 and trys != 0: + random_cards = mongo.get_random_cards(10, True) + if not random_cards: + response.status_code = status.HTTP_404_NOT_FOUND + return BaseResponse(result="No active cards", error=True) + filtered_cards, filtered_cards_id = mongo.filter_cards(random_cards, cards_visited.result.cards_visited) + trys -= 1 + if not filtered_cards: + continue + else: + result.extend(filtered_cards) + cards_visited.result.cards_visited.update(filtered_cards_id) + + if not result: + response.status_code = status.HTTP_404_NOT_FOUND + return BaseResponse(result="No active cards fo this user", error=True) + else: + return BaseResponse(result=result) + +@app.post("/add_card", status_code=201) +async def add_card(new_card: AddCardBody, + response: Response, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + if moderate_text(new_card.choice_A) and moderate_text(new_card.choice_B): + card = mongo.add_card_by_api(new_card.choice_A, + new_card.choice_B, + new_card.author_id) + return BaseResponse(result=card) + else: + response.status_code = status.HTTP_400_BAD_REQUEST + return BaseResponse(result="Card has not passed base moderation", error=True) + + +@app.patch("/select_choice", status_code=200) +async def select_choice(choice_data: SelectChoice, + response: Response, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + check_visited = mongo.get_visited_cards(choice_data.user_id) + if check_visited.error: + response.status_code = status.HTTP_404_NOT_FOUND + return check_visited + elif not check_visited.error and choice_data.card_id in check_visited.result.cards_visited: + response.status_code = status.HTTP_403_FORBIDDEN + return BaseResponse(result="Card already visited!", error=True) + else: + select_choice_result = mongo.select_choice(choice_data.card_id, choice_data.choice) + if select_choice_result.error: + response.status_code = status.HTTP_404_NOT_FOUND + return select_choice_result + else: + update_visited_result = mongo.update_visited_cards(choice_data.user_id, choice_data.card_id) + return BaseResponse(result="Select choice complite!") + +@app.patch("/like_card", status_code=200) +async def like_card(like_data: ReactionCard, + response: Response, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + result = mongo.like_card(like_data.card_id, like_data.user_id) + if not result.error and result.result: + return BaseResponse(result="Added like to card") + else: + response.status_code = status.HTTP_404_NOT_FOUND + return result + +@app.patch("/dislike_card", status_code=200) +async def dislike_card(dislike_data: ReactionCard, + response: Response, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + result = mongo.dislike_card(dislike_data.card_id, dislike_data.user_id) + if not result.error and result.result: + return BaseResponse(result="Added dislike to card") + else: + response.status_code = status.HTTP_404_NOT_FOUND + return result + +@app.post("/comment", status_code=201) +async def comment(comment_info: AddCommentBody, + response: Response, + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: + if not moderate_text(comment_info.comment_text): + response.status_code = status.HTTP_400_BAD_REQUEST + return BaseResponse(result="Comment has not passed base moderation", error=True) + + result = mongo.add_comment(comment_info.author_id, comment_info.card_id, comment_info.comment_text) + if result.error and result.result in ["User doesn't exist", "Card doesn't exist"]: + response.status_code = status.HTTP_404_NOT_FOUND + return result + elif result.error: + response.status_code = status.HTTP_400_BAD_REQUEST + return result + else: + return result + + +async def main(): + config = uvicorn.Config("main:app", port=5000, log_level="debug") + server = uvicorn.Server(config) + await server.serve() if __name__ == "__main__": - print("Hello World!") \ No newline at end of file + asyncio.run(main()) \ No newline at end of file diff --git a/app/mongo_worker.py b/app/mongo_worker.py index e165b4b..d3133a4 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -1,45 +1,214 @@ +import os + import pymongo from datetime import datetime +from dotenv import load_dotenv +from typing import Optional -from bson import ObjectId +from schemas.base_schemas import * +from schemas.api_schemas import * class MongoWorker: def __init__(self): - self.client = pymongo.MongoClient(host="127.0.0.1", port=27017) + load_dotenv() + self.client = pymongo.MongoClient(host = os.getenv('MONGO_HOST'), + port = int(os.getenv('MONGO_PORT')), + username = os.getenv('MONGO_USER'), + password = os.getenv('MONGO_PASS')) self.db = self.client["data"] - self.users_data = self.db["users_data"] - self.game_data = self.db["game_data"] + self.users_data = self.db["users"] + self.visited_data = self.db["visited"] + self.counters = self.db["counters"] + self.game_data = self.db["cards"] + self.comments_data = self.db["comments"] - def get_mongodb_info(self) -> None: - print(self.client.list_database_names()) - print(self.db.list_collection_names()) - def find_user(self, user_id: int) -> dict: - result = self.users_data.find_one({"user_id": user_id}) - return result + def check_user(self, user_id: int) -> bool: + if self.users_data.find_one({"user_id": user_id}): + return True + else: + return False - def add_user(self, user_id: int, username: str, first_name: str, last_name: str, photo_path: str) -> ObjectId: + def add_user(self, user_id: int, username: str, first_name: str, last_name: str, photo_url: str) -> User: + new_user = User(user_id=user_id, + username=username, + first_name=first_name, + last_name=last_name, + photo_url=photo_url, + registration_date=datetime.now().isoformat()) try: - res = self.users_data.insert_one({"user_id": user_id, - "username": username, - "first_name": first_name, - "last_name": last_name, - "photo_path": photo_path, - "activity": 0, - "liked_post_ids": [], - "disliked_post_ids": [], - "comments_ids": [], - "registration_date": datetime.now().isoformat() - }) - return res.inserted_id + self.users_data.insert_one(new_user.model_dump()) + return new_user + except Exception as exception: + return new_user + + def get_user(self, user_id: int) -> User: + return User.model_validate(self.users_data.find_one({"user_id": user_id})) + + + def get_and_update_counter(self, counter_name: str) -> int: + counter = self.counters.find_one_and_update( + {"counter_name": counter_name}, + {"$inc": {"counter": 1}}, + upsert=True, + return_document=True) + return counter["counter"] + + + def get_visited_cards(self, user_id: int) -> BaseResponse: + document = self.visited_data.find_one({"user_id": user_id}) + if not document: + check_user = self.check_user(user_id) + if check_user: + return BaseResponse(result=Visited(user_id=user_id, + cards_visited=set())) + else: + return BaseResponse(result="User doesn't exist", error=True) + else: + return BaseResponse(result=Visited.model_validate(document)) + + def filter_cards(self, random_cards: list[Card], cards_visited: set) -> tuple[list[Card], list[int]]: + filtered_cards = [card for card in random_cards if card.card_id not in cards_visited] + filtered_cards_id = [filtered_card.card_id for filtered_card in filtered_cards] + + return filtered_cards, filtered_cards_id + + def update_visited_cards(self, user_id: int, visited_card_id: int) -> Visited: + update_visited = self.visited_data.find_one_and_update({"user_id": user_id}, + {"$addToSet": {"cards_visited": visited_card_id}}, + upsert=True, + return_document=True) + return Visited.model_validate(update_visited) + + + def add_card_by_api(self, choice_A: str, choice_B: str, author_id: int) -> Card: + new_card = Card(card_id=self.get_and_update_counter(counter_name="card"), + choice_A=choice_A, + choice_B=choice_B, + author_id=author_id, + creation_date=datetime.now().isoformat()) + try: + self.game_data.insert_one(new_card.model_dump()) + return new_card except Exception as exception: print(exception) + return new_card + + def add_card_by_base_model(self, new_card: Card) -> Optional[Card]: + new_card.card_id = self.get_and_update_counter(counter_name="card") + try: + self.game_data.insert_one(new_card.model_dump()) + return new_card + except Exception as exception: + print(exception) + return new_card + + def get_card(self, card_id: int) -> Optional[Card]: + document = self.game_data.find_one({"card_id": card_id}) + if document: + return Card.model_validate(document) + else: + return None + def get_random_cards(self, amount: int, active_status: bool) -> Optional[list[Card]]: + pipeline = [{"$match": {"active_status": active_status}}, + {"$sample": {"size": amount}}] + raw_items = list(self.game_data.aggregate(pipeline)) -if __name__ == "__main__": - mongo = MongoWorker() - mongo.get_mongodb_info() - print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "path/to/img")) - print(mongo.find_user(123)) + if raw_items: + validated_items = [Card.model_validate(item) for item in raw_items] + return validated_items + else: + return None + + + def select_choice(self, card_id: int, choice: str) -> BaseResponse: + if choice == "A": + count_choice = "count_choice_A" + elif choice == "B": + count_choice = "count_choice_B" + else: + return BaseResponse(result="Wrong choice", error=True) + + result = self.game_data.find_one_and_update({"card_id": card_id}, + {"$inc": {"count_total": 1, count_choice: 1}}) + + if not result: + return BaseResponse(result="Card doesn't exist", error=True) + else: + return BaseResponse(result=result, error=False) + + def check_user_reactions(self, user_id: int, card_id: int) -> BaseResponse: + user_info: User = self.get_user(user_id) + liked_card_ids: list = user_info.liked_card_ids + disliked_card_ids: list = user_info.disliked_card_ids + + if card_id in liked_card_ids: + return BaseResponse(result="Card already liked", error=True) + elif card_id in disliked_card_ids: + return BaseResponse(result="Card already disliked", error=True) + else: + return BaseResponse(result="No reactions", error=False) + + def like_card(self, card_id: int, user_id: int) -> BaseResponse: + if self.check_user(user_id): + user_reaction = self.check_user_reactions(user_id, card_id) + if user_reaction.error: + return user_reaction + update_card_info = self.game_data.find_one_and_update({"card_id": card_id}, + {"$inc": {"count_likes": 1}}) + if not update_card_info: + return BaseResponse(result="Card doesn't exist", error=True) + + add_card_to_user = self.users_data.update_one({'user_id': user_id}, + {'$push': {'liked_card_ids': card_id}}) + if not add_card_to_user: + return BaseResponse(result="User doesn't exist", error=True) + else: + return BaseResponse(result=True, error=False) + else: + return BaseResponse(result="User doesn't exist", error=True) + + def dislike_card(self, card_id: int, user_id: int) -> BaseResponse: + if self.check_user(user_id): + user_reaction = self.check_user_reactions(user_id, card_id) + if user_reaction.error: + return user_reaction + update_card_info = self.game_data.find_one_and_update({"card_id": card_id}, + {"$inc": {"count_dislikes": 1}}) + if not update_card_info: + return BaseResponse(result="Card doesn't exist", error=True) + + add_card_to_user = self.users_data.update_one({'user_id': user_id}, + {'$push': {'disliked_card_ids': card_id}}) + if not add_card_to_user: + return BaseResponse(result="User doesn't exist", error=True) + else: + return BaseResponse(result=True, error=False) + else: + return BaseResponse(result="User doesn't exist", error=True) + + def add_comment(self, user_id: int, card_id: int, comment_text: str) -> BaseResponse: + if self.check_user(user_id): + if self.get_card(card_id): + new_comment = Comment(comment_id=self.get_and_update_counter(counter_name="comment"), + author_id=user_id, + card_id=card_id, + commet_text=comment_text, + creation_date=datetime.now().isoformat()) + result = self.comments_data.insert_one(new_comment.model_dump()) + if result: + update_user_comments = self.users_data.find_one_and_update({"user_id": user_id}, + {"$addToSet": {"comments_ids": new_comment.comment_id}}) + if update_user_comments: + return BaseResponse(result=new_comment) + else: + return BaseResponse(result="Difficulty adding comment_id to user", error=True) + else: + return BaseResponse(result="Add comment error", error=True) + else: + return BaseResponse(result="Card doesn't exist", error=True) + else: + return BaseResponse(result="User doesn't exist", error=True) \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..f0ee27b --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,4 @@ +fastapi==0.115.7 +pymongo==4.10.1 +python-dotenv==1.0.1 +uvicorn==0.34.0 \ No newline at end of file diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schemas/api_schemas.py b/app/schemas/api_schemas.py new file mode 100644 index 0000000..745d791 --- /dev/null +++ b/app/schemas/api_schemas.py @@ -0,0 +1,38 @@ +import typing + +from pydantic import BaseModel, NonNegativeInt + + +class BaseResponse(BaseModel): + result: typing.Any + error: bool = False + +class AddUserBody(BaseModel): + user_id: NonNegativeInt + username: str + + first_name: str + last_name: str + photo_url: str + +class AddCardBody(BaseModel): + choice_A: str + choice_B: str + + author_id: NonNegativeInt + +class SelectChoice(BaseModel): + user_id: NonNegativeInt + card_id: NonNegativeInt + + choice: typing.Literal["A", "B"] + +class ReactionCard(BaseModel): + user_id: NonNegativeInt + card_id: NonNegativeInt + +class AddCommentBody(BaseModel): + author_id: NonNegativeInt + card_id: NonNegativeInt + + comment_text: str \ No newline at end of file diff --git a/app/schemas/base_schemas.py b/app/schemas/base_schemas.py new file mode 100644 index 0000000..52f5464 --- /dev/null +++ b/app/schemas/base_schemas.py @@ -0,0 +1,48 @@ +from pydantic import BaseModel + +class User(BaseModel): + user_id: int + username: str + + first_name: str + last_name: str + photo_url: str + + activity: int = 0 + liked_card_ids: list[int] = list() + disliked_card_ids: list[int] = list() + comments_ids: list[int] = list() + + registration_date: str + +class Visited(BaseModel): + user_id: int + cards_visited: set[int] + +class Card(BaseModel): + card_id: int + + choice_A: str + choice_B: str + + count_choice_A: int = 0 + count_choice_B: int = 0 + count_total: int = 0 + + count_likes: int = 0 + count_dislikes: int = 0 + comments: list[int] = list() + + author_id: int + creation_date: str + moderation_date: str = "Not moderated" + active_status: bool = False + +class Comment(BaseModel): + comment_id: int + + author_id: int + card_id: int + commet_text: str + + creation_date: str \ No newline at end of file diff --git a/app/tests/__init__.py b/app/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/test_cards.py b/app/tests/test_cards.py new file mode 100644 index 0000000..1e0cbab --- /dev/null +++ b/app/tests/test_cards.py @@ -0,0 +1,186 @@ +import random +import asyncio +import pytest +from httpx import AsyncClient, ASGITransport + +from main import app +from schemas.base_schemas import * +from schemas.api_schemas import * + + +EXIST_AUTHOR = random.randint(100000000, 1000000000) +NON_EXIST_CARD_ID = 1000 + + +# ---------- /add_card ---------- + +@pytest.mark.asyncio +async def test_add_card_valid(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + payload = { + "choice_A": "Option A", + "choice_B": "Option B", + "author_id": EXIST_AUTHOR + } + response = await client.post("/add_card", json=payload) + print(f"\nINPUT: endpoint=/add_card | payload={payload}\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code in (200, 201) + base_resp = BaseResponse.model_validate(response.json()) + assert base_resp.error is False + card = Card.model_validate(base_resp.result) + assert card.choice_A == payload["choice_A"] + assert card.choice_B == payload["choice_B"] + assert card.author_id == payload["author_id"] + +@pytest.mark.asyncio +async def test_add_card_missing_field(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + payload = { + #choice_A + "choice_B": "Option B", + "author_id": EXIST_AUTHOR + } + response = await client.post("/add_card", json=payload) + print(f"\nINPUT: endpoint=/add_card | payload (missing field)={payload}\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code == 422 + +@pytest.mark.asyncio +async def test_add_card_wrong_type(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + payload = { + "choice_A": 123, + "choice_B": "Option B", + "author_id": EXIST_AUTHOR + } + response = await client.post("/add_card", json=payload) + print(f"\nINPUT: endpoint=/add_card | payload (wrong type)={payload}\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code == 422 + +@pytest.mark.asyncio +async def test_add_card_empty_strings(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + payload = { + "choice_A": "", + "choice_B": "", + "author_id": EXIST_AUTHOR + } + response = await client.post("/add_card", json=payload) + print(f"\nINPUT: endpoint=/add_card | payload (empty strings)={payload}\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code == 400 + +@pytest.mark.asyncio +async def test_add_card_long_strings(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + long_str = "A" * 5000 # long string + payload = { + "choice_A": long_str, + "choice_B": long_str, + "author_id": EXIST_AUTHOR + } + response = await client.post("/add_card", json=payload) + print(f"\nINPUT: endpoint=/add_card | payload with long strings (length={len(long_str)})\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code == 400 + +@pytest.mark.asyncio +async def test_add_card_negative_author_id(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + payload = { + "choice_A": "Option A", + "choice_B": "Option B", + "author_id": -10 # Negative id + } + response = await client.post("/add_card", json=payload) + print(f"\nINPUT: endpoint=/add_card | payload (negative author_id)={payload}\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code == 422 + +@pytest.mark.asyncio +async def test_add_card_malformed_json(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + malformed_json = '{"choice_A": "Option A", "choice_B": "Option B", "author_id": 123' # broken json + response = await client.post( + "/add_card", + data=malformed_json, + headers={"Content-Type": "application/json"} + ) + print(f"\nINPUT: endpoint=/add_card | payload (malformed JSON)={malformed_json}\nOUTPUT: status={response.status_code} | json={response.json() if response.content else 'No JSON'}") + assert response.status_code == 422 + +@pytest.mark.asyncio +async def test_async_card_creation(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + tasks = [] + num_cards = 8 + for i in range(num_cards): + payload = { + "choice_A": f"Async Option A {i}", + "choice_B": f"Async Option B {i}", + "author_id": EXIST_AUTHOR + } + tasks.append(client.post("/add_card", json=payload)) + responses = await asyncio.gather(*tasks) + + card_ids = [] + for idx, response in enumerate(responses): + print(f"\nAsync creation {idx}: status={response.status_code}, response={response.json()}") + assert response.status_code in (200, 201) + base_resp = BaseResponse.model_validate(response.json()) + assert base_resp.error is False + card = Card.model_validate(base_resp.result) + card_ids.append(card.card_id) + assert card.choice_A == f"Async Option A {idx}" + assert card.choice_B == f"Async Option B {idx}" + assert card.author_id == EXIST_AUTHOR + + assert len(set(card_ids)) == num_cards + + +# ---------- /get_card ---------- + +@pytest.mark.asyncio +async def test_get_card_valid(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + payload = { + "choice_A": "GetTest A", + "choice_B": "GetTest B", + "author_id": EXIST_AUTHOR + } + create_resp = await client.post("/add_card", json=payload) + base_create = BaseResponse.model_validate(create_resp.json()) + card = Card.model_validate(base_create.result) + card_id = card.card_id + + response = await client.get("/get_card", params={"card_id": card_id}) + print(f"\nINPUT: endpoint=/get_card | params={{'card_id': {card_id}}}\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code == 200 + base_resp = BaseResponse.model_validate(response.json()) + assert base_resp.error is False + card_from_get = Card.model_validate(base_resp.result) + assert card_from_get.card_id == card_id + +@pytest.mark.asyncio +async def test_get_card_nonexistent(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get("/get_card", params={"card_id": NON_EXIST_CARD_ID}) + print(f"\nINPUT: endpoint=/get_card | params={{'card_id': {NON_EXIST_CARD_ID}}}\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code == 404 + +@pytest.mark.asyncio +async def test_get_card_missing_param(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get("/get_card") + print(f"\nINPUT: endpoint=/get_card (missing card_id param)\nOUTPUT: status={response.status_code} | json={response.json() if response.content else 'No content'}") + assert response.status_code == 422 + +@pytest.mark.asyncio +async def test_get_card_wrong_type(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get("/get_card", params={"card_id": "abc"}) + print(f"\nINPUT: endpoint=/get_card | params={{'card_id': 'abc'}}\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code == 422 + +@pytest.mark.asyncio +async def test_get_card_negative(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + response = await client.get("/get_card", params={"card_id": -10}) + print(f"\nINPUT: endpoint=/get_card | params={{'card_id': -10}}\nOUTPUT: status={response.status_code} | json={response.json()}") + assert response.status_code == 422 \ No newline at end of file diff --git a/app/tests/test_user_info.py b/app/tests/test_user_info.py new file mode 100644 index 0000000..67f52c4 --- /dev/null +++ b/app/tests/test_user_info.py @@ -0,0 +1,154 @@ +import random + +import pytest + +from main import app +from schemas.api_schemas import * +from schemas.base_schemas import * + +from httpx import AsyncClient, ASGITransport + + +EXIST_USER = random.randint(100000000, 1000000000) +NON_EXIST_USER = random.randint(100000000, 1000000000) + + + +# TEST ADD USERS UTILS # + +@pytest.mark.asyncio +async def test_add_user_non_full_data(): + async with AsyncClient(transport=ASGITransport(app=app), + base_url='http://test') as client: + end_point = "/add_user" + data = { + "user_id": EXIST_USER, + "username": "TestUsername", + } + raw_response = await client.post(url=end_point,json=data) + print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}") + + assert raw_response.status_code == 422 + +@pytest.mark.asyncio +async def test_add_user_negative_int_id(): + async with AsyncClient(transport=ASGITransport(app=app), + base_url='http://test') as client: + end_point = "/add_user" + data = { + "user_id": -1, + "username": "TestUsername", + "first_name": "FName", + "last_name": "LName", + "photo_url": "http://test.test/photo.jpg" + } + raw_response = await client.post(url=end_point,json=data) + print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}") + + assert raw_response.status_code == 422 + +@pytest.mark.asyncio +async def test_add_new_user(): + async with AsyncClient(transport=ASGITransport(app=app), + base_url='http://test') as client: + end_point = "/add_user" + data = { + "user_id": EXIST_USER, + "username": "TestUsername", + "first_name": "FName", + "last_name": "LName", + "photo_url": "http://test.test/photo.jpg" + } + raw_response = await client.post(url=end_point,json=data) + print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}") + + assert raw_response.status_code == 201 + response = BaseResponse.model_validate(raw_response.json()) + assert response.error == False + assert User.model_validate(response.result) + +@pytest.mark.asyncio +async def test_add_already_exist_user(): + async with AsyncClient(transport=ASGITransport(app=app), + base_url='http://test') as client: + end_point = "/add_user" + data = { + "user_id": EXIST_USER, + "username": "TestUsername", + "first_name": "FName", + "last_name": "LName", + "photo_url": "http://test.test/photo.jpg" + } + raw_response = await client.post(url=end_point,json=data) + print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}") + + assert raw_response.status_code == 409 + response = BaseResponse.model_validate(raw_response.json()) + assert response.error == True + assert response.result == "User already exist" + + + + +# TEST CHECK USERS UTILS # + +@pytest.mark.asyncio +async def test_check_non_exist_user(): + async with AsyncClient(transport=ASGITransport(app=app), + base_url='http://test') as client: + end_point = "/check_user" + params = {"user_id": NON_EXIST_USER} + raw_response = await client.get(url=end_point, params=params) + print(f"\nINPUT: endpiont={end_point} | params={params}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}") + + assert raw_response.status_code == 200 + response = BaseResponse.model_validate(raw_response.json()) + assert response.error == False + assert response.result == False + +@pytest.mark.asyncio +async def test_check_exist_user(): + async with AsyncClient(transport=ASGITransport(app=app), + base_url='http://test') as client: + end_point = "/check_user" + params = {"user_id": EXIST_USER} + raw_response = await client.get(url=end_point, params=params) + print(f"\nINPUT: endpiont={end_point} | params={params}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}") + + assert raw_response.status_code == 200 + response = BaseResponse.model_validate(raw_response.json()) + assert response.error == False + assert response.result == True + + + + +# TEST GET USERS UTILS # + +@pytest.mark.asyncio +async def test_get_non_exist_user(): + async with AsyncClient(transport=ASGITransport(app=app), + base_url='http://test') as client: + end_point = "/get_user" + params = {"user_id": NON_EXIST_USER} + raw_response = await client.get(url=end_point, params=params) + print(f"\nINPUT: endpiont={end_point} | params={params}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}") + + assert raw_response.status_code == 404 + response = BaseResponse.model_validate(raw_response.json()) + assert response.error == True + assert response.result == "User doesn't exist" + +@pytest.mark.asyncio +async def test_get_exist_user(): + async with AsyncClient(transport=ASGITransport(app=app), + base_url='http://test') as client: + end_point = "/get_user" + params = {"user_id": EXIST_USER} + raw_response = await client.get(url=end_point, params=params) + print(f"\nINPUT: endpiont={end_point} | params={params}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}") + + assert raw_response.status_code == 200 + response = BaseResponse.model_validate(raw_response.json()) + assert response.error == False + assert User.model_validate(response.result) \ No newline at end of file diff --git a/app/tests/test_visited_cards.py b/app/tests/test_visited_cards.py new file mode 100644 index 0000000..62ea676 --- /dev/null +++ b/app/tests/test_visited_cards.py @@ -0,0 +1,92 @@ +import random +import asyncio +import pytest +from httpx import AsyncClient, ASGITransport + +from main import app +from schemas.base_schemas import * +from schemas.api_schemas import * + +EXIST_USER = random.randint(100000000, 1000000000) +EXIST_AUTHOR = random.randint(100000000, 1000000000) +NO_ACTIVE_CARDS_STATUS = False +ACTIVE_CARDS_LESS_THAN_TEN = False + +# ------------- /add_user --------------- + +@pytest.mark.asyncio +async def test_add_new_user(): + async with AsyncClient(transport=ASGITransport(app=app), + base_url='http://test') as client: + end_point = "/add_user" + data = { + "user_id": EXIST_USER, + "username": "TestUsername", + "first_name": "FName", + "last_name": "LName", + "photo_url": "http://test.test/photo.jpg" + } + raw_response = await client.post(url=end_point,json=data) + print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}") + + assert raw_response.status_code == 201 + response = BaseResponse.model_validate(raw_response.json()) + assert response.error == False + assert User.model_validate(response.result) + +# ---------- /get_random_cards ---------- + +@pytest.mark.asyncio +async def test_get_random_cards_valid(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + params = {"user_id": EXIST_USER} + response = await client.get("/get_random_cards", params=params) + print(f"\nINPUT: endpoint=/get_random_cards\nOUTPUT: status={response.status_code} | json={response.json()}") + if response.status_code == 404 and BaseResponse.model_validate(response.json()).result == "No active cards": + global NO_ACTIVE_CARDS_STATUS + NO_ACTIVE_CARDS_STATUS = True + pytest.skip(reason="No active cards in MongoDB") + else: + assert response.status_code == 200 + result = response.json().get("result") + assert isinstance(result, list) + if len(result) == 10: + for card in result: + assert "choice_A" in card + assert "choice_B" in card + assert "author_id" in card + assert "card_id" in card + else: + global ACTIVE_CARDS_LESS_THAN_TEN + ACTIVE_CARDS_LESS_THAN_TEN = True + pytest.skip(reason="The number of active cards is less than 10 in MongoDB") + +@pytest.mark.asyncio +async def test_get_random_cards_randomness(): + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + params = {"user_id": EXIST_USER} + response1 = await client.get("/get_random_cards", params=params) + response2 = await client.get("/get_random_cards", params=params) + result1 = response1.json().get("result") + result2 = response2.json().get("result") + print(f"\nINPUT: endpoint=/get_random_cards (двойной вызов)\nOUTPUT 1: {result1}\nOUTPUT 2: {result2}") + if len(result1) == 10 and len(result2) == 10: + assert result1 != result2 + +@pytest.mark.asyncio +async def test_get_random_cards_parallel_requests(): + if NO_ACTIVE_CARDS_STATUS: + pytest.skip(reason="No active cards in MongoDB") + elif ACTIVE_CARDS_LESS_THAN_TEN: + pytest.skip(reason="The number of active cards is less than 10 in MongoDB") + else: + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + params = {"user_id": EXIST_USER} + tasks = [client.get("/get_random_cards", params=params) for _ in range(5)] + responses = await asyncio.gather(*tasks) + for resp in responses: + print(f"\nParallel call: status={resp.status_code} | json={resp.json()}") + assert resp.status_code == 200 + result = resp.json().get("result") + assert isinstance(result, list) + assert len(result) == 10 \ No newline at end of file diff --git a/app/tools/__init__.py b/app/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tools/_add_base_cards.py b/app/tools/_add_base_cards.py new file mode 100644 index 0000000..0e8ce00 --- /dev/null +++ b/app/tools/_add_base_cards.py @@ -0,0 +1,72 @@ +import argparse +import sys +import json +from datetime import datetime + +sys.path.append('..') +from mongo_worker import MongoWorker +from schemas.base_schemas import Card + + +def create_cards(num_cards=10, author_id=1): + cards_list = [] + for card_id in range(1, num_cards + 1): + print(f"Creating Card {card_id}") + card = Card( + card_id=card_id, + choice_A=input("Choice A: "), + choice_B=input("Choice B: "), + author_id=author_id, + creation_date=datetime.now().isoformat(), + moderation_date=datetime.now().isoformat(), + active_status=True + ) + cards_list.append(card) + return cards_list + +def read_json(json_file): + try: + with open(json_file, 'r', encoding='utf-8') as f: + data = json.load(f) + return [Card(**card) for card in data] + except Exception as e: + print(f"Error reading JSON file: {e}") + return [] + +def write_json(cards_list, json_file): + try: + with open(json_file, 'w', encoding='utf-8') as f: + json.dump([card.model_dump() for card in cards_list], f, ensure_ascii=False, indent=4) + print(f"Successfully saved {len(cards_list)} cards to {json_file}") + except Exception as e: + print(f"Error writing to JSON file: {e}") + +def add_cards_to_mongodb(cards_list): + mongo = MongoWorker() + for card in cards_list: + mongo.add_card_by_base_model(card) + print(f"Successfully added {len(cards_list)} cards to MongoDB") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('-a', '--action', type=int, required=True, choices=[0, 1, 2], + help="0 - Manual input & save to MongoDB, 1 - Manual input & save to JSON, 2 - Read JSON & save to MongoDB") + parser.add_argument('-f', '--file', type=str, default="cards.json", help="JSON file name for reading/writing") + parser.add_argument('-n', '--num', type=int, default=10, help="Number of cards to create") + parser.add_argument('-u', '--user', type=int, default=1, help="Author ID") + + args = parser.parse_args() + + if args.action == 0: + cards = create_cards(args.num, args.user) + add_cards_to_mongodb(cards) + elif args.action == 1: + cards = create_cards(args.num, args.user) + write_json(cards, args.file) + elif args.action == 2: + cards = read_json(args.file) + if cards: + add_cards_to_mongodb(cards) + else: + print("No valid cards found in JSON file.") diff --git a/app/tools/base_moderation.py b/app/tools/base_moderation.py new file mode 100644 index 0000000..ad6772a --- /dev/null +++ b/app/tools/base_moderation.py @@ -0,0 +1,24 @@ +import re + +from tools.data.dirty_words import dirty_words_set + +def is_not_empty(text: str) -> bool: + """Checks that the text is not empty.""" + return bool(text.strip()) + +def has_no_links(text: str) -> bool: + """Checks that there are no links in the text.""" + url_pattern = re.compile(r'https?://\S+|www\.\S+') + return not bool(url_pattern.search(text)) + +def has_no_dirty_words(text: str) -> bool: + """Checks that there are no forbidden words in the text.""" + words = set(re.findall(r'\w+', text.lower())) + return not bool(words & dirty_words_set) + +def text_lenght(text: str) -> bool: + """Check max text lenght""" + return len(text) <= 150 # TODO: update in future + +def moderate_text(text: str) -> bool: + return is_not_empty(text) and has_no_links(text) and has_no_dirty_words(text) and text_lenght(text) diff --git a/app/tools/data/base_cards.json b/app/tools/data/base_cards.json new file mode 100644 index 0000000..8320fcc --- /dev/null +++ b/app/tools/data/base_cards.json @@ -0,0 +1,152 @@ +[ + { + "card_id": 1, + "choice_A": "Котики", + "choice_B": "Собачки", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:56:05.290110", + "moderation_date": "2025-03-04T16:56:05.290143", + "active_status": true + }, + { + "card_id": 2, + "choice_A": "Чай", + "choice_B": "Кофе", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:56:11.108762", + "moderation_date": "2025-03-04T16:56:11.108793", + "active_status": true + }, + { + "card_id": 3, + "choice_A": "Отпуск в горах", + "choice_B": "Отпуск на море", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:56:25.062542", + "moderation_date": "2025-03-04T16:56:25.062586", + "active_status": true + }, + { + "card_id": 4, + "choice_A": "День", + "choice_B": "Ночь", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:56:34.148692", + "moderation_date": "2025-03-04T16:56:34.148725", + "active_status": true + }, + { + "card_id": 5, + "choice_A": "Работать в офисе", + "choice_B": "Работать удалённо", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:57:00.723472", + "moderation_date": "2025-03-04T16:57:00.723518", + "active_status": true + }, + { + "card_id": 6, + "choice_A": "Заниматься спортом", + "choice_B": "Играть в видеоигры", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:57:31.524721", + "moderation_date": "2025-03-04T16:57:31.524754", + "active_status": true + }, + { + "card_id": 7, + "choice_A": "Гулять", + "choice_B": "Сидеть дома", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:57:52.022380", + "moderation_date": "2025-03-04T16:57:52.022413", + "active_status": true + }, + { + "card_id": 8, + "choice_A": "Посмотреть фильм", + "choice_B": "Почитать книжку", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:58:15.032749", + "moderation_date": "2025-03-04T16:58:15.032762", + "active_status": true + }, + { + "card_id": 9, + "choice_A": "Зима", + "choice_B": "Лето", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:58:24.800416", + "moderation_date": "2025-03-04T16:58:24.800450", + "active_status": true + }, + { + "card_id": 10, + "choice_A": "Лучший друг/подруга", + "choice_B": "Любимый человек", + "count_choice_A": 0, + "count_choice_B": 0, + "count_total": 0, + "count_likes": 0, + "count_dislikes": 0, + "comments": [], + "author_id": 1, + "creation_date": "2025-03-04T16:59:05.265795", + "moderation_date": "2025-03-04T16:59:05.265829", + "active_status": true + } +] \ No newline at end of file diff --git a/app/tools/data/dirty_words.py b/app/tools/data/dirty_words.py new file mode 100644 index 0000000..8064150 --- /dev/null +++ b/app/tools/data/dirty_words.py @@ -0,0 +1 @@ +dirty_words_set = set(["6ля","6лядь","6лять","b3ъeб","cock","cunt","e6aль","ebal","eblan","eбaл","eбaть","eбyч","eбать","eбёт","eблантий","fuck","fucker","fucking","xyёв","xyй","xyя","xуе,xуй","xую","zaeb","zaebal","zaebali","zaebat","архипиздрит","ахуел","ахуеть","бздение","бздеть","бздех","бздецы","бздит","бздицы","бздло","бзднуть","бздун","бздунья","бздюха","бздюшка","бздюшко","бля","блябу","блябуду","бляд","бляди","блядина","блядище","блядки","блядовать","блядство","блядун","блядуны","блядунья","блядь","блядюга","блять","вафел","вафлёр","взъебка","взьебка","взьебывать","въеб","въебался","въебенн","въебусь","въебывать","выблядок","выблядыш","выеб","выебать","выебен","выебнулся","выебон","выебываться","выпердеть","высраться","выссаться","вьебен","гавно","гавнюк","гавнючка","гамно","гандон","гнид","гнида","гниды","говенка","говенный","говешка","говназия","говнецо","говнище","говно","говноед","говнолинк","говночист","говнюк","говнюха","говнядина","говняк","говняный","говнять","гондон","доебываться","долбоеб","долбоёб","долбоящер","дрисня","дрист","дристануть","дристать","дристун","дристуха","дрочелло","дрочена","дрочила","дрочилка","дрочистый","дрочить","дрочка","дрочун","е6ал","е6ут","еб твою мать","ёб твою мать","ёбaн","ебaть","ебyч","ебал","ебало","ебальник","ебан","ебанамать","ебанат","ебаная","ёбаная","ебанический","ебанный","ебанныйврот","ебаное","ебануть","ебануться","ёбаную","ебаный","ебанько","ебарь","ебат","ёбат","ебатория","ебать","ебать-копать","ебаться","ебашить","ебёна","ебет","ебёт","ебец","ебик","ебин","ебись","ебическая","ебки","ебла","еблан","ебливый","еблище","ебло","еблыст","ебля","ёбн","ебнуть","ебнуться","ебня","ебошить","ебская","ебский","ебтвоюмать","ебун","ебут","ебуч","ебуче","ебучее","ебучий","ебучим","ебущ","ебырь","елда","елдак","елдачить","жопа","жопу","заговнять","задрачивать","задристать","задрота","зае6","заё6","заеб","заёб","заеба","заебал","заебанец","заебастая","заебастый","заебать","заебаться","заебашить","заебистое","заёбистое","заебистые","заёбистые","заебистый","заёбистый","заебись","заебошить","заебываться","залуп","залупа","залупаться","залупить","залупиться","замудохаться","запиздячить","засерать","засерун","засеря","засирать","засрун","захуячить","заябестая","злоеб","злоебучая","злоебучее","злоебучий","ибанамат","ибонех","изговнять","изговняться","изъебнуться","ипать","ипаться","ипаццо","Какдвапальцаобоссать","конча","курва","курвятник","лох","лошарa","лошара","лошары","лошок","лярва","малафья","манда","мандавошек","мандавошка","мандавошки","мандей","мандень","мандеть","мандища","мандой","манду","мандюк","минет","минетчик","минетчица","млять","мокрощелка","мокрощёлка","мразь","мудak","мудaк","мудаг","мудак","муде","мудель","мудеть","муди","мудил","мудила","мудистый","мудня","мудоеб","мудозвон","мудоклюй","на хер","на хуй","набздел","набздеть","наговнять","надристать","надрочить","наебать","наебет","наебнуть","наебнуться","наебывать","напиздел","напиздели","напиздело","напиздили","насрать","настопиздить","нахер","нахрен","нахуй","нахуйник","не ебет","не ебёт","невротебучий","невъебенно","нехира","нехрен","Нехуй","нехуйственно","ниибацо","ниипацца","ниипаццо","ниипет","никуя","нихера","нихуя","обдристаться","обосранец","обосрать","обосцать","обосцаться","обсирать","объебос","обьебать обьебос","однохуйственно","опездал","опизде","опизденивающе","остоебенить","остопиздеть","отмудохать","отпиздить","отпиздячить","отпороть","отъебись","охуевательский","охуевать","охуевающий","охуел","охуенно","охуеньчик","охуеть","охуительно","охуительный","охуяньчик","охуячивать","охуячить","очкун","падла","падонки","падонок","паскуда","педерас","педик","педрик","педрила","педрилло","педрило","педрилы","пездень","пездит","пездишь","пездо","пездят","пердануть","пердеж","пердение","пердеть","пердильник","перднуть","пёрднуть","пердун","пердунец","пердунина","пердунья","пердуха","пердь","переёбок","пернуть","пёрнуть","пи3д","пи3де","пи3ду","пиzдец","пидар","пидарaс","пидарас","пидарасы","пидары","пидор","пидорасы","пидорка","пидорок","пидоры","пидрас","пизда","пиздануть","пиздануться","пиздарваньчик","пиздато","пиздатое","пиздатый","пизденка","пизденыш","пиздёныш","пиздеть","пиздец","пиздит","пиздить","пиздиться","пиздишь","пиздища","пиздище","пиздобол","пиздоболы","пиздобратия","пиздоватая","пиздоватый","пиздолиз","пиздонутые","пиздорванец","пиздорванка","пиздострадатель","пизду","пиздуй","пиздун","пиздунья","пизды","пиздюга","пиздюк","пиздюлина","пиздюля","пиздят","пиздячить","писбшки","писька","писькострадатель","писюн","писюшка","по хуй","по хую","подговнять","подонки","подонок","подъебнуть","подъебнуться","поебать","поебень","поёбываает","поскуда","посрать","потаскуха","потаскушка","похер","похерил","похерила","похерили","похеру","похрен","похрену","похуй","похуист","похуистка","похую","придурок","приебаться","припиздень","припизднутый","припиздюлина","пробзделся","проблядь","проеб","проебанка","проебать","промандеть","промудеть","пропизделся","пропиздеть","пропиздячить","раздолбай","разхуячить","разъеб","разъеба","разъебай","разъебать","распиздай","распиздеться","распиздяй","распиздяйство","распроеть","сволота","сволочь","сговнять","секель","серун","серька","сестроеб","сикель","сила","сирать","сирывать","соси","спиздел","спиздеть","спиздил","спиздила","спиздили","спиздит","спиздить","срака","сраку","сраный","сранье","срать","срун","ссака","ссышь","стерва","страхопиздище","сука","суки","суходрочка","сучара","сучий","сучка","сучко","сучонок","сучье","сцание","сцать","сцука","сцуки","сцуконах","сцуль","сцыха","сцышь","съебаться","сыкун","трахае6","трахаеб","трахаёб","трахатель","ублюдок","уебать","уёбища","уебище","уёбище","уебищное","уёбищное","уебк","уебки","уёбки","уебок","уёбок","урюк","усраться","ушлепок","х_у_я_р_а","хyё","хyй","хyйня","хамло","хер","херня","херовато","херовина","херовый","хитровыебанный","хитрожопый","хуeм","хуе","хуё","хуевато","хуёвенький","хуевина","хуево","хуевый","хуёвый","хуек","хуёк","хуел","хуем","хуенч","хуеныш","хуенький","хуеплет","хуеплёт","хуепромышленник","хуерик","хуерыло","хуесос","хуесоска","хуета","хуетень","хуею","хуи","хуй","хуйком","хуйло","хуйня","хуйрик","хуище","хуля","хую","хуюл","хуя","хуяк","хуякать","хуякнуть","хуяра","хуясе","хуячить","целка","чмо","чмошник","чмырь","шалава","шалавой","шараёбиться","шлюха","шлюхой","шлюшка","ябывает"]) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c21f383 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + mongodb: + image: mongodb/mongodb-community-server + container_name: tort-mongodb + restart: always + network_mode: bridge + environment: + MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASS} + ports: + - "127.0.0.1:${MONGO_PORT}:27017" + healthcheck: + test: ["CMD", "mongosh", "--username", "${MONGO_USER}", "--password", "${MONGO_PASS}", "--eval", "db.runCommand({ ping: 1 })"] + interval: 10s + timeout: 5s + retries: 2 + + backend: + build: + context: ./app + image: tort-backend:latest + depends_on: + mongodb: + condition: service_healthy + container_name: tort-backend + network_mode: "host" + environment: + MONGO_HOST: ${MONGO_HOST} + MONGO_PORT: ${MONGO_PORT} + MONGO_USER: ${MONGO_USER} + MONGO_PASS: ${MONGO_PASS} + diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8f61843..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -fastapi==0.115.7 -pymongo==4.10.1