diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..51d5a5c --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 120 +extend-ignore = E203 \ No newline at end of file diff --git a/Pipfile b/Pipfile index 1e6cdf3..dc9527d 100644 --- a/Pipfile +++ b/Pipfile @@ -28,6 +28,7 @@ coverage = "*" pytest = "*" pytest-asyncio = "*" flake8 = "*" +black = "*" [requires] python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock index d49506f..1018ce8 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5764345626b30779222e0a592f2ab91ce0938823d1baa12a4168ceed03a95170" + "sha256": "075e07880b28878645b61e807c6091ae0ecdef50b3486e047aa853023fc2ae33" }, "pipfile-spec": 6, "requires": { @@ -331,7 +331,7 @@ "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" ], - "markers": "python_version >= '3.6' and python_full_version < '4.0.0'", + "markers": "python_version >= '3.6' and python_version < '4'", "version": "==2.2.1" }, "ecdsa": { @@ -933,7 +933,7 @@ "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17", "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb" ], - "markers": "python_version >= '3.6' and python_full_version < '4.0.0'", + "markers": "python_version >= '3.6' and python_version < '4'", "version": "==4.8" }, "sentry-sdk": { @@ -1031,7 +1031,7 @@ "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.26.9" }, "uvicorn": { @@ -1284,6 +1284,43 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==21.4.0" }, + "black": { + "hashes": [ + "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b", + "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176", + "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09", + "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a", + "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015", + "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79", + "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb", + "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20", + "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464", + "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968", + "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82", + "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21", + "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0", + "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265", + "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b", + "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a", + "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72", + "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce", + "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0", + "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a", + "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163", + "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad", + "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d" + ], + "index": "pypi", + "version": "==22.3.0" + }, + "click": { + "hashes": [ + "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e", + "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.2" + }, "coverage": { "hashes": [ "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9", @@ -1353,6 +1390,13 @@ ], "version": "==0.6.1" }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, "packaging": { "hashes": [ "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", @@ -1361,6 +1405,21 @@ "markers": "python_version >= '3.6'", "version": "==21.3" }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, + "platformdirs": { + "hashes": [ + "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d", + "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227" + ], + "markers": "python_version >= '3.7'", + "version": "==2.5.1" + }, "pluggy": { "hashes": [ "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", @@ -1423,7 +1482,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.11'", "version": "==2.0.1" } } diff --git a/classquiz/auth.py b/classquiz/auth.py index 6bfc97e..17d711c 100644 --- a/classquiz/auth.py +++ b/classquiz/auth.py @@ -28,11 +28,11 @@ ACCESS_TOKEN_EXPIRE_MINUTES = settings.access_token_expire_minutes class OAuth2PasswordBearerWithCookie(OAuth2): def __init__( - self, - tokenUrl: str, - scheme_name: Optional[str] = None, - scopes: Optional[Dict[str, str]] = None, - auto_error: bool = True, + self, + tokenUrl: str, + scheme_name: Optional[str] = None, + scopes: Optional[Dict[str, str]] = None, + auto_error: bool = True, ): if not scopes: scopes = {} diff --git a/classquiz/config.py b/classquiz/config.py index 7c348a9..0cde720 100644 --- a/classquiz/config.py +++ b/classquiz/config.py @@ -10,6 +10,7 @@ class Settings(BaseSettings): """ Settings class for the shop app. """ + root_address: str = "http://127.0.0.1:8000" redis: RedisDsn = "redis://localhost:6379/0?decode_responses=True" skip_email_verification: bool = False @@ -36,7 +37,7 @@ class Settings(BaseSettings): class Config: env_file = ".env" - env_file_encoding = 'utf-8' + env_file_encoding = "utf-8" @lru_cache() @@ -45,5 +46,9 @@ def settings() -> Settings: redis: redis_lib.client.Redis = redis_lib.Redis().from_url(settings().redis) -storage: Storage = Storage(backend=settings().storage_backend, deta_key=settings().deta_project_key, - deta_id=settings().deta_project_id, storage_path=settings().storage_path) +storage: Storage = Storage( + backend=settings().storage_backend, + deta_key=settings().deta_project_key, + deta_id=settings().deta_project_id, + storage_path=settings().storage_path, +) diff --git a/classquiz/db/models.py b/classquiz/db/models.py index a9d730e..a91c962 100644 --- a/classquiz/db/models.py +++ b/classquiz/db/models.py @@ -11,6 +11,7 @@ class User(ormar.Model): """ The user model in the database """ + id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4()) email: str = ormar.String(unique=True, max_length=100) username: str = ormar.String(unique=True, max_length=100) @@ -20,7 +21,7 @@ class User(ormar.Model): created_at: datetime = ormar.DateTime(default=datetime.now()) class Meta: - tablename = 'users' + tablename = "users" metadata = metadata database = database @@ -29,6 +30,7 @@ class UserSession(ormar.Model): """ The user session model for user-sessions """ + id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4()) user: uuid.UUID = ormar.ForeignKey(User) session_key: str = ormar.String(unique=True, max_length=64) @@ -38,7 +40,7 @@ class UserSession(ormar.Model): last_seen: datetime = ormar.DateTime(default=datetime.now()) class Meta: - tablename = 'user_sessions' + tablename = "user_sessions" metadata = metadata database = database @@ -73,7 +75,7 @@ class Quiz(ormar.Model): questions: Json[list[QuizQuestion]] = ormar.JSON(nullable=False) class Meta: - tablename = 'quiz' + tablename = "quiz" metadata = metadata database = database @@ -82,6 +84,7 @@ class Token(BaseModel): """ For JWT """ + access_token: str token_type: str @@ -90,6 +93,7 @@ class TokenData(BaseModel): """ For JWT """ + email: str | None = None @@ -97,6 +101,7 @@ class PlayGame(BaseModel): """ For JWT """ + quiz_id: uuid.UUID | str description: str title: str diff --git a/classquiz/emails/__init__.py b/classquiz/emails/__init__.py index bf650e9..995d86f 100644 --- a/classquiz/emails/__init__.py +++ b/classquiz/emails/__init__.py @@ -12,17 +12,18 @@ from classquiz.db.models import User settings = settings() jinja = Environment( - loader=PackageLoader('classquiz.emails', 'templates'), - autoescape=select_autoescape(['html', 'xml']), - enable_async=True) + loader=PackageLoader("classquiz.emails", "templates"), + autoescape=select_autoescape(["html", "xml"]), + enable_async=True, +) def _sendMail(template: str, to: str, subject: str): - msg = MIMEMultipart('alternative') - msg['Subject'] = subject - msg['From'] = settings.mail_address - msg['To'] = to - msg.attach(MIMEText(template, 'html')) + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = settings.mail_address + msg["To"] = to + msg.attach(MIMEText(template, "html")) context = ssl.SSLContext(ssl.PROTOCOL_TLS) server = smtplib.SMTP(host=settings.mail_server, port=settings.mail_port) server.ehlo() @@ -35,25 +36,19 @@ def _sendMail(template: str, to: str, subject: str): async def send_register_email(email: str): user = await User.objects.get_or_none(email=email, verified=False) if user is None: - raise ValueError('User not found') - template = jinja.get_template('register.jinja2') - template = await template.render_async( - base_url=settings.root_address, - token=user.verify_key - ) - _sendMail(template=template, to=email, subject='Verify your email') + raise ValueError("User not found") + template = jinja.get_template("register.jinja2") + template = await template.render_async(base_url=settings.root_address, token=user.verify_key) + _sendMail(template=template, to=email, subject="Verify your email") async def send_forgotten_password_email(email: str): user = await User.objects.get_or_none(email=email) if user is None: - raise ValueError('User not found') - template = jinja.get_template('forgotten_password.jinja2') + raise ValueError("User not found") + template = jinja.get_template("forgotten_password.jinja2") token = os.urandom(32).hex() - template = await template.render_async( - base_url=settings.root_address, - token=token - ) + template = await template.render_async(base_url=settings.root_address, token=token) await redis.set(f"reset_passwd:{token}", str(user.id), ex=3600) - _sendMail(template=template, to=email, subject='Reset your password') + _sendMail(template=template, to=email, subject="Reset your password") pass diff --git a/classquiz/kahoot_importer/import_quiz.py b/classquiz/kahoot_importer/import_quiz.py index ac749f7..8eb43d4 100644 --- a/classquiz/kahoot_importer/import_quiz.py +++ b/classquiz/kahoot_importer/import_quiz.py @@ -53,6 +53,7 @@ async def _download_image(url: str) -> bytes: # data = await resp.json() # return data["data"]["id"] + async def import_quiz(quiz_id: str, user: User) -> Quiz | str: """ Imports a quiz from Kahoot. @@ -78,8 +79,21 @@ async def import_quiz(quiz_id: str, user: User) -> Quiz | str: for a in q.choices: answers.append((QuizAnswer(right=a.correct, answer=html.unescape(a.answer)))) quiz_questions.append( - QuizQuestion(question=q.question, answers=answers, time=str(q.time / 1000), image=image).dict()) - quiz_data = Quiz(id=quiz_id, public=False, title=quiz.kahoot.title, description=quiz.kahoot.description, - created_at=datetime.now(), updated_at=datetime.now(), user_id=user.id, - questions=json.dumps(quiz_questions)) + QuizQuestion( + question=q.question, + answers=answers, + time=str(q.time / 1000), + image=image, + ).dict() + ) + quiz_data = Quiz( + id=quiz_id, + public=False, + title=quiz.kahoot.title, + description=quiz.kahoot.description, + created_at=datetime.now(), + updated_at=datetime.now(), + user_id=user.id, + questions=json.dumps(quiz_questions), + ) return await quiz_data.save() diff --git a/classquiz/kahoot_importer/search.py b/classquiz/kahoot_importer/search.py index 52c027a..d7f19e0 100644 --- a/classquiz/kahoot_importer/search.py +++ b/classquiz/kahoot_importer/search.py @@ -14,8 +14,13 @@ class _Response(BaseModel): pageTimestamp: int -async def search(query: str | None, limit: int | None = 9, cursor: int | None = 1, - search_cluster: int | None = 1, inventory_item_id: str | None = "ANY") -> _Response: +async def search( + query: str | None, + limit: int | None = 9, + cursor: int | None = 1, + search_cluster: int | None = 1, + inventory_item_id: str | None = "ANY", +) -> _Response: """ :param inventory_item_id: I dkon't know @@ -27,8 +32,7 @@ async def search(query: str | None, limit: int | None = 9, cursor: int | None = """ async with ClientSession() as session: async with session.get( - f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}" # noqa : E501 - + f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}" # noqa : E501 ) as response: # print( # f"https://create.kahoot.it/rest/kahoots/?query={query}&limit={limit}&cursor={cursor}&searchCluster={search_cluster}&includeExtendedCounters=false&inventoryItemId={inventory_item_id}") diff --git a/classquiz/routers/quiz.py b/classquiz/routers/quiz.py index 1aa365d..16ad00e 100644 --- a/classquiz/routers/quiz.py +++ b/classquiz/routers/quiz.py @@ -62,8 +62,14 @@ async def start_quiz(quiz_id: str, user: User = Depends(get_current_user)): return JSONResponse(status_code=404, content={"detail": "quiz not found"}) else: game_pin = randint(10000000, 99999999) - game = PlayGame(quiz_id=quiz_id, game_pin=str(game_pin), questions=quiz.questions, game_id=uuid.uuid4(), - title=quiz.title, description=quiz.description) + game = PlayGame( + quiz_id=quiz_id, + game_pin=str(game_pin), + questions=quiz.questions, + game_id=uuid.uuid4(), + title=quiz.title, + description=quiz.description, + ) await redis.set(f"game:{str(game.game_pin)}", (game.json()), ex=18000) return {**quiz.dict(exclude={"id"}), **game.dict(exclude={"questions"})} diff --git a/classquiz/routers/storage.py b/classquiz/routers/storage.py index 23728aa..d925a55 100644 --- a/classquiz/routers/storage.py +++ b/classquiz/routers/storage.py @@ -12,7 +12,7 @@ router = APIRouter() file_regex = r"^[a-z0-9]{8}-[a-z0-9-]{27}--[a-z0-9-]{36}$" -@router.get('/download/{file_name}') +@router.get("/download/{file_name}") async def download_file(file_name: str): if not re.match(file_regex, file_name): raise HTTPException(status_code=400, detail="Invalid file name") @@ -24,5 +24,8 @@ async def download_file(file_name: str): def iter_file(): yield from download - return StreamingResponse(iter_file(), media_type='image/*', - headers={"Cache-Control": "public, immutable, max-age=31536000"}) + return StreamingResponse( + iter_file(), + media_type="image/*", + headers={"Cache-Control": "public, immutable, max-age=31536000"}, + ) diff --git a/classquiz/routers/users.py b/classquiz/routers/users.py index de01ef5..b388c96 100644 --- a/classquiz/routers/users.py +++ b/classquiz/routers/users.py @@ -7,18 +7,32 @@ from fastapi.background import BackgroundTasks from fastapi.responses import JSONResponse, RedirectResponse from fastapi.security import OAuth2PasswordRequestForm -from classquiz.auth import get_password_hash, verify_password, authenticate_user, create_access_token, get_current_user +from classquiz.auth import ( + get_password_hash, + verify_password, + authenticate_user, + create_access_token, + get_current_user, +) from classquiz.cache import clear_cache_for_account from classquiz.config import redis, settings import uuid from pydantic import BaseModel from classquiz.db.models import User, UserSession, UpdatePassword, Token from classquiz.emails import send_register_email, send_forgotten_password_email + settings = settings() router = APIRouter() route_user = User.get_pydantic( - exclude={"id": ..., "verified": ..., "verify_key": ..., "created_at": ..., "usersessions": ...}) + exclude={ + "id": ..., + "verified": ..., + "verify_key": ..., + "created_at": ..., + "usersessions": ..., + } +) async def _sign_out_everywhere(user: User) -> None: @@ -26,8 +40,11 @@ async def _sign_out_everywhere(user: User) -> None: await clear_cache_for_account(user) -@router.post("/create", response_model=User, - response_model_include={"id": ..., "verified": ..., "email": ...}) +@router.post( + "/create", + response_model=User, + response_model_include={"id": ..., "verified": ..., "email": ...}, +) async def create_user(user: route_user, background_task: BackgroundTasks) -> User | JSONResponse: user = User(**user.dict(), id=uuid.uuid4()) try: @@ -50,8 +67,11 @@ async def create_user(user: route_user, background_task: BackgroundTasks) -> Use @router.post("/token/cookie", response_model=Token) -async def login_for_cookie_access_token(request: Request, response: Response, - form_data: OAuth2PasswordRequestForm = Depends()): +async def login_for_cookie_access_token( + request: Request, + response: Response, + form_data: OAuth2PasswordRequestForm = Depends(), +): user = await authenticate_user(form_data.username, form_data.password) if not user: raise HTTPException( @@ -59,21 +79,29 @@ async def login_for_cookie_access_token(request: Request, response: Response, detail="Incorrect username or password", ) session_key = os.urandom(32).hex() - user_session = UserSession(user=user, session_key=session_key, ip_address=request.client.host, - user_agent=request.headers.get("User-Agent"), id=uuid.uuid4()) + user_session = UserSession( + user=user, + session_key=session_key, + ip_address=request.client.host, + user_agent=request.headers.get("User-Agent"), + id=uuid.uuid4(), + ) await user_session.save() # await user_session.save() access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) - access_token = create_access_token( - data={"sub": user.email}, expires_delta=access_token_expires - ) + access_token = create_access_token(data={"sub": user.email}, expires_delta=access_token_expires) await redis.set(access_token, user.email, ex=settings.access_token_expire_minutes * 60) - response.set_cookie(key="access_token", value=f"Bearer {access_token}", - httponly=True, samesite='strict', max_age=settings.access_token_expire_minutes * 60) + response.set_cookie( + key="access_token", + value=f"Bearer {access_token}", + httponly=True, + samesite="strict", + max_age=settings.access_token_expire_minutes * 60, + ) response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60) response.set_cookie(key="rememberme", value="") - response.set_cookie(key="rememberme_token", value=session_key, httponly=True, samesite='strict') + response.set_cookie(key="rememberme_token", value=session_key, httponly=True, samesite="strict") return {"access_token": access_token, "token_type": "bearer"} @@ -82,16 +110,20 @@ async def rememberme_token(request: Request, response: Response): rememberme_cookie = request.cookies.get("rememberme_token") if rememberme_cookie is None: raise HTTPException(status_code=400, detail="No rememberme cookie") - user_session: UserSession | None = await UserSession.objects.filter(session_key=rememberme_cookie).select_related( - UserSession.user).get_or_none() + user_session: UserSession | None = ( + await UserSession.objects.filter(session_key=rememberme_cookie).select_related(UserSession.user).get_or_none() + ) if (user_session is None) or (user_session.user is None): raise HTTPException(status_code=401, detail="No user session") access_token_expires = timedelta(minutes=settings.access_token_expire_minutes * 60) - access_token = create_access_token( - data={"sub": user_session.user.email}, expires_delta=access_token_expires + access_token = create_access_token(data={"sub": user_session.user.email}, expires_delta=access_token_expires) + response.set_cookie( + key="access_token", + value=f"Bearer {access_token}", + httponly=True, + samesite="strict", + max_age=settings.access_token_expire_minutes * 60, ) - response.set_cookie(key="access_token", value=f"Bearer {access_token}", - httponly=True, samesite='strict', max_age=settings.access_token_expire_minutes * 60) response.set_cookie(key="expiry", value="", max_age=settings.access_token_expire_minutes * 60) await user_session.update(last_seen=datetime.now()) @@ -127,7 +159,11 @@ async def verify_user(verify_key: str): @router.put("/password/update") -async def change_password(password_data: UpdatePassword, response: Response, user: User = Depends(get_current_user)): +async def change_password( + password_data: UpdatePassword, + response: Response, + user: User = Depends(get_current_user), +): if not verify_password(password_data.old_password, user.password): raise HTTPException(status_code=400, detail="Incorrect password") user.password = get_password_hash(password_data.new_password) diff --git a/classquiz/socket_server/__init__.py b/classquiz/socket_server/__init__.py index 08e9c02..e7eac75 100644 --- a/classquiz/socket_server/__init__.py +++ b/classquiz/socket_server/__init__.py @@ -14,8 +14,10 @@ settings = settings() async def join_game(sid, data): async with aiohttp.ClientSession() as session: try: - async with session.post("https://hcaptcha.com/siteverify", - data={"response": data["captcha"], "secret": settings.hcaptcha_key}) as resp: + async with session.post( + "https://hcaptcha.com/siteverify", + data={"response": data["captcha"], "secret": settings.hcaptcha_key}, + ) as resp: resp_data = await resp.json() if not resp_data["success"]: print("CAPTCHA FAILED") @@ -27,17 +29,34 @@ async def join_game(sid, data): if redis_res is None: await sio.emit("game_not_found", room=sid) else: - session = {'game_pin': data["game_pin"], "username": data["username"], "admin": False} + session = { + "game_pin": data["game_pin"], + "username": data["username"], + "admin": False, + } await sio.save_session(sid, session) await sio.emit("joined_game", redis_res, room=sid) - redis_res = (await redis.get(f"game_session:{data['game_pin']}")) + redis_res = await redis.get(f"game_session:{data['game_pin']}") redis_res = json.loads(redis_res) # print(redis_res) redis_res["players"].append({"username": data["username"], "sid": sid}) - await redis.set(f"game_session:{data['game_pin']}", - json.dumps({"admin": redis_res["admin"], "game_id": redis_res["game_id"], - "players": redis_res["players"], "answers": []}), ex=18000) - await sio.emit("player_joined", {"username": data["username"], "sid": sid}, room=redis_res["admin"]) + await redis.set( + f"game_session:{data['game_pin']}", + json.dumps( + { + "admin": redis_res["admin"], + "game_id": redis_res["game_id"], + "players": redis_res["players"], + "answers": [], + } + ), + ex=18000, + ) + await sio.emit( + "player_joined", + {"username": data["username"], "sid": sid}, + room=redis_res["admin"], + ) sio.enter_room(sid, data["game_pin"]) # TODO: make more secure @@ -55,14 +74,19 @@ async def register_as_admin(sid, data): game_pin = data["game_pin"] game_id = data["game_id"] if (await redis.get(f"game_session:{game_pin}")) is None: - await redis.set(f"game_session:{game_pin}", json.dumps({"admin": sid, "game_id": game_id, "players": []}), - ex=18000) + await redis.set( + f"game_session:{game_pin}", + json.dumps({"admin": sid, "game_id": game_id, "players": []}), + ex=18000, + ) - await sio.emit("registered_as_admin", {"game_id": game_id, "game": await redis.get( - f"game:{data['game_pin']}")}, - room=sid) + await sio.emit( + "registered_as_admin", + {"game_id": game_id, "game": await redis.get(f"game:{data['game_pin']}")}, + room=sid, + ) async with sio.session(sid) as session: - session['game_pin'] = data["game_pin"] + session["game_pin"] = data["game_pin"] session["admin"] = True sio.enter_room(sid, data["game_pin"]) else: @@ -74,7 +98,7 @@ async def get_question_results(sid, data): session = await sio.get_session(sid) if session["admin"]: redis_res = await redis.get(f"game_session:{session['game_pin']}:{data['question_number']}") - game_pin = session['game_pin'] + game_pin = session["game_pin"] await sio.emit("question_results", redis_res, room=game_pin) @@ -82,7 +106,7 @@ async def get_question_results(sid, data): async def set_question_number(sid, data): session = await sio.get_session(sid) if session["admin"]: - game_pin = session['game_pin'] + game_pin = session["game_pin"] await sio.emit("set_question_number", data, room=game_pin) @@ -100,21 +124,40 @@ async def submit_answer(sid, data): break answers = await redis.get(f"game_session:{session['game_pin']}:{data['question_index']}") if answers is None: - await redis.set(f"game_session:{session['game_pin']}:{data['question_index']}", - json.dumps( - [{"username": session["username"], "answer": data["answer"], "right": answer_right}]), - ex=18000) + await redis.set( + f"game_session:{session['game_pin']}:{data['question_index']}", + json.dumps( + [ + { + "username": session["username"], + "answer": data["answer"], + "right": answer_right, + } + ] + ), + ex=18000, + ) else: answers = json.loads(answers) - answers.append({"username": session["username"], "answer": data["answer"], "right": answer_right}) - await redis.set(f"game_session:{session['game_pin']}:{data['question_index']}", json.dumps(answers), ex=18000) + answers.append( + { + "username": session["username"], + "answer": data["answer"], + "right": answer_right, + } + ) + await redis.set( + f"game_session:{session['game_pin']}:{data['question_index']}", + json.dumps(answers), + ex=18000, + ) # await redis.set(f"game_data:{session['game_pin']}", json.dumps(data)) @sio.event async def get_game_data(sid, _data): - game_pin = (await sio.get_session(sid))['game_pin'] + game_pin = (await sio.get_session(sid))["game_pin"] game_data = await redis.get(f"game:{game_pin}") if game_data is not None: await sio.emit("game_data", json.loads(game_data), room=game_pin) diff --git a/classquiz/storage/__init__.py b/classquiz/storage/__init__.py index c1ec5bb..c45ec7f 100644 --- a/classquiz/storage/__init__.py +++ b/classquiz/storage/__init__.py @@ -6,7 +6,13 @@ from .local_storage import LocalStorage class Storage: - def __init__(self, backend: str, deta_key: Optional[str], deta_id: Optional[str], storage_path: Optional[str]): + def __init__( + self, + backend: str, + deta_key: Optional[str], + deta_id: Optional[str], + storage_path: Optional[str], + ): self.backend = backend self.deta_key: str | None = deta_key self.deta_id: str | None = deta_id @@ -15,8 +21,11 @@ class Storage: if deta_key is None or deta_id is None: raise ValueError("deta_key and deta_id must be provided") else: - self.deta_instance = DetaStorage(deta_base_url=self.deta_base_url, deta_key=self.deta_key, - deta_id=self.deta_id) + self.deta_instance = DetaStorage( + deta_base_url=self.deta_base_url, + deta_key=self.deta_key, + deta_id=self.deta_id, + ) elif backend == "local": if storage_path is None: @@ -29,8 +38,7 @@ class Storage: async def download(self, file_name: str) -> BytesIO | None: if self.backend == "deta": - return await self.deta_instance.download( - file_name) + return await self.deta_instance.download(file_name) elif self.backend == "local": return await self.local_instance.get_file(file_name) diff --git a/classquiz/storage/local_storage.py b/classquiz/storage/local_storage.py index 0d7438d..dadaa39 100644 --- a/classquiz/storage/local_storage.py +++ b/classquiz/storage/local_storage.py @@ -11,13 +11,13 @@ class LocalStorage: async def get_file(self, file_name: str) -> io.BytesIO | None: try: - async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode='rb') as f: + async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode="rb") as f: return io.BytesIO(await f.read()) except FileNotFoundError: return None async def write_file(self, file_name: str, data: bytes) -> None: - async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode='wb') as f: + async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode="wb") as f: await f.write(data) async def delete_file(self, file_names: [str]) -> None: diff --git a/classquiz/tests/test_kahoot_get.py b/classquiz/tests/test_kahoot_get.py index 73b48a3..f9b7ae0 100644 --- a/classquiz/tests/test_kahoot_get.py +++ b/classquiz/tests/test_kahoot_get.py @@ -5,60 +5,112 @@ from classquiz.kahoot_importer.get import get @pytest.mark.asyncio async def test_get(): - await get('1f95eb0b-fcf4-4db2-879b-5418ef75116b') + await get("1f95eb0b-fcf4-4db2-879b-5418ef75116b") await get("c63d4d39-767e-4754-8c16-66c3c2d9633c") await get("c5d31de6-4598-4574-8e67-ca9a2aba49f8") await get("3d545fc3-b830-44b6-94ac-c8e4a3c9fb88") - quiz_list = ['c63d4d39-767e-4754-8c16-66c3c2d9633c', 'a73689d2-1108-4524-88e1-10c54282346c', - 'e8e2d869-7c75-4ad6-8872-031bc8e37fa9', '387b4139-9e77-4b47-93b7-dbb6d75c54d1', - 'a1fff657-b99d-4547-93cf-4e9629994844', '0880d792-a77e-4cba-8a8b-359de40ffe76', - 'e2d12f01-a3dd-4d0e-a6c4-29f38046117f', 'e0af9b8e-8840-4a7d-8d51-3d02bb6a07e7', - 'e655c086-558d-4602-99fd-5fc429ffe0ce', '1642f29d-b5cb-4213-82e6-7c1fae401aca', - 'fabdd572-da3e-4621-b912-41b9bc5a18fb', 'ded3cbbf-5af3-4761-a6da-b3b6ca040e1a', - '370b3e33-ed97-4d36-9362-7f76aa9d12b9', 'cfa32a28-1a01-47f1-9bca-c85640bceeb8', - '993f615d-6890-4dd3-ac08-f462de9ac5a3', '7f08a377-e371-425e-95f9-7196401ae7ff', - '19656fed-5891-48e7-ae7f-1358f623bfd0', '5e766d32-930a-4640-82a1-87c58612f579', - 'ab2450e8-ecbb-4aed-bb3c-87e47b2b81f5', 'dd3c9175-addd-48df-8297-7598d4f12006', - 'b8de862c-fc4b-4df6-9fa5-81b91868fc3b', '3b082f4e-9c50-45ea-98c4-3845b14e400c', - '10179f67-a344-4fd0-80a1-d1e1b6ca7953', '8288c1dd-9675-411c-9175-4180004da3c3', - 'af838c22-dcbb-497d-8c7b-f6499835b83d', '152c5fdb-3b0a-4cdf-a0d9-1984de0643ad', - '846fd2bb-6b95-4333-b5d4-ef0a363e73a9', 'dd6b446f-817b-469b-9e6b-e1d34c466a7f', - '2720d636-8918-4c6a-86a9-928c489382f4', 'a6985641-df97-4e56-97fc-e0901e32b464', - '1d2705b4-b79e-4b51-8d61-cfb60fc7d1b9', '677fa485-4231-4479-beb2-fe757053bf41', - '2b6379f8-450e-46c0-a854-5e368a03d5a6', 'f3ba42fe-11e8-47fc-8978-8d67bc0ce0b1', - '51d2e839-a9d7-4f26-b1fd-fb0743c3a236', 'f2212243-4553-456d-87e7-7ddfd6ee7563', - '2d6982d3-7b46-4022-bbb6-cb5451c581ba', '1cccae22-5140-46b5-ba6a-aa17cec14dc3', - 'b01dd2c2-ffcf-4c02-a274-e12296dbe4e1', '2669d193-a81f-4752-a003-dd2433beef92', - '06203ee6-3615-4e26-bb08-4fab249698a4', '933c1426-c759-43a3-9770-c8e3948db998', - '02046384-fb05-4f1d-855a-f0e52a0761e3', 'b5266e9b-7372-4df0-94a9-ec8a6c0aabc6', - 'fb8e3a3d-505a-4157-bd9a-b73b00591f10', '42092675-9171-400f-ab4d-c898ec9f70bd', - '1d6307e2-98c2-42f2-b765-d02a68e0da80', '8f431ed0-31df-4435-bcba-aff46929fe82', - 'd573d76f-d715-4a54-bdde-7ac7b6235fb2', 'feb8985b-84fb-4d41-bc41-70c322f508fa', - 'f9f945cc-c166-4f7d-a95f-f0be154c43cf', '009b130a-1709-41a6-943d-85eee4889792', - 'a3f765f7-2329-47e0-83eb-0837b4b54b43', 'fea9e558-206f-444a-8d7f-a3f0301fda8d', - '9b419098-ccfc-467d-9de0-db18b60d6c28', 'e6c2174e-3a3f-4193-9782-1bf3b5cfae63', - 'd42c962e-1691-4fa4-8926-c33157423bd2', '8d27aa83-a103-4f05-8230-69e857bc3ffd', - '60cd19b2-6858-4704-b07b-5196e8e24325', '31624ea4-1e4f-4f90-ae4f-759a0eb93b61', - 'fcd038f4-6b05-485b-afe3-ce67e05b1695', '1207bbe5-2df5-4c86-8f54-1b2675a86859', - 'eea3b0b0-deae-41de-b010-d6d50855d016', 'ca8b0278-744c-4a3f-a2b8-6ab29d76fa7f', - '68d37bc8-28b9-49fd-9e02-48a3c9195fdf', '8bf13f61-c5cc-4332-aab9-37405d1fdd14', - '8ceef7de-9408-48d1-8bb9-b8ac3ef73596', '7e053a85-b04c-484b-8815-d26ce646c748', - 'dfc09237-85b7-4596-b4e8-0bfbfd1aaeb7', 'd8d291ab-9dfa-45c0-bfc2-0a81167e3c63', - 'd0834157-d464-4632-b620-147e211e0614', '30758f35-fabc-48bc-b410-47450de64c82', - '7232bc80-a9cb-4668-9449-16b9282a2331', '29a194bd-2ecc-44bc-a6a9-6b8a25c3d860', - '52b43400-117a-4e16-81b8-b510fe987750', '52ef0c45-9ba1-4d5e-ba0c-82703196ec6c', - '9f0fa778-e6ae-4f7b-b4fd-2ccb1bd37eb7', '54b57afd-c91c-4d5f-8177-c3fe940bc723', - '23620661-6eb6-41df-9de4-18ff99e6f35f', 'c983fe89-6ac3-4395-882e-4b11aa11a99d', - 'd4d01494-e7d0-4a38-aa93-523a77855371', 'e5a77df6-fd29-48c7-8df6-f625de0179e7', - 'bca45eef-fc64-465f-9106-403814f180a0', '3b5c305d-be5e-4209-9ba2-c2d1620a5a4f', - '14c533a5-c15c-493b-8c7d-7c20a0bc5ea1', '7b3b1e40-c75d-4c2c-843b-1599c10123c2', - '54604482-72aa-4d64-a3bb-9d8dc4a38b41', 'baf6f609-2dce-4978-840b-b74261c5ba6d', - '250c536f-6cd9-4707-8706-b682961be032', 'a9b08140-ae44-409a-9941-22794c3fd097', - 'e516bd18-c46f-46b5-88dc-c7eac5a0f5a9', '6a220133-c71f-44df-8912-16f5b72568cb', - 'b63c2adf-b8f3-4e43-8651-44574475621f', '01d88287-387b-4a34-b6d3-0931aba8eb1a', - 'eb0cef3d-4e2b-45b3-88df-fc547a763d01', 'a79dee0f-8579-4e0a-b77f-0a468a3ca71f', - '62e50edb-03e6-4f95-a6f4-8ff3ef28a32b', '337fffe8-cc68-44b7-9636-2433f86ed5a9', - 'bd68f7b7-f8bb-489e-ac29-f8cbfe1978b3', '93d2a7d6-06a6-41bf-9632-c24e1f1c917a'] + quiz_list = [ + "c63d4d39-767e-4754-8c16-66c3c2d9633c", + "a73689d2-1108-4524-88e1-10c54282346c", + "e8e2d869-7c75-4ad6-8872-031bc8e37fa9", + "387b4139-9e77-4b47-93b7-dbb6d75c54d1", + "a1fff657-b99d-4547-93cf-4e9629994844", + "0880d792-a77e-4cba-8a8b-359de40ffe76", + "e2d12f01-a3dd-4d0e-a6c4-29f38046117f", + "e0af9b8e-8840-4a7d-8d51-3d02bb6a07e7", + "e655c086-558d-4602-99fd-5fc429ffe0ce", + "1642f29d-b5cb-4213-82e6-7c1fae401aca", + "fabdd572-da3e-4621-b912-41b9bc5a18fb", + "ded3cbbf-5af3-4761-a6da-b3b6ca040e1a", + "370b3e33-ed97-4d36-9362-7f76aa9d12b9", + "cfa32a28-1a01-47f1-9bca-c85640bceeb8", + "993f615d-6890-4dd3-ac08-f462de9ac5a3", + "7f08a377-e371-425e-95f9-7196401ae7ff", + "19656fed-5891-48e7-ae7f-1358f623bfd0", + "5e766d32-930a-4640-82a1-87c58612f579", + "ab2450e8-ecbb-4aed-bb3c-87e47b2b81f5", + "dd3c9175-addd-48df-8297-7598d4f12006", + "b8de862c-fc4b-4df6-9fa5-81b91868fc3b", + "3b082f4e-9c50-45ea-98c4-3845b14e400c", + "10179f67-a344-4fd0-80a1-d1e1b6ca7953", + "8288c1dd-9675-411c-9175-4180004da3c3", + "af838c22-dcbb-497d-8c7b-f6499835b83d", + "152c5fdb-3b0a-4cdf-a0d9-1984de0643ad", + "846fd2bb-6b95-4333-b5d4-ef0a363e73a9", + "dd6b446f-817b-469b-9e6b-e1d34c466a7f", + "2720d636-8918-4c6a-86a9-928c489382f4", + "a6985641-df97-4e56-97fc-e0901e32b464", + "1d2705b4-b79e-4b51-8d61-cfb60fc7d1b9", + "677fa485-4231-4479-beb2-fe757053bf41", + "2b6379f8-450e-46c0-a854-5e368a03d5a6", + "f3ba42fe-11e8-47fc-8978-8d67bc0ce0b1", + "51d2e839-a9d7-4f26-b1fd-fb0743c3a236", + "f2212243-4553-456d-87e7-7ddfd6ee7563", + "2d6982d3-7b46-4022-bbb6-cb5451c581ba", + "1cccae22-5140-46b5-ba6a-aa17cec14dc3", + "b01dd2c2-ffcf-4c02-a274-e12296dbe4e1", + "2669d193-a81f-4752-a003-dd2433beef92", + "06203ee6-3615-4e26-bb08-4fab249698a4", + "933c1426-c759-43a3-9770-c8e3948db998", + "02046384-fb05-4f1d-855a-f0e52a0761e3", + "b5266e9b-7372-4df0-94a9-ec8a6c0aabc6", + "fb8e3a3d-505a-4157-bd9a-b73b00591f10", + "42092675-9171-400f-ab4d-c898ec9f70bd", + "1d6307e2-98c2-42f2-b765-d02a68e0da80", + "8f431ed0-31df-4435-bcba-aff46929fe82", + "d573d76f-d715-4a54-bdde-7ac7b6235fb2", + "feb8985b-84fb-4d41-bc41-70c322f508fa", + "f9f945cc-c166-4f7d-a95f-f0be154c43cf", + "009b130a-1709-41a6-943d-85eee4889792", + "a3f765f7-2329-47e0-83eb-0837b4b54b43", + "fea9e558-206f-444a-8d7f-a3f0301fda8d", + "9b419098-ccfc-467d-9de0-db18b60d6c28", + "e6c2174e-3a3f-4193-9782-1bf3b5cfae63", + "d42c962e-1691-4fa4-8926-c33157423bd2", + "8d27aa83-a103-4f05-8230-69e857bc3ffd", + "60cd19b2-6858-4704-b07b-5196e8e24325", + "31624ea4-1e4f-4f90-ae4f-759a0eb93b61", + "fcd038f4-6b05-485b-afe3-ce67e05b1695", + "1207bbe5-2df5-4c86-8f54-1b2675a86859", + "eea3b0b0-deae-41de-b010-d6d50855d016", + "ca8b0278-744c-4a3f-a2b8-6ab29d76fa7f", + "68d37bc8-28b9-49fd-9e02-48a3c9195fdf", + "8bf13f61-c5cc-4332-aab9-37405d1fdd14", + "8ceef7de-9408-48d1-8bb9-b8ac3ef73596", + "7e053a85-b04c-484b-8815-d26ce646c748", + "dfc09237-85b7-4596-b4e8-0bfbfd1aaeb7", + "d8d291ab-9dfa-45c0-bfc2-0a81167e3c63", + "d0834157-d464-4632-b620-147e211e0614", + "30758f35-fabc-48bc-b410-47450de64c82", + "7232bc80-a9cb-4668-9449-16b9282a2331", + "29a194bd-2ecc-44bc-a6a9-6b8a25c3d860", + "52b43400-117a-4e16-81b8-b510fe987750", + "52ef0c45-9ba1-4d5e-ba0c-82703196ec6c", + "9f0fa778-e6ae-4f7b-b4fd-2ccb1bd37eb7", + "54b57afd-c91c-4d5f-8177-c3fe940bc723", + "23620661-6eb6-41df-9de4-18ff99e6f35f", + "c983fe89-6ac3-4395-882e-4b11aa11a99d", + "d4d01494-e7d0-4a38-aa93-523a77855371", + "e5a77df6-fd29-48c7-8df6-f625de0179e7", + "bca45eef-fc64-465f-9106-403814f180a0", + "3b5c305d-be5e-4209-9ba2-c2d1620a5a4f", + "14c533a5-c15c-493b-8c7d-7c20a0bc5ea1", + "7b3b1e40-c75d-4c2c-843b-1599c10123c2", + "54604482-72aa-4d64-a3bb-9d8dc4a38b41", + "baf6f609-2dce-4978-840b-b74261c5ba6d", + "250c536f-6cd9-4707-8706-b682961be032", + "a9b08140-ae44-409a-9941-22794c3fd097", + "e516bd18-c46f-46b5-88dc-c7eac5a0f5a9", + "6a220133-c71f-44df-8912-16f5b72568cb", + "b63c2adf-b8f3-4e43-8651-44574475621f", + "01d88287-387b-4a34-b6d3-0931aba8eb1a", + "eb0cef3d-4e2b-45b3-88df-fc547a763d01", + "a79dee0f-8579-4e0a-b77f-0a468a3ca71f", + "62e50edb-03e6-4f95-a6f4-8ff3ef28a32b", + "337fffe8-cc68-44b7-9636-2433f86ed5a9", + "bd68f7b7-f8bb-489e-ac29-f8cbfe1978b3", + "93d2a7d6-06a6-41bf-9632-c24e1f1c917a", + ] rounds = 0 for i in quiz_list: rounds = rounds + 1 diff --git a/classquiz/tests/test_storage.py b/classquiz/tests/test_storage.py index 3dfb396..adc26b7 100644 --- a/classquiz/tests/test_storage.py +++ b/classquiz/tests/test_storage.py @@ -10,11 +10,14 @@ file_contents = b"hello world" def test_storage_init(): with pytest.raises(NotImplementedError): - Storage(backend="asdsad", deta_key=settings.deta_project_key, - deta_id=settings.deta_project_id, storage_path=settings.storage_path) + Storage( + backend="asdsad", + deta_key=settings.deta_project_key, + deta_id=settings.deta_project_id, + storage_path=settings.storage_path, + ) with pytest.raises(ValueError): - Storage(backend="deta", deta_key=None, - deta_id=None, storage_path=None) + Storage(backend="deta", deta_key=None, deta_id=None, storage_path=None) with pytest.raises(ValueError): Storage(backend="local", storage_path=None, deta_key=None, deta_id=None) @@ -34,8 +37,12 @@ async def storage_tester(storage: Storage): @pytest.mark.asyncio async def test_deta(): - storage: Storage = Storage(backend="deta", deta_key=settings.deta_project_key, - deta_id=settings.deta_project_id, storage_path=settings.storage_path) + storage: Storage = Storage( + backend="deta", + deta_key=settings.deta_project_key, + deta_id=settings.deta_project_id, + storage_path=settings.storage_path, + ) await storage_tester(storage) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c0cf861 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 120 +target-version = ["py310"]