From e5b336e8e47d8a310044a3a86a3178046a6c8dbc Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 15 Feb 2025 16:24:00 +0300 Subject: [PATCH 01/41] Adding data validation --- .idea/.gitignore | 3 +++ README.md | 2 +- app/.env | 5 +++++ app/mongo_worker.py | 52 +++++++++++++++++++++++++++------------------ app/schemas.py | 17 +++++++++++++++ requirements.txt | 1 + 6 files changed, 58 insertions(+), 22 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 app/.env create mode 100644 app/schemas.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/README.md b/README.md index 1b148f7..67a106c 100644 --- a/README.md +++ b/README.md @@ -19,5 +19,5 @@ pip3 install -r 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 ``` diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..3944b68 --- /dev/null +++ b/app/.env @@ -0,0 +1,5 @@ +MONGO_HOST = 127.0.0.1 +MONGO_PORT = 27017 + +MONGO_USER = user +MONGO_PASS = pass \ No newline at end of file diff --git a/app/mongo_worker.py b/app/mongo_worker.py index e165b4b..e0f0fe6 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -1,13 +1,20 @@ +import os + import pymongo -from datetime import datetime +from schemas import * -from bson import ObjectId +from datetime import datetime +from dotenv import load_dotenv 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"] @@ -16,30 +23,33 @@ class MongoWorker: 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) -> True | False: + 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_path: str) -> User: + new_user = User(user_id=user_id, + username=username, + first_name=first_name, + last_name=last_name, + photo_path=photo_path, + 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 + res = self.users_data.insert_one(new_user.model_dump()) + return new_user except Exception as exception: print(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})) 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)) + print(mongo.check_user(123)) + # print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", os.path.join("/path/to", "photo.jpg"))) + print(mongo.get_user(123)) diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..91cec5f --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel + + +class User(BaseModel): + user_id: int + username: str + + first_name: str + last_name: str + photo_path: str + + activity: int = 0 + liked_post_ids: list[int] = None + disliked_post_ids: list[int] = None + comments_ids: list[int] = None + + registration_date: str diff --git a/requirements.txt b/requirements.txt index 8f61843..73be66f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ fastapi==0.115.7 pymongo==4.10.1 +python-dotenv==1.0.1 \ No newline at end of file -- 2.54.0 From 444d010bbfe9b01a02c9eeea5970b29934109c00 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 15 Feb 2025 16:43:46 +0300 Subject: [PATCH 02/41] Refactor workflows --- .../{mypy-test.yml => app-actions.yml} | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) rename .github/workflows/{mypy-test.yml => app-actions.yml} (51%) diff --git a/.github/workflows/mypy-test.yml b/.github/workflows/app-actions.yml similarity index 51% rename from .github/workflows/mypy-test.yml rename to .github/workflows/app-actions.yml index 0ac5f6c..81665fe 100644 --- a/.github/workflows/mypy-test.yml +++ b/.github/workflows/app-actions.yml @@ -1,22 +1,31 @@ -name: mypy-test +name: app-actions on: push: - paths: - - '**.py' + branches: + - main + - app + pull_request: + branches: + - main jobs: mypy: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v3 + - 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: Install dependencies + run: | + pip install mypy + pip install -r requirements.txt + - name: Run mypy - run: mypy --ignore-missing-imports ./app + run: mypy --ignore-missing-imports ./app \ No newline at end of file -- 2.54.0 From 1248a461e7e2b264ca1939370c7b9908f6036a1d Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 15 Feb 2025 16:47:20 +0300 Subject: [PATCH 03/41] Changes to meet mypy requirements --- app/mongo_worker.py | 2 +- app/schemas.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index e0f0fe6..69ac46f 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -23,7 +23,7 @@ class MongoWorker: print(self.client.list_database_names()) print(self.db.list_collection_names()) - def check_user(self, user_id: int) -> True | False: + def check_user(self, user_id: int) -> bool: if self.users_data.find_one({"user_id": user_id}): return True else: diff --git a/app/schemas.py b/app/schemas.py index 91cec5f..aa68d8b 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -10,8 +10,8 @@ class User(BaseModel): photo_path: str activity: int = 0 - liked_post_ids: list[int] = None - disliked_post_ids: list[int] = None - comments_ids: list[int] = None + liked_post_ids: list[int] = list() + disliked_post_ids: list[int] = list() + comments_ids: list[int] = list() registration_date: str -- 2.54.0 From eacc8c29df3cc638d84c8bd74ff993329e5be6fa Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 15 Feb 2025 16:48:46 +0300 Subject: [PATCH 04/41] Delete .idea --- .idea/.gitignore | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -- 2.54.0 From 90c99c3f853baa32e84679e32ff5fac69bc051fd Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 15 Feb 2025 16:55:58 +0300 Subject: [PATCH 05/41] Update workflows Change that CI will only run if .py files are changed or added --- .github/workflows/app-actions.yml | 2 ++ .gitignore | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/app-actions.yml b/.github/workflows/app-actions.yml index 81665fe..ee9033b 100644 --- a/.github/workflows/app-actions.yml +++ b/.github/workflows/app-actions.yml @@ -2,6 +2,8 @@ name: app-actions on: push: + paths: + - '**.py' branches: - main - app diff --git a/.gitignore b/.gitignore index 035a5af..e07ccd3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ *.py[cod] *$py.class +.idea/ # C extensions *.so @@ -170,3 +171,4 @@ cython_debug/ # PyPI configuration file .pypirc +app/.env -- 2.54.0 From fc0074d9622dc3e5b793af6fabcbd78434cb25e6 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Mon, 17 Feb 2025 13:23:16 +0300 Subject: [PATCH 06/41] Update user photo logic --- app/mongo_worker.py | 6 +++--- app/schemas.py | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 69ac46f..7f6c80d 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -29,12 +29,12 @@ class MongoWorker: else: return False - def add_user(self, user_id: int, username: str, first_name: str, last_name: str, photo_path: str) -> User: + def add_user(self, user_id: int, username: str, first_name: str, last_name: str, photo_id: str) -> User: new_user = User(user_id=user_id, username=username, first_name=first_name, last_name=last_name, - photo_path=photo_path, + photo_id=photo_id, registration_date=datetime.now().isoformat()) try: res = self.users_data.insert_one(new_user.model_dump()) @@ -51,5 +51,5 @@ if __name__ == "__main__": mongo = MongoWorker() mongo.get_mongodb_info() print(mongo.check_user(123)) - # print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", os.path.join("/path/to", "photo.jpg"))) + # print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) print(mongo.get_user(123)) diff --git a/app/schemas.py b/app/schemas.py index aa68d8b..9e2b63a 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,3 +1,5 @@ +import typing + from pydantic import BaseModel @@ -7,7 +9,7 @@ class User(BaseModel): first_name: str last_name: str - photo_path: str + photo_id: str activity: int = 0 liked_post_ids: list[int] = list() @@ -15,3 +17,7 @@ class User(BaseModel): comments_ids: list[int] = list() registration_date: str + +class BaseResponse(BaseModel): + result: typing.Any + error: bool = False -- 2.54.0 From 388c89cc40575a752ee34fa71575761f1e5934f9 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Mon, 17 Feb 2025 13:24:19 +0300 Subject: [PATCH 07/41] Create base FastAPI routs --- app/main.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 5a11942..2fd3573 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,29 @@ +import uvicorn +import asyncio + +from schemas import * + +from fastapi import FastAPI, Depends from mongo_worker import MongoWorker + +app = FastAPI() + +@app.get("/check_user") +async def check_user(user_id: int, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + result = mongo.check_user(user_id) + return BaseResponse(result=result) + +@app.get("/get_user") +async def get_user(user_id: int, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + result = mongo.get_user(user_id) + return BaseResponse(result=result) + + +async def main(): + config = uvicorn.Config("main:app", port=5000, log_level="info") + 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 -- 2.54.0 From 51cc2eb2b2e2b429cd12cdffe14ee10e7b083835 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Mon, 17 Feb 2025 14:22:49 +0300 Subject: [PATCH 08/41] Create add_user rout And fix some bullshit --- app/main.py | 24 ++++++++++++++++----- app/mongo_worker.py | 13 ++++------- app/schemas/api_schemas.py | 8 +++++++ app/{schemas.py => schemas/base_schemas.py} | 10 ++------- 4 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 app/schemas/api_schemas.py rename app/{schemas.py => schemas/base_schemas.py} (66%) diff --git a/app/main.py b/app/main.py index 2fd3573..4687ec5 100644 --- a/app/main.py +++ b/app/main.py @@ -1,24 +1,38 @@ import uvicorn import asyncio -from schemas import * - -from fastapi import FastAPI, Depends +from schemas.api_schemas import * from mongo_worker import MongoWorker +from typing import Annotated + +from fastapi import FastAPI, Depends, UploadFile, File + app = FastAPI() @app.get("/check_user") -async def check_user(user_id: int, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: +async def check_user(user_id: int, + mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: result = mongo.check_user(user_id) return BaseResponse(result=result) @app.get("/get_user") -async def get_user(user_id: int, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: +async def get_user(user_id: int, + mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: result = mongo.get_user(user_id) return BaseResponse(result=result) +@app.post("/add_user") +async def add_user(user_id: int, + username: str, + first_name: str, + last_name: str, + photo_url: str, + mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + result = mongo.add_user(user_id, username, first_name, last_name, photo_url) + return BaseResponse(result=result) + async def main(): config = uvicorn.Config("main:app", port=5000, log_level="info") diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 7f6c80d..3b10c56 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -2,7 +2,7 @@ import os import pymongo -from schemas import * +from schemas.base_schemas import * from datetime import datetime from dotenv import load_dotenv @@ -19,28 +19,23 @@ class MongoWorker: self.users_data = self.db["users_data"] self.game_data = self.db["game_data"] - def get_mongodb_info(self) -> None: - print(self.client.list_database_names()) - print(self.db.list_collection_names()) - 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_id: str) -> User: + 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_id=photo_id, + photo_url=photo_url, registration_date=datetime.now().isoformat()) try: - res = self.users_data.insert_one(new_user.model_dump()) + self.users_data.insert_one(new_user.model_dump()) return new_user except Exception as exception: - print(exception) return new_user def get_user(self, user_id: int) -> User: diff --git a/app/schemas/api_schemas.py b/app/schemas/api_schemas.py new file mode 100644 index 0000000..562312c --- /dev/null +++ b/app/schemas/api_schemas.py @@ -0,0 +1,8 @@ +import typing + +from pydantic import BaseModel + + +class BaseResponse(BaseModel): + result: typing.Any + error: bool = False diff --git a/app/schemas.py b/app/schemas/base_schemas.py similarity index 66% rename from app/schemas.py rename to app/schemas/base_schemas.py index 9e2b63a..1940af1 100644 --- a/app/schemas.py +++ b/app/schemas/base_schemas.py @@ -1,5 +1,3 @@ -import typing - from pydantic import BaseModel @@ -9,15 +7,11 @@ class User(BaseModel): first_name: str last_name: str - photo_id: str + photo_url: str activity: int = 0 liked_post_ids: list[int] = list() disliked_post_ids: list[int] = list() comments_ids: list[int] = list() - registration_date: str - -class BaseResponse(BaseModel): - result: typing.Any - error: bool = False + registration_date: str \ No newline at end of file -- 2.54.0 From 8c4b061e6a2289cc4d4ba8727979bb8f0a9e8dba Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Mon, 17 Feb 2025 14:26:47 +0300 Subject: [PATCH 09/41] Create add_user rout And fix some bullshit x2 --- app/mongo_worker.py | 1 - app/schemas/__init__.py | 0 2 files changed, 1 deletion(-) create mode 100644 app/schemas/__init__.py diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 3b10c56..5ad56db 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -44,7 +44,6 @@ class MongoWorker: if __name__ == "__main__": mongo = MongoWorker() - mongo.get_mongodb_info() print(mongo.check_user(123)) # print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) print(mongo.get_user(123)) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 -- 2.54.0 From a1585bd5a0bd769d37705a400183a9c2c6a2cbb1 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Mon, 17 Feb 2025 14:54:10 +0300 Subject: [PATCH 10/41] Rework add_user rout --- app/main.py | 12 ++++++------ app/schemas/api_schemas.py | 8 ++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/main.py b/app/main.py index 4687ec5..49d45fd 100644 --- a/app/main.py +++ b/app/main.py @@ -24,13 +24,13 @@ async def get_user(user_id: int, return BaseResponse(result=result) @app.post("/add_user") -async def add_user(user_id: int, - username: str, - first_name: str, - last_name: str, - photo_url: str, +async def add_user(new_user: AddUserBody, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: - result = mongo.add_user(user_id, username, first_name, last_name, photo_url) + 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) diff --git a/app/schemas/api_schemas.py b/app/schemas/api_schemas.py index 562312c..386a51a 100644 --- a/app/schemas/api_schemas.py +++ b/app/schemas/api_schemas.py @@ -6,3 +6,11 @@ from pydantic import BaseModel class BaseResponse(BaseModel): result: typing.Any error: bool = False + +class AddUserBody(BaseModel): + user_id: int + username: str + + first_name: str + last_name: str + photo_url: str \ No newline at end of file -- 2.54.0 From edf53514c093dbbaa9f791beec29d22c5c3236ba Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Wed, 19 Feb 2025 16:15:10 +0300 Subject: [PATCH 11/41] Add pytest action in workflow --- .github/workflows/app-actions.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/app-actions.yml b/.github/workflows/app-actions.yml index ee9033b..4bac828 100644 --- a/.github/workflows/app-actions.yml +++ b/.github/workflows/app-actions.yml @@ -14,6 +14,7 @@ on: jobs: mypy: runs-on: ubuntu-latest + continue-on-error: true steps: - name: Checkout uses: actions/checkout@v3 @@ -30,4 +31,27 @@ jobs: pip install -r requirements.txt - name: Run mypy - run: mypy --ignore-missing-imports ./app \ No newline at end of file + run: mypy --ignore-missing-imports ./app + + pytest: + runs-on: ubuntu-latest + 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 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=user -e MONGO_INITDB_ROOT_PASSWORD=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 requirements.txt + + - name: Run pytest + run: pytest -vs \ No newline at end of file -- 2.54.0 From 3dea898c32e0b4917fa72dac5407fa526b5fff68 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Wed, 19 Feb 2025 16:17:35 +0300 Subject: [PATCH 12/41] Add test --- app/tests/__init__.py | 0 app/tests/test_user_info.py | 154 ++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 app/tests/__init__.py create mode 100644 app/tests/test_user_info.py 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_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 -- 2.54.0 From 014a52396792e0801a832e6acc566eafacd8ba56 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Wed, 19 Feb 2025 16:19:47 +0300 Subject: [PATCH 13/41] Bugfix --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 73be66f..f0ee27b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ fastapi==0.115.7 pymongo==4.10.1 -python-dotenv==1.0.1 \ No newline at end of file +python-dotenv==1.0.1 +uvicorn==0.34.0 \ No newline at end of file -- 2.54.0 From 834df9a28320322f7c4120f56a1a03f590890fcf Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Wed, 19 Feb 2025 16:21:06 +0300 Subject: [PATCH 14/41] Update main users routs --- app/main.py | 40 +++++++++++++++++++++++--------------- app/schemas/api_schemas.py | 4 ++-- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/app/main.py b/app/main.py index 49d45fd..6c8ef7c 100644 --- a/app/main.py +++ b/app/main.py @@ -4,34 +4,42 @@ import asyncio from schemas.api_schemas import * from mongo_worker import MongoWorker -from typing import Annotated - -from fastapi import FastAPI, Depends, UploadFile, File +from fastapi import FastAPI, Depends, Response, status app = FastAPI() -@app.get("/check_user") -async def check_user(user_id: int, +@app.get("/check_user", status_code=200) +async def check_user(user_id: NonNegativeInt, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: result = mongo.check_user(user_id) return BaseResponse(result=result) -@app.get("/get_user") -async def get_user(user_id: int, +@app.get("/get_user", status_code=200) +async def get_user(user_id: NonNegativeInt, + response: Response, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: - result = mongo.get_user(user_id) - return BaseResponse(result=result) + 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") +@app.post("/add_user", status_code=201) async def add_user(new_user: AddUserBody, + response: Response, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: - 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) + 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) async def main(): diff --git a/app/schemas/api_schemas.py b/app/schemas/api_schemas.py index 386a51a..5b117e1 100644 --- a/app/schemas/api_schemas.py +++ b/app/schemas/api_schemas.py @@ -1,6 +1,6 @@ import typing -from pydantic import BaseModel +from pydantic import BaseModel, NonNegativeInt class BaseResponse(BaseModel): @@ -8,7 +8,7 @@ class BaseResponse(BaseModel): error: bool = False class AddUserBody(BaseModel): - user_id: int + user_id: NonNegativeInt username: str first_name: str -- 2.54.0 From 20c81dbd58ad1c8b280522c7ffe4d08bfa8540b9 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Mon, 24 Feb 2025 17:50:28 +0300 Subject: [PATCH 15/41] feat: create card schema --- app/schemas/base_schemas.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/app/schemas/base_schemas.py b/app/schemas/base_schemas.py index 1940af1..2ade802 100644 --- a/app/schemas/base_schemas.py +++ b/app/schemas/base_schemas.py @@ -1,5 +1,5 @@ -from pydantic import BaseModel - +from pydantic import BaseModel, Field +from bson.objectid import ObjectId class User(BaseModel): user_id: int @@ -14,4 +14,26 @@ class User(BaseModel): disliked_post_ids: list[int] = list() comments_ids: list[int] = list() - registration_date: str \ No newline at end of file + registration_date: str + +class Card(BaseModel): + card_id: ObjectId = Field(alias="_id") + + 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 Config: + arbitrary_types_allowed = True \ No newline at end of file -- 2.54.0 From c20f423057dd653bc1152b3caecc7227af90887b Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Tue, 25 Feb 2025 14:42:59 +0300 Subject: [PATCH 16/41] fix: default ObjectId generator --- app/schemas/base_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/schemas/base_schemas.py b/app/schemas/base_schemas.py index 2ade802..03af8da 100644 --- a/app/schemas/base_schemas.py +++ b/app/schemas/base_schemas.py @@ -17,7 +17,7 @@ class User(BaseModel): registration_date: str class Card(BaseModel): - card_id: ObjectId = Field(alias="_id") + card_id: ObjectId = Field(alias="_id", default=ObjectId()) choice_A: str choice_B: str -- 2.54.0 From d26136b1526a5548d8e5a4b815b5c50b9aa25b68 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Tue, 25 Feb 2025 14:47:20 +0300 Subject: [PATCH 17/41] feat: add cards functionals in MongoWorker --- app/mongo_worker.py | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 5ad56db..09a7ef4 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -3,6 +3,7 @@ import os import pymongo from schemas.base_schemas import * +from typing import Optional from datetime import datetime from dotenv import load_dotenv @@ -19,6 +20,7 @@ class MongoWorker: self.users_data = self.db["users_data"] self.game_data = self.db["game_data"] + def check_user(self, user_id: int) -> bool: if self.users_data.find_one({"user_id": user_id}): return True @@ -40,10 +42,41 @@ class MongoWorker: def get_user(self, user_id: int) -> User: return User.model_validate(self.users_data.find_one({"user_id": user_id})) + + def add_card(self, choice_A: str, choice_B: str, author_id: int) -> Card: + new_card = 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(by_alias=True)) + return new_card + except Exception as exception: + return new_card + + def get_card(self, card_id: ObjectId) -> Optional[Card]: + document = self.game_data.find_one({"_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 raw_items: + validated_items = [Card.model_validate(item) for item in raw_items] + return validated_items + else: + return None if __name__ == "__main__": mongo = MongoWorker() - print(mongo.check_user(123)) - # print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) - print(mongo.get_user(123)) + #print(mongo.check_user(123)) + #print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) + #print(mongo.get_user(123)) + print(mongo.add_card("A", "B", 123)) + print(mongo.get_random_cards(10, active_status=False)) -- 2.54.0 From 02c098a6ee92a7ce9243b359488ba0d93322dc35 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Tue, 25 Feb 2025 16:53:59 +0300 Subject: [PATCH 18/41] fix: fix check if a list is empty --- app/mongo_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 09a7ef4..41936ed 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -67,7 +67,7 @@ class MongoWorker: {"$sample": {"size": amount}}] raw_items = list(self.game_data.aggregate(pipeline)) - if raw_items: + if not raw_items: validated_items = [Card.model_validate(item) for item in raw_items] return validated_items else: -- 2.54.0 From 839476b9dc6a7b831226a8210888f8ed43e6a798 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Tue, 25 Feb 2025 17:59:44 +0300 Subject: [PATCH 19/41] fix: reworked cards indexing system --- app/mongo_worker.py | 28 +++++++++++++++++++++------- app/schemas/base_schemas.py | 12 +++++------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 41936ed..0de81a8 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -15,9 +15,11 @@ class MongoWorker: 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')) + password = os.getenv('MONGO_PASS'), + uuidRepresentation="standard") self.db = self.client["data"] self.users_data = self.db["users_data"] + self.counters = self.db["counters"] self.game_data = self.db["game_data"] @@ -44,19 +46,30 @@ class MongoWorker: 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 add_card(self, choice_A: str, choice_B: str, author_id: int) -> Card: - new_card = Card(choice_A=choice_A, + 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(by_alias=True)) + 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: ObjectId) -> Optional[Card]: - document = self.game_data.find_one({"_id": card_id}) + 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: @@ -67,7 +80,7 @@ class MongoWorker: {"$sample": {"size": amount}}] raw_items = list(self.game_data.aggregate(pipeline)) - if not raw_items: + if raw_items: validated_items = [Card.model_validate(item) for item in raw_items] return validated_items else: @@ -79,4 +92,5 @@ if __name__ == "__main__": #print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) #print(mongo.get_user(123)) print(mongo.add_card("A", "B", 123)) - print(mongo.get_random_cards(10, active_status=False)) + #print(mongo.get_random_cards(10, active_status=False)) + #print(mongo.update_counter("cards_counter")) diff --git a/app/schemas/base_schemas.py b/app/schemas/base_schemas.py index 03af8da..d2fe829 100644 --- a/app/schemas/base_schemas.py +++ b/app/schemas/base_schemas.py @@ -1,5 +1,6 @@ -from pydantic import BaseModel, Field -from bson.objectid import ObjectId +import uuid + +from pydantic import BaseModel class User(BaseModel): user_id: int @@ -17,7 +18,7 @@ class User(BaseModel): registration_date: str class Card(BaseModel): - card_id: ObjectId = Field(alias="_id", default=ObjectId()) + card_id: int choice_A: str choice_B: str @@ -33,7 +34,4 @@ class Card(BaseModel): author_id: int creation_date: str moderation_date: str = "Not moderated" - active_status: bool = False - - class Config: - arbitrary_types_allowed = True \ No newline at end of file + active_status: bool = False \ No newline at end of file -- 2.54.0 From 99867cef808e3a73c814ce0822d70dd1b5037513 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Tue, 25 Feb 2025 19:11:35 +0300 Subject: [PATCH 20/41] feat: creating FastAPI routes for working with cards Added 3 routes: - add_card (add a card via POST request with AddCardBody scheme) - get_card (get a specific card by card_id) - get_random_cards (get 10 random cards) --- app/base_moderation.py | 20 ++++++++++++++++++++ app/dirty_words.py | 1 + app/main.py | 36 ++++++++++++++++++++++++++++++++++++ app/schemas/api_schemas.py | 8 +++++++- 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 app/base_moderation.py create mode 100644 app/dirty_words.py diff --git a/app/base_moderation.py b/app/base_moderation.py new file mode 100644 index 0000000..4a7e096 --- /dev/null +++ b/app/base_moderation.py @@ -0,0 +1,20 @@ +import re + +from 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 moderate_text(text: str) -> bool: + return is_not_empty(text) and has_no_links(text) and has_no_dirty_words(text) diff --git a/app/dirty_words.py b/app/dirty_words.py new file mode 100644 index 0000000..8064150 --- /dev/null +++ b/app/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/app/main.py b/app/main.py index 6c8ef7c..c14b8f6 100644 --- a/app/main.py +++ b/app/main.py @@ -3,6 +3,7 @@ import asyncio from schemas.api_schemas import * from mongo_worker import MongoWorker +from base_moderation import moderate_text from fastapi import FastAPI, Depends, Response, status @@ -40,6 +41,41 @@ async def add_user(new_user: AddUserBody, 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(MongoWorker)) -> 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(response: Response, + mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + result = mongo.get_random_cards(10, True) + if result: + return BaseResponse(result=result) + else: + response.status_code = status.HTTP_404_NOT_FOUND + return BaseResponse(result="No active cards", error=True) + +@app.post("/add_card", status_code=201) +async def add_card(new_card: AddCardBody, + response: Response, + mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + if moderate_text(new_card.choice_A) and moderate_text(new_card.choice_B): + card = mongo.add_card(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) async def main(): diff --git a/app/schemas/api_schemas.py b/app/schemas/api_schemas.py index 5b117e1..e71c99e 100644 --- a/app/schemas/api_schemas.py +++ b/app/schemas/api_schemas.py @@ -13,4 +13,10 @@ class AddUserBody(BaseModel): first_name: str last_name: str - photo_url: str \ No newline at end of file + photo_url: str + +class AddCardBody(BaseModel): + choice_A: str + choice_B: str + + author_id: NonNegativeInt \ No newline at end of file -- 2.54.0 From cc5ee65a53ede93ba5b5ed9f3a164529ec565144 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Wed, 26 Feb 2025 17:37:19 +0300 Subject: [PATCH 21/41] fix: add check text lenght in base moderation --- app/base_moderation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/base_moderation.py b/app/base_moderation.py index 4a7e096..5623d2f 100644 --- a/app/base_moderation.py +++ b/app/base_moderation.py @@ -16,5 +16,9 @@ def has_no_dirty_words(text: str) -> bool: 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) + return is_not_empty(text) and has_no_links(text) and has_no_dirty_words(text) and text_lenght(text) -- 2.54.0 From 2556194d4c878a95da8b21ddea8ab8b7599cfc60 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Wed, 26 Feb 2025 19:28:12 +0300 Subject: [PATCH 22/41] feat: add tests for cards routs --- app/tests/test_cards.py | 243 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 app/tests/test_cards.py diff --git a/app/tests/test_cards.py b/app/tests/test_cards.py new file mode 100644 index 0000000..875bd13 --- /dev/null +++ b/app/tests/test_cards.py @@ -0,0 +1,243 @@ +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 +NO_ACTIVE_CARDS_STATUS = False +ACTIVE_CARDS_LESS_THAN_TEN = False + + +# ---------- /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 + + +# ---------- /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: + response = await client.get("/get_random_cards") + 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: + response1 = await client.get("/get_random_cards") + response2 = await client.get("/get_random_cards") + 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: + tasks = [client.get("/get_random_cards") 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 -- 2.54.0 From 5c69a5c2e8f1581d766e12007582650ccdb5b01f Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Thu, 27 Feb 2025 16:59:16 +0300 Subject: [PATCH 23/41] feat: add MongoDB functional for select, like, dislike cards --- app/mongo_worker.py | 55 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 0de81a8..fb080c2 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -3,6 +3,7 @@ import os import pymongo from schemas.base_schemas import * +from schemas.api_schemas import * from typing import Optional from datetime import datetime @@ -18,9 +19,10 @@ class MongoWorker: password = os.getenv('MONGO_PASS'), uuidRepresentation="standard") self.db = self.client["data"] - self.users_data = self.db["users_data"] + self.users_data = self.db["users"] + self.visited_data = self.db["visited"] self.counters = self.db["counters"] - self.game_data = self.db["game_data"] + self.game_data = self.db["cards"] def check_user(self, user_id: int) -> bool: @@ -85,12 +87,59 @@ class MongoWorker: 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 like_card(self, card_id: int, user_id: int) -> BaseResponse: + 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) + else: + 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) + + def dislike_card(self, card_id: int, user_id: int) -> BaseResponse: + 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) + else: + 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) + if __name__ == "__main__": mongo = MongoWorker() #print(mongo.check_user(123)) - #print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) + print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) #print(mongo.get_user(123)) print(mongo.add_card("A", "B", 123)) #print(mongo.get_random_cards(10, active_status=False)) #print(mongo.update_counter("cards_counter")) + print(mongo.select_choice(1, "A")) + print(mongo.like_card(1, 455412573)) + print(mongo.dislike_card(1, 455412573)) -- 2.54.0 From 0d3338b8866eb0a18dfcb79f85d4e60967a0c599 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sun, 2 Mar 2025 13:49:12 +0300 Subject: [PATCH 24/41] feat: add visited card info --- app/mongo_worker.py | 24 +++++++++++++++++++----- app/schemas/base_schemas.py | 10 ++++++---- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index fb080c2..800f4ba 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -55,6 +55,18 @@ class MongoWorker: upsert=True, return_document=True) return counter["counter"] + + + def get_visited_cards(self, user_id: int) -> Visited: + document = self.visited_data.find_one({"user_id": user_id}) + return Visited.model_validate(document) + + 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(self, choice_A: str, choice_B: str, author_id: int) -> Card: @@ -135,11 +147,13 @@ class MongoWorker: if __name__ == "__main__": mongo = MongoWorker() #print(mongo.check_user(123)) - print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) + #print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) #print(mongo.get_user(123)) - print(mongo.add_card("A", "B", 123)) + #print(mongo.add_card("A", "B", 123)) #print(mongo.get_random_cards(10, active_status=False)) #print(mongo.update_counter("cards_counter")) - print(mongo.select_choice(1, "A")) - print(mongo.like_card(1, 455412573)) - print(mongo.dislike_card(1, 455412573)) + #print(mongo.select_choice(1, "A")) + #print(mongo.like_card(1, 455412573)) + #print(mongo.dislike_card(1, 455412573)) + print(mongo.update_visited_cards(123, 2)) + print(mongo.get_visited_cards(123)) diff --git a/app/schemas/base_schemas.py b/app/schemas/base_schemas.py index d2fe829..eb6bb71 100644 --- a/app/schemas/base_schemas.py +++ b/app/schemas/base_schemas.py @@ -1,5 +1,3 @@ -import uuid - from pydantic import BaseModel class User(BaseModel): @@ -11,12 +9,16 @@ class User(BaseModel): photo_url: str activity: int = 0 - liked_post_ids: list[int] = list() - disliked_post_ids: list[int] = list() + 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 -- 2.54.0 From 82b7a7f6976906ec1767f02f4e3d568dd5e2852f Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sun, 2 Mar 2025 14:19:09 +0300 Subject: [PATCH 25/41] fix: fix get visited cards functional --- app/mongo_worker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 800f4ba..7f1de2c 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -57,9 +57,12 @@ class MongoWorker: return counter["counter"] - def get_visited_cards(self, user_id: int) -> Visited: + def get_visited_cards(self, user_id: int) -> Optional[Visited]: document = self.visited_data.find_one({"user_id": user_id}) - return Visited.model_validate(document) + if not document: + return None + else: + return Visited.model_validate(document) 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}, @@ -156,4 +159,4 @@ if __name__ == "__main__": #print(mongo.like_card(1, 455412573)) #print(mongo.dislike_card(1, 455412573)) print(mongo.update_visited_cards(123, 2)) - print(mongo.get_visited_cards(123)) + print(mongo.get_visited_cards(12)) -- 2.54.0 From a3b7a09d79c732d2f495ef7bcd1d7b23826f333e Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sun, 2 Mar 2025 17:11:50 +0300 Subject: [PATCH 26/41] fix: rework /get_random_cards tests --- app/tests/test_cards.py | 59 +-------------------- app/tests/test_visited_cards.py | 92 +++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 58 deletions(-) create mode 100644 app/tests/test_visited_cards.py diff --git a/app/tests/test_cards.py b/app/tests/test_cards.py index 875bd13..1e0cbab 100644 --- a/app/tests/test_cards.py +++ b/app/tests/test_cards.py @@ -10,8 +10,6 @@ from schemas.api_schemas import * EXIST_AUTHOR = random.randint(100000000, 1000000000) NON_EXIST_CARD_ID = 1000 -NO_ACTIVE_CARDS_STATUS = False -ACTIVE_CARDS_LESS_THAN_TEN = False # ---------- /add_card ---------- @@ -185,59 +183,4 @@ 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 - - -# ---------- /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: - response = await client.get("/get_random_cards") - 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: - response1 = await client.get("/get_random_cards") - response2 = await client.get("/get_random_cards") - 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: - tasks = [client.get("/get_random_cards") 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 + assert response.status_code == 422 \ 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 -- 2.54.0 From 8f6719eb7209056dafda82c5fa354a09bd481314 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sun, 2 Mar 2025 17:14:20 +0300 Subject: [PATCH 27/41] fix: rework /get_random_cards, add visited cards functional --- app/main.py | 40 ++++++++++++++++++++++++++++++++++------ app/mongo_worker.py | 19 +++++++++++++++---- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/app/main.py b/app/main.py index c14b8f6..7bc69be 100644 --- a/app/main.py +++ b/app/main.py @@ -55,14 +55,42 @@ async def get_card(card_id: NonNegativeInt, 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(response: Response, +async def get_random_cards(user_id: NonNegativeInt, + response: Response, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: - result = mongo.get_random_cards(10, True) - if result: - return BaseResponse(result=result) + cards_visited = mongo.get_visited_cards(user_id) + + if cards_visited.result == "User doesn't exist": + response.status_code = status.HTTP_401_UNAUTHORIZED + return BaseResponse(result="User doesn't exist", error=True) + 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) else: - response.status_code = status.HTTP_404_NOT_FOUND - return BaseResponse(result="No active cards", error=True) + result = 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, diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 7f1de2c..7559cfb 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -57,12 +57,23 @@ class MongoWorker: return counter["counter"] - def get_visited_cards(self, user_id: int) -> Optional[Visited]: + def get_visited_cards(self, user_id: int) -> BaseResponse: document = self.visited_data.find_one({"user_id": user_id}) if not document: - return None + 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 Visited.model_validate(document) + return BaseResponse(result=Visited.model_validate(document)) + + def filter_cards(self, random_cards: list[Card], cards_visited: set) -> tuple[list[Optional[Card]], 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}, @@ -159,4 +170,4 @@ if __name__ == "__main__": #print(mongo.like_card(1, 455412573)) #print(mongo.dislike_card(1, 455412573)) print(mongo.update_visited_cards(123, 2)) - print(mongo.get_visited_cards(12)) + print(mongo.get_visited_cards(123).result.cards_visited) -- 2.54.0 From f147512d608c0e297b7331ab60b51e5dfaf102c9 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sun, 2 Mar 2025 17:17:33 +0300 Subject: [PATCH 28/41] fix: fix type hint error --- app/mongo_worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 7559cfb..d4424e5 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -69,7 +69,7 @@ class MongoWorker: else: return BaseResponse(result=Visited.model_validate(document)) - def filter_cards(self, random_cards: list[Card], cards_visited: set) -> tuple[list[Optional[Card]], int]: + 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] -- 2.54.0 From 62d085b898c762d39cf844765657519ad17e64bd Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Tue, 4 Mar 2025 17:20:08 +0300 Subject: [PATCH 29/41] feat: adding automatic setup pre-moderated cards Use _add_base_cards to create or use already created cards --- .github/workflows/app-actions.yml | 4 +- app/tools/__init__.py | 0 app/tools/_add_base_cards.py | 73 ++++++++++++++ app/tools/data/base_cards.json | 152 ++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 app/tools/__init__.py create mode 100644 app/tools/_add_base_cards.py create mode 100644 app/tools/data/base_cards.json diff --git a/.github/workflows/app-actions.yml b/.github/workflows/app-actions.yml index 4bac828..4110409 100644 --- a/.github/workflows/app-actions.yml +++ b/.github/workflows/app-actions.yml @@ -52,6 +52,8 @@ jobs: run: | pip install pytest==8.3.4 pytest-asyncio==0.25.3 httpx==0.28.1 pip install -r 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/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..403f137 --- /dev/null +++ b/app/tools/_add_base_cards.py @@ -0,0 +1,73 @@ +import argparse +import sys +import json +from datetime import datetime +from pathlib import Path + +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/data/base_cards.json b/app/tools/data/base_cards.json new file mode 100644 index 0000000..f57d95e --- /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 -- 2.54.0 From 2c545dc961c970a2f2d9a07178d8fdd4d0fec470 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Tue, 4 Mar 2025 17:25:03 +0300 Subject: [PATCH 30/41] feat: add /select_choice route --- app/main.py | 37 ++++++++++++++++++++++------- app/mongo_worker.py | 11 ++++++++- app/schemas/api_schemas.py | 8 ++++++- app/tools/_add_base_cards.py | 2 +- app/{ => tools}/base_moderation.py | 2 +- app/tools/data/base_cards.json | 4 ++-- app/{ => tools/data}/dirty_words.py | 0 7 files changed, 50 insertions(+), 14 deletions(-) rename app/{ => tools}/base_moderation.py (93%) rename app/{ => tools/data}/dirty_words.py (100%) diff --git a/app/main.py b/app/main.py index 7bc69be..d950856 100644 --- a/app/main.py +++ b/app/main.py @@ -2,13 +2,14 @@ import uvicorn import asyncio from schemas.api_schemas import * +from schemas.base_schemas import * from mongo_worker import MongoWorker -from base_moderation import moderate_text +from tools.base_moderation import moderate_text from fastapi import FastAPI, Depends, Response, status -app = FastAPI() +app: FastAPI = FastAPI() @app.get("/check_user", status_code=200) async def check_user(user_id: NonNegativeInt, @@ -71,7 +72,7 @@ async def get_random_cards(user_id: NonNegativeInt, response.status_code = status.HTTP_404_NOT_FOUND return BaseResponse(result="No active cards", error=True) else: - result = list() + result: list[Card] = list() trys = 3 while len(result) < 10 and trys != 0: random_cards = mongo.get_random_cards(10, True) @@ -91,23 +92,43 @@ async def get_random_cards(user_id: NonNegativeInt, else: return BaseResponse(result=result) - @app.post("/add_card", status_code=201) async def add_card(new_card: AddCardBody, response: Response, mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: if moderate_text(new_card.choice_A) and moderate_text(new_card.choice_B): - card = mongo.add_card(new_card.choice_A, - new_card.choice_B, - new_card.author_id) + 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(MongoWorker)) -> 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!") async def main(): - config = uvicorn.Config("main:app", port=5000, log_level="info") + config = uvicorn.Config("main:app", port=5000, log_level="debug") server = uvicorn.Server(config) await server.serve() diff --git a/app/mongo_worker.py b/app/mongo_worker.py index d4424e5..678d6ef 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -83,7 +83,7 @@ class MongoWorker: return Visited.model_validate(update_visited) - def add_card(self, choice_A: str, choice_B: str, author_id: int) -> Card: + 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, @@ -96,6 +96,15 @@ class MongoWorker: print(exception) return new_card + def add_card_by_base_model(self, new_card: Card) -> 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: diff --git a/app/schemas/api_schemas.py b/app/schemas/api_schemas.py index e71c99e..e8e33d0 100644 --- a/app/schemas/api_schemas.py +++ b/app/schemas/api_schemas.py @@ -19,4 +19,10 @@ class AddCardBody(BaseModel): choice_A: str choice_B: str - author_id: NonNegativeInt \ No newline at end of file + author_id: NonNegativeInt + +class SelectChoice(BaseModel): + user_id: NonNegativeInt + card_id: NonNegativeInt + + choice: typing.Literal["A", "B"] \ No newline at end of file diff --git a/app/tools/_add_base_cards.py b/app/tools/_add_base_cards.py index 403f137..83cc700 100644 --- a/app/tools/_add_base_cards.py +++ b/app/tools/_add_base_cards.py @@ -4,7 +4,7 @@ import json from datetime import datetime from pathlib import Path -sys.path.append('..') +#sys.path.append('..') from mongo_worker import MongoWorker from schemas.base_schemas import Card diff --git a/app/base_moderation.py b/app/tools/base_moderation.py similarity index 93% rename from app/base_moderation.py rename to app/tools/base_moderation.py index 5623d2f..ad6772a 100644 --- a/app/base_moderation.py +++ b/app/tools/base_moderation.py @@ -1,6 +1,6 @@ import re -from dirty_words import dirty_words_set +from tools.data.dirty_words import dirty_words_set def is_not_empty(text: str) -> bool: """Checks that the text is not empty.""" diff --git a/app/tools/data/base_cards.json b/app/tools/data/base_cards.json index f57d95e..8320fcc 100644 --- a/app/tools/data/base_cards.json +++ b/app/tools/data/base_cards.json @@ -31,8 +31,8 @@ }, { "card_id": 3, - "choice_A": "Поездка в горы", - "choice_B": "Поездка на море", + "choice_A": "Отпуск в горах", + "choice_B": "Отпуск на море", "count_choice_A": 0, "count_choice_B": 0, "count_total": 0, diff --git a/app/dirty_words.py b/app/tools/data/dirty_words.py similarity index 100% rename from app/dirty_words.py rename to app/tools/data/dirty_words.py -- 2.54.0 From 6a7bed28903b617e2bec5861b1ecf8cef8588555 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Tue, 4 Mar 2025 17:28:00 +0300 Subject: [PATCH 31/41] fix: fix _add_base_cards bug in GitHub action --- app/tools/_add_base_cards.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/tools/_add_base_cards.py b/app/tools/_add_base_cards.py index 83cc700..0e8ce00 100644 --- a/app/tools/_add_base_cards.py +++ b/app/tools/_add_base_cards.py @@ -2,9 +2,8 @@ import argparse import sys import json from datetime import datetime -from pathlib import Path -#sys.path.append('..') +sys.path.append('..') from mongo_worker import MongoWorker from schemas.base_schemas import Card -- 2.54.0 From de2224406f36590bc34b6066972e273b7495515b Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Thu, 6 Mar 2025 14:20:28 +0300 Subject: [PATCH 32/41] feat: add /like_card and /dislike_card routs --- app/main.py | 22 ++++++++++++++++++++++ app/mongo_worker.py | 44 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/app/main.py b/app/main.py index d950856..21ead3c 100644 --- a/app/main.py +++ b/app/main.py @@ -125,7 +125,29 @@ async def select_choice(choice_data: SelectChoice, 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(MongoWorker)) -> 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(MongoWorker)) -> 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 + async def main(): config = uvicorn.Config("main:app", port=5000, log_level="debug") diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 678d6ef..5ffeadc 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -140,31 +140,55 @@ class MongoWorker: else: return BaseResponse(result=result, error=False) - def like_card(self, card_id: int, user_id: int) -> BaseResponse: - 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) + 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: - 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) - else: + 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) if __name__ == "__main__": -- 2.54.0 From d05e62a032a2c8fdaa868571246f6a58ddc858ed Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Thu, 6 Mar 2025 14:21:34 +0300 Subject: [PATCH 33/41] feat: add /like_card and /dislike_card routs --- app/schemas/api_schemas.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/schemas/api_schemas.py b/app/schemas/api_schemas.py index e8e33d0..0cccd80 100644 --- a/app/schemas/api_schemas.py +++ b/app/schemas/api_schemas.py @@ -25,4 +25,8 @@ class SelectChoice(BaseModel): user_id: NonNegativeInt card_id: NonNegativeInt - choice: typing.Literal["A", "B"] \ No newline at end of file + choice: typing.Literal["A", "B"] + +class ReactionCard(BaseModel): + user_id: NonNegativeInt + card_id: NonNegativeInt \ No newline at end of file -- 2.54.0 From f092e189c262e3487b7da77970fc5c2717b8ca1c Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Thu, 6 Mar 2025 16:28:09 +0300 Subject: [PATCH 34/41] feat: add /comment rout --- app/main.py | 18 ++++++++++++++++++ app/mongo_worker.py | 27 +++++++++++++++++++++++++-- app/schemas/api_schemas.py | 8 +++++++- app/schemas/base_schemas.py | 11 ++++++++++- 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index 21ead3c..c8d6da3 100644 --- a/app/main.py +++ b/app/main.py @@ -148,6 +148,24 @@ async def dislike_card(dislike_data: ReactionCard, 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(MongoWorker)) -> 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") diff --git a/app/mongo_worker.py b/app/mongo_worker.py index 5ffeadc..f6496ef 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -16,13 +16,13 @@ class MongoWorker: 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'), - uuidRepresentation="standard") + password = os.getenv('MONGO_PASS')) self.db = self.client["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 check_user(self, user_id: int) -> bool: @@ -189,6 +189,29 @@ class MongoWorker: 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) if __name__ == "__main__": diff --git a/app/schemas/api_schemas.py b/app/schemas/api_schemas.py index 0cccd80..745d791 100644 --- a/app/schemas/api_schemas.py +++ b/app/schemas/api_schemas.py @@ -29,4 +29,10 @@ class SelectChoice(BaseModel): class ReactionCard(BaseModel): user_id: NonNegativeInt - card_id: NonNegativeInt \ No newline at end of file + 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 index eb6bb71..52f5464 100644 --- a/app/schemas/base_schemas.py +++ b/app/schemas/base_schemas.py @@ -36,4 +36,13 @@ class Card(BaseModel): author_id: int creation_date: str moderation_date: str = "Not moderated" - active_status: bool = False \ No newline at end of file + 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 -- 2.54.0 From 4ddcf859b35ab32df96f3bdd40ed042c27509404 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 8 Mar 2025 14:31:01 +0300 Subject: [PATCH 35/41] refactor: remove extra elements --- app/.env | 5 ---- app/main.py | 71 ++++++++++++++++++++++++--------------------- app/mongo_worker.py | 27 ++++------------- 3 files changed, 44 insertions(+), 59 deletions(-) delete mode 100644 app/.env diff --git a/app/.env b/app/.env deleted file mode 100644 index 3944b68..0000000 --- a/app/.env +++ /dev/null @@ -1,5 +0,0 @@ -MONGO_HOST = 127.0.0.1 -MONGO_PORT = 27017 - -MONGO_USER = user -MONGO_PASS = pass \ No newline at end of file diff --git a/app/main.py b/app/main.py index c8d6da3..9f0598c 100644 --- a/app/main.py +++ b/app/main.py @@ -1,26 +1,30 @@ import uvicorn import asyncio +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 -from fastapi import FastAPI, Depends, Response, status - -app: FastAPI = FastAPI() +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"}) +mongo_worker = MongoWorker() @app.get("/check_user", status_code=200) async def check_user(user_id: NonNegativeInt, - mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + 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(MongoWorker)) -> BaseResponse: + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: if mongo.check_user(user_id): result = mongo.get_user(user_id) return BaseResponse(result=result) @@ -31,7 +35,7 @@ async def get_user(user_id: NonNegativeInt, @app.post("/add_user", status_code=201) async def add_user(new_user: AddUserBody, response: Response, - mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + 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, @@ -47,7 +51,7 @@ async def add_user(new_user: AddUserBody, @app.get("/get_card", status_code=200) async def get_card(card_id: NonNegativeInt, response: Response, - mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: card = mongo.get_card(card_id) if card: return BaseResponse(result=card) @@ -58,12 +62,12 @@ async def get_card(card_id: NonNegativeInt, @app.get("/get_random_cards", status_code=200) async def get_random_cards(user_id: NonNegativeInt, response: Response, - mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse: cards_visited = mongo.get_visited_cards(user_id) - if cards_visited.result == "User doesn't exist": + if cards_visited.error: response.status_code = status.HTTP_401_UNAUTHORIZED - return BaseResponse(result="User doesn't exist", error=True) + return cards_visited elif not cards_visited.result.cards_visited: random_cards = mongo.get_random_cards(10, True) if random_cards: @@ -71,31 +75,32 @@ async def get_random_cards(user_id: NonNegativeInt, else: response.status_code = status.HTTP_404_NOT_FOUND return BaseResponse(result="No active cards", error=True) - else: - 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: + + 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 fo this user", error=True) + 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: - return BaseResponse(result=result) + 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(MongoWorker)) -> BaseResponse: + 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, @@ -109,7 +114,7 @@ async def add_card(new_card: AddCardBody, @app.patch("/select_choice", status_code=200) async def select_choice(choice_data: SelectChoice, response: Response, - mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + 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 @@ -129,7 +134,7 @@ async def select_choice(choice_data: SelectChoice, @app.patch("/like_card", status_code=200) async def like_card(like_data: ReactionCard, response: Response, - mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + 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") @@ -140,7 +145,7 @@ async def like_card(like_data: ReactionCard, @app.patch("/dislike_card", status_code=200) async def dislike_card(dislike_data: ReactionCard, response: Response, - mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + 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") @@ -151,7 +156,7 @@ async def dislike_card(dislike_data: ReactionCard, @app.post("/comment", status_code=201) async def comment(comment_info: AddCommentBody, response: Response, - mongo: MongoWorker = Depends(MongoWorker)) -> BaseResponse: + 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) diff --git a/app/mongo_worker.py b/app/mongo_worker.py index f6496ef..d3133a4 100644 --- a/app/mongo_worker.py +++ b/app/mongo_worker.py @@ -2,12 +2,12 @@ import os import pymongo -from schemas.base_schemas import * -from schemas.api_schemas import * -from typing import Optional - from datetime import datetime from dotenv import load_dotenv +from typing import Optional + +from schemas.base_schemas import * +from schemas.api_schemas import * class MongoWorker: @@ -96,7 +96,7 @@ class MongoWorker: print(exception) return new_card - def add_card_by_base_model(self, new_card: Card) -> 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()) @@ -211,19 +211,4 @@ class MongoWorker: else: return BaseResponse(result="Card doesn't exist", error=True) else: - return BaseResponse(result="User doesn't exist", error=True) - - -if __name__ == "__main__": - mongo = MongoWorker() - #print(mongo.check_user(123)) - #print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "photo_0.jpg")) - #print(mongo.get_user(123)) - #print(mongo.add_card("A", "B", 123)) - #print(mongo.get_random_cards(10, active_status=False)) - #print(mongo.update_counter("cards_counter")) - #print(mongo.select_choice(1, "A")) - #print(mongo.like_card(1, 455412573)) - #print(mongo.dislike_card(1, 455412573)) - print(mongo.update_visited_cards(123, 2)) - print(mongo.get_visited_cards(123).result.cards_visited) + return BaseResponse(result="User doesn't exist", error=True) \ No newline at end of file -- 2.54.0 From 4c39153a75a12a7cddfdcaa336d93eeec10d4ead Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 8 Mar 2025 18:43:36 +0300 Subject: [PATCH 36/41] fix: file location changed --- .env | 4 ++++ .github/workflows/app-actions.yml | 7 ++++--- README.md | 10 +++++++++- requirements.txt => app/requirements.txt | 0 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 .env rename requirements.txt => app/requirements.txt (100%) diff --git a/.env b/.env new file mode 100644 index 0000000..5263644 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +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 index 4110409..42e030e 100644 --- a/.github/workflows/app-actions.yml +++ b/.github/workflows/app-actions.yml @@ -28,7 +28,7 @@ jobs: - name: Install dependencies run: | pip install mypy - pip install -r requirements.txt + pip install -r app/requirements.txt - name: Run mypy run: mypy --ignore-missing-imports ./app @@ -46,13 +46,14 @@ jobs: architecture: x64 - name: Setup MongoDB - run: docker run --name mongodb -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=user -e MONGO_INITDB_ROOT_PASSWORD=pass mongodb/mongodb-community-server + run: docker run --name mongodb -d -p $MONGO_PORT:27017 -e MONGO_INITDB_ROOT_USERNAME=$MONGO_USER -e MONGO_INITDB_ROOT_PASSWORD=$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 requirements.txt + 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 diff --git a/README.md b/README.md index 67a106c..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 -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/requirements.txt b/app/requirements.txt similarity index 100% rename from requirements.txt rename to app/requirements.txt -- 2.54.0 From 9d50d113d523647f295f9e43cec8af7751fdd02f Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 8 Mar 2025 18:46:04 +0300 Subject: [PATCH 37/41] build: add dockerfile and docker-compose file for automatically build --- app/dockerfile | 11 +++++++++++ docker-compose.yml | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 app/dockerfile create mode 100644 docker-compose.yml 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/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} + -- 2.54.0 From adb1b463eb85e96452397028756f0603beaf8397 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 8 Mar 2025 20:53:37 +0300 Subject: [PATCH 38/41] build: disabling OpenAPI via env --- .env | 2 ++ .github/workflows/app-actions.yml | 2 ++ app/main.py | 12 +++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.env b/.env index 5263644..1e2f09a 100644 --- a/.env +++ b/.env @@ -1,3 +1,5 @@ +DISABLE_DOCS=true + MONGO_HOST=127.0.0.1 MONGO_PORT=27017 MONGO_USER=user diff --git a/.github/workflows/app-actions.yml b/.github/workflows/app-actions.yml index 42e030e..1e7d1d8 100644 --- a/.github/workflows/app-actions.yml +++ b/.github/workflows/app-actions.yml @@ -10,6 +10,8 @@ on: pull_request: branches: - main + workflow_dispatch: + jobs: mypy: diff --git a/app/main.py b/app/main.py index 9f0598c..e5cbb3e 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,9 @@ +import os + import uvicorn import asyncio +from dotenv import load_dotenv from fastapi import FastAPI, Depends, Response, status from schemas.api_schemas import * @@ -9,12 +12,19 @@ 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"}) + 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: -- 2.54.0 From af3424fca812952fa578eb62d10bd9b42401d022 Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 8 Mar 2025 20:58:25 +0300 Subject: [PATCH 39/41] fix: fix workflow --- .github/workflows/app-actions.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/app-actions.yml b/.github/workflows/app-actions.yml index 1e7d1d8..22c2c6f 100644 --- a/.github/workflows/app-actions.yml +++ b/.github/workflows/app-actions.yml @@ -48,6 +48,11 @@ jobs: architecture: x64 - name: Setup MongoDB + env: + MONGO_HOST: ${{ env.MONGO_HOST }} + MONGO_PORT: ${{ env.MONGO_PORT }} + MONGO_USER: ${{ env.MONGO_USER }} + MONGO_PASS: ${{ env.MONGO_PASS }} run: docker run --name mongodb -d -p $MONGO_PORT:27017 -e MONGO_INITDB_ROOT_USERNAME=$MONGO_USER -e MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASS mongodb/mongodb-community-server - name: Install dependencies -- 2.54.0 From 90939d2634b4400e5c2c1b7b3c068f46ea5fdbdf Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 8 Mar 2025 21:10:43 +0300 Subject: [PATCH 40/41] fix: fix workflow --- .github/workflows/app-actions.yml | 3 ++- app/main.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/app-actions.yml b/.github/workflows/app-actions.yml index 22c2c6f..656633e 100644 --- a/.github/workflows/app-actions.yml +++ b/.github/workflows/app-actions.yml @@ -1,6 +1,7 @@ name: app-actions on: + workflow_dispatch: push: paths: - '**.py' @@ -10,7 +11,7 @@ on: pull_request: branches: - main - workflow_dispatch: + jobs: diff --git a/app/main.py b/app/main.py index e5cbb3e..fdb7be6 100644 --- a/app/main.py +++ b/app/main.py @@ -183,7 +183,7 @@ async def comment(comment_info: AddCommentBody, async def main(): - config = uvicorn.Config("main:app", port=5000, log_level="debug") + config = uvicorn.Config("main:app", port=5000, log_level="debug",) server = uvicorn.Server(config) await server.serve() -- 2.54.0 From b58ed47247a56887253f006b27dc8d6c4c651a1d Mon Sep 17 00:00:00 2001 From: IgorVolochay Date: Sat, 8 Mar 2025 21:44:06 +0300 Subject: [PATCH 41/41] fix: fix workflow --- .github/workflows/app-actions.yml | 13 ++++++------- app/main.py | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/app-actions.yml b/.github/workflows/app-actions.yml index 656633e..9af1516 100644 --- a/.github/workflows/app-actions.yml +++ b/.github/workflows/app-actions.yml @@ -13,7 +13,6 @@ on: - main - jobs: mypy: runs-on: ubuntu-latest @@ -38,6 +37,11 @@ jobs: 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 @@ -49,12 +53,7 @@ jobs: architecture: x64 - name: Setup MongoDB - env: - MONGO_HOST: ${{ env.MONGO_HOST }} - MONGO_PORT: ${{ env.MONGO_PORT }} - MONGO_USER: ${{ env.MONGO_USER }} - MONGO_PASS: ${{ env.MONGO_PASS }} - run: docker run --name mongodb -d -p $MONGO_PORT:27017 -e MONGO_INITDB_ROOT_USERNAME=$MONGO_USER -e MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASS mongodb/mongodb-community-server + 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: | diff --git a/app/main.py b/app/main.py index fdb7be6..e5cbb3e 100644 --- a/app/main.py +++ b/app/main.py @@ -183,7 +183,7 @@ async def comment(comment_info: AddCommentBody, async def main(): - config = uvicorn.Config("main:app", port=5000, log_level="debug",) + config = uvicorn.Config("main:app", port=5000, log_level="debug") server = uvicorn.Server(config) await server.serve() -- 2.54.0