RELEASE 1.0! #5
@@ -0,0 +1,6 @@
|
||||
DISABLE_DOCS=true
|
||||
|
||||
MONGO_HOST=127.0.0.1
|
||||
MONGO_PORT=27017
|
||||
MONGO_USER=user
|
||||
MONGO_PASS=pass
|
||||
@@ -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
|
||||
@@ -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
|
||||
+2
-25
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"]
|
||||
+188
-1
@@ -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!")
|
||||
asyncio.run(main())
|
||||
+197
-28
@@ -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)
|
||||
@@ -0,0 +1,4 @@
|
||||
fastapi==0.115.7
|
||||
pymongo==4.10.1
|
||||
python-dotenv==1.0.1
|
||||
uvicorn==0.34.0
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -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}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
fastapi==0.115.7
|
||||
pymongo==4.10.1
|
||||
Reference in New Issue
Block a user