diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..f5e96dbfaec8bd23554e839a582259cf17837f26 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +venv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..5e90a4ce5ff4d157caeea17babc0a985bd3d29f5 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.9-alpine AS builder + +WORKDIR /app/deps + +COPY ./pyproject.toml . +COPY ./poetry.lock . + +RUN apk add gcc musl-dev python3-dev libffi-dev openssl-dev cargo g++ libxslt-dev postgresql-dev build-base + +RUN pip install poetry +RUN poetry export --without-hashes -f requirements.txt --output requirements.txt +RUN pip wheel -r requirements.txt -w /whls + +FROM python:3.9-alpine +RUN apk add libpq + +WORKDIR /deps +COPY --from=builder /whls /deps +RUN pip install *.whl +RUN rm -rf * + +WORKDIR /app +COPY ./ . +#RUN mv ./misc/etc/gunicorn.conf.py . + +EXPOSE 7860 + +CMD ["python app.py"] \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000000000000000000000000000000000000..78e670e8aced18f840f2d55b1d55c789d4764e47 --- /dev/null +++ b/Readme.md @@ -0,0 +1,3 @@ +# Gurukul + +### First steps \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/__pycache__/app.cpython-310.pyc b/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ddfcb958e92b27c13b403290892098b9ee5cc4b6 Binary files /dev/null and b/__pycache__/app.cpython-310.pyc differ diff --git a/__pycache__/manage.cpython-310.pyc b/__pycache__/manage.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85f58809340cffcbe0da5f84ad80efdedb27ad1b Binary files /dev/null and b/__pycache__/manage.cpython-310.pyc differ diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000000000000000000000000000000000000..0759bbf2c7e9ea86f9217f60bb7cad7ca231cd15 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,86 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# timezone to use when rendering the date +# within the migration file as well as the filename. +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat migrations/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql://postadmin:postpass@localhost/siksalaya + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks=black +# black.type=console_scripts +# black.entrypoint=black +# black.options=-l 79 + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S + diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e07d26b2b6614da96785c14c7c302f93a5697437 --- /dev/null +++ b/api/__init__.py @@ -0,0 +1 @@ +from .api import api_router as router \ No newline at end of file diff --git a/api/__pycache__/__init__.cpython-310.pyc b/api/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a26caab01dc4f069f656c348052adb1f07bc91d7 Binary files /dev/null and b/api/__pycache__/__init__.cpython-310.pyc differ diff --git a/api/__pycache__/api.cpython-310.pyc b/api/__pycache__/api.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d3dd3036710fdd28356f99365ee7704dc770ebb Binary files /dev/null and b/api/__pycache__/api.cpython-310.pyc differ diff --git a/api/api.py b/api/api.py new file mode 100644 index 0000000000000000000000000000000000000000..a1a19d96844e3da9966fc3dd7912094900dbe850 --- /dev/null +++ b/api/api.py @@ -0,0 +1,48 @@ +from fastapi import APIRouter + +from api.endpoints import program, quiz_answer, teacher_note, users, group, quiz +from api.endpoints import ( + program, + users, + auth, + two_fa, + utils, + course, + school, + department, + class_session, + personal_note, + teacher_note, + assignment, + assignment_upload, +) + +api_router = APIRouter() +api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"]) +api_router.include_router( + two_fa.router, prefix="/2fa", tags=["Two Factor Authentication"] +) +api_router.include_router(users.router, prefix="/users", tags=["Users"]) +api_router.include_router(utils.router, prefix="/utils", tags=["Utils"]) +api_router.include_router(school.router, prefix="/school", tags=["Schools"]) +api_router.include_router(course.router, prefix="/course", tags=["Courses"]) +api_router.include_router(department.router, prefix="/department", tags=["Departments"]) +api_router.include_router( + class_session.router, prefix="/class_session", tags=["Class Sessions"] +) +api_router.include_router( + personal_note.router, prefix="/personal_note", tags=["Personal Notes"] +) +api_router.include_router(program.router, prefix="/program", tags=["Programs"]) +api_router.include_router( + teacher_note.router, prefix="/teacher_note", tags=["Teacher Notes"] +) +api_router.include_router(group.router, prefix="/group", tags=["Groups"]) +api_router.include_router(quiz.router, prefix="/quiz", tags=["Quizzes"]) +api_router.include_router( + quiz_answer.router, prefix="/quizanswer", tags=["Quiz Answers"] +) +api_router.include_router(assignment.router, prefix="/assignment", tags=["Assignments"]) +api_router.include_router( + assignment_upload.router, prefix="/assignmentupload", tags=["Assignment Uploads"] +) diff --git a/api/endpoints/__init__.py b/api/endpoints/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/endpoints/__pycache__/__init__.cpython-310.pyc b/api/endpoints/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a30aedb2ead614295fa03d6ced7afdc3d982cf6c Binary files /dev/null and b/api/endpoints/__pycache__/__init__.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/assignment.cpython-310.pyc b/api/endpoints/__pycache__/assignment.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73d788da1ad75e6f38c1184da11ea351158d737e Binary files /dev/null and b/api/endpoints/__pycache__/assignment.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/assignment_upload.cpython-310.pyc b/api/endpoints/__pycache__/assignment_upload.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3e9fd7029fe968d745b1ff845b0151ebd4e511e8 Binary files /dev/null and b/api/endpoints/__pycache__/assignment_upload.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/auth.cpython-310.pyc b/api/endpoints/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02605e625af37412e0121a44ac5c774d5d36a3cf Binary files /dev/null and b/api/endpoints/__pycache__/auth.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/class_session.cpython-310.pyc b/api/endpoints/__pycache__/class_session.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f11a53223f58e543c42e9d4f8487f7e2b5d5144b Binary files /dev/null and b/api/endpoints/__pycache__/class_session.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/course.cpython-310.pyc b/api/endpoints/__pycache__/course.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9e9d176ac5417729cbb921354ff4aceae6f3984d Binary files /dev/null and b/api/endpoints/__pycache__/course.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/department.cpython-310.pyc b/api/endpoints/__pycache__/department.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64bb985f70cf2d3651f4742d404e3550fbd789ee Binary files /dev/null and b/api/endpoints/__pycache__/department.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/group.cpython-310.pyc b/api/endpoints/__pycache__/group.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc76f328be28e673d7a1c92b9d3aa30c799ea7f6 Binary files /dev/null and b/api/endpoints/__pycache__/group.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/personal_note.cpython-310.pyc b/api/endpoints/__pycache__/personal_note.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5264673ba0f14ff21f999a7d7ce48accb2ece32a Binary files /dev/null and b/api/endpoints/__pycache__/personal_note.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/program.cpython-310.pyc b/api/endpoints/__pycache__/program.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a1b241e565312b8a8f8d830ac3167a8773e5b97 Binary files /dev/null and b/api/endpoints/__pycache__/program.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/quiz.cpython-310.pyc b/api/endpoints/__pycache__/quiz.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b96a4c55c9d194d095f34af7f72291439c975b0e Binary files /dev/null and b/api/endpoints/__pycache__/quiz.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/quiz_answer.cpython-310.pyc b/api/endpoints/__pycache__/quiz_answer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6735ffb48d12b113c2bac2627e54374f78cd2023 Binary files /dev/null and b/api/endpoints/__pycache__/quiz_answer.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/school.cpython-310.pyc b/api/endpoints/__pycache__/school.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b107914ebee316976021c7142708c8bb973e07aa Binary files /dev/null and b/api/endpoints/__pycache__/school.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/teacher_note.cpython-310.pyc b/api/endpoints/__pycache__/teacher_note.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f869e78aa9eec2f2a2303fab7ce12258a57ba9f8 Binary files /dev/null and b/api/endpoints/__pycache__/teacher_note.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/two_fa.cpython-310.pyc b/api/endpoints/__pycache__/two_fa.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9dacef10371eade34db00274aeb6ba4f35d3561c Binary files /dev/null and b/api/endpoints/__pycache__/two_fa.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/users.cpython-310.pyc b/api/endpoints/__pycache__/users.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec3c27a3d6fac52f667b6cbceda0e360511aec66 Binary files /dev/null and b/api/endpoints/__pycache__/users.cpython-310.pyc differ diff --git a/api/endpoints/__pycache__/utils.cpython-310.pyc b/api/endpoints/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72190301766c3558b2edc3c2500765d929d03233 Binary files /dev/null and b/api/endpoints/__pycache__/utils.cpython-310.pyc differ diff --git a/api/endpoints/assignment.py b/api/endpoints/assignment.py new file mode 100644 index 0000000000000000000000000000000000000000..07c7b9e34d37346c78ec4ea40030299b25b97c8a --- /dev/null +++ b/api/endpoints/assignment.py @@ -0,0 +1,188 @@ +from locale import currency +from typing import Any, List + +from hashlib import sha1 +import os +import shutil + +from fastapi import APIRouter, Depends, UploadFile, File +from sqlalchemy.orm import Session +from core.config import settings + +from models import User + +import aiofiles + +from utils import deps +from cruds import crud_assignment, crud_group, crud_assignment_upload +from schemas import Assignment, AssignmentUpdate, AssignmentCreate + +router = APIRouter() + +ASSIGNMENT_ROUTE: str = "assignments" + + +@router.get("/", response_model=List[Assignment]) +async def get_assignment( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = -1, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + + if current_user.user_type == settings.UserType.STUDENT.value: + group = crud_group.get(db, id=current_user.group_id) + assignment = crud_assignment.get_quiz_by_group_id(db=db, group=group) + index = 0 + for assig in assignment: + assignmentUpload = crud_assignment_upload.get_by_assignment_id( + db=db, + assignmentId=assig.id, + studentId=current_user.id, + ) + + if assignmentUpload: + assignment[index].exists = True + else: + assignment[index].exists = False + index += 1 + + return assignment + + if current_user.user_type == settings.UserType.TEACHER.value: + return crud_assignment.get_quiz_by_instructor_id(db=db, user=current_user) + + if current_user.user_type <= settings.UserType.ADMIN.value: + return crud_assignment.get_multi(db, skip=skip, limit=limit) + + +@router.post("/", response_model=Assignment) +async def create_assignment( + db: Session = Depends(deps.get_db), + *, + obj_in: AssignmentCreate, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + + if obj_in.instructor: + if current_user.id not in obj_in.instructor: + obj_in.instructor.append(current_user.id) + else: + obj_in.instructor = [current_user.id] + + assignment = crud_assignment.create(db, obj_in=obj_in) + return assignment + + +@router.post("/{id}/files/") +async def post_files( + db: Session = Depends(deps.get_db), + files: List[UploadFile] = File(...), + current_user=Depends(deps.get_current_active_teacher_or_above), + *, + id: int, +): + + assignment = crud_assignment.get(db=db, id=id) + + hashedAssignmentId = sha1(str(id).encode(encoding="UTF-8", errors="strict")) + + FILE_ASSIGNMENT_PATH = os.path.join( + ASSIGNMENT_ROUTE, + hashedAssignmentId.hexdigest(), + ) + + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, + FILE_ASSIGNMENT_PATH, + ) + + if not os.path.exists(FILE_PATH): + os.makedirs(FILE_PATH) + + if assignment.files: + assignmentFiles = assignment.files.copy() + fileIndex = len(assignment.files) + else: + assignmentFiles = [] + fileIndex = 0 + + for file in files: + fileName, fileExtension = os.path.splitext(file.filename) + hashedFileName = sha1( + (fileName + str(fileIndex)).encode(encoding="UTF-8", errors="strict") + ) + fileIndex = fileIndex + 1 + filename = f"{FILE_PATH}/{hashedFileName.hexdigest()}{fileExtension}" + async with aiofiles.open(filename, mode="wb") as f: + content = await file.read() + await f.write(content) + assignmentFiles.append( + { + "path": f"{FILE_ASSIGNMENT_PATH}/{hashedFileName.hexdigest()}{fileExtension}", + "name": file.filename, + } + ) + + obj_in = AssignmentUpdate(files=assignmentFiles) + updated = crud_assignment.update(db=db, db_obj=assignment, obj_in=obj_in) + + return updated + + +@router.get("/{id}/", response_model=Assignment) +async def get_specific_assignment( + db: Session = Depends(deps.get_db), + current_user: User = Depends(deps.get_current_active_user), + *, + id: int, +) -> Any: + + assignments = await get_assignment(db=db, current_user=current_user) + + if assignments: + for assignment in assignments: + if assignment.id == id: + return assignment + + +@router.delete("/{id}/") +async def delete_assignment( + db: Session = Depends(deps.get_db), + current_user: User = Depends(deps.get_current_active_teacher_or_above), + *, + id: int, +) -> Any: + assignment = await get_specific_assignment(db=db, current_user=current_user, id=id) + + if not assignment: + return {"msg": "assignment not found"} + + deleted = crud_assignment.remove(db=db, id=assignment.id) + if deleted: + hashedAssignmentId = sha1( + str(assignment.id).encode(encoding="UTF-8", errors="strict") + ) + FILE_ASSIGNMENT_PATH = os.path.join( + ASSIGNMENT_ROUTE, + hashedAssignmentId.hexdigest(), + ) + + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, + FILE_ASSIGNMENT_PATH, + ) + + if os.path.exists(FILE_PATH): + shutil.rmtree(FILE_PATH) + + return {"msg": "delete success"} + + +@router.put("/{id}", response_model=Assignment) +async def update_assignment( + db: Session = Depends(deps.get_db), *, id: int, obj_in: AssignmentUpdate +) -> Any: + assignment = crud_assignment.get(db, id) + assignment = crud_assignment.update(db, db_obj=assignment, obj_in=obj_in) + return assignment diff --git a/api/endpoints/assignment_upload.py b/api/endpoints/assignment_upload.py new file mode 100644 index 0000000000000000000000000000000000000000..8e84ce2336781cf73ee888936f4bbf9c75ce706d --- /dev/null +++ b/api/endpoints/assignment_upload.py @@ -0,0 +1,235 @@ +import math +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File +from sqlalchemy.orm import Session +from models import User +from utils import deps +from datetime import datetime, timedelta +from cruds import crud_assignment_upload, crud_assignment +from schemas import ( + AssignmentUpload, + AssignmentUploadCreate, + AssignmentUploadUpdate, + AssignmentUploadwithName, +) +import os +from fastapi.responses import FileResponse +from hashlib import sha1 + +import aiofiles + +import shutil + +from typing import Any, Optional, List, Dict # noqa +from core.config import settings + + +router = APIRouter() + +assignment_upload_ROUTE: str = "assignmentUpload" + + +@router.get("/") +async def get_assignments( + db: Session = Depends(deps.get_db), + *, + current_user: User = Depends(deps.get_current_active_user), +): + pass + + +@router.get("/{assignmentid}", response_model=AssignmentUpload) +async def get_assignment_upload( + db: Session = Depends(deps.get_db), + *, + assignmentid: int, + current_user: User = Depends(deps.get_current_active_user), +): + + assignmentUpload = crud_assignment_upload.get_by_assignment_id( + db=db, assignmentId=assignmentid, studentId=current_user.id + ) + + if assignmentUpload: + return assignmentUpload + + raise HTTPException(status_code=404, detail="Error ID: 147") + + +@router.get( + "/{assignmentid}/getUploadsAsTeacher", response_model=List[AssignmentUploadwithName] +) +async def get_assignment_upload_as_teacher( + db: Session = Depends(deps.get_db), + *, + assignmentid: int, + current_user: User = Depends(deps.get_current_active_teacher_or_above), +): + if current_user.assignments: + for assignment in current_user.assignments: + if assignment.id == assignmentid: + assignmentUpload = ( + crud_assignment_upload.get_all_by_assignment_id_as_teacher( + db=db, assignmentId=assignmentid + ) + ) + if assignmentUpload: + return assignmentUpload + + raise HTTPException( + status_code=404, + detail="Error ID: 148", # could not populate answer + ) + + +@router.get("/{assignmentid}/exists") +async def check_existence( + db: Session = Depends(deps.get_db), + current_user: User = Depends(deps.get_current_active_user), + *, + assignmentid: int, +): + assignmentUpload = crud_assignment_upload.get_by_assignment_id( + db=db, assignmentId=assignmentid, studentId=current_user.id + ) + + if not assignmentUpload: + return {"exists": False} + else: + return {"exists": True} + + +@router.post("/{assignmentid}/upload") +async def post_files( + db: Session = Depends(deps.get_db), + files: List[UploadFile] = File(...), + current_user=Depends(deps.get_current_active_user), + *, + assignmentid: int, +): + + hashedAssignmentId = sha1( + str(assignmentid).encode(encoding="UTF-8", errors="strict") + ) + hashedUserId = sha1(str(current_user.id).encode(encoding="UTF-8", errors="strict")) + + FILE_ASSIGNMENT_PATH = os.path.join( + assignment_upload_ROUTE, + hashedUserId.hexdigest(), + hashedAssignmentId.hexdigest(), + ) + + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, + FILE_ASSIGNMENT_PATH, + ) + + if os.path.exists(FILE_PATH): + shutil.rmtree(FILE_PATH) + + if not os.path.exists(FILE_PATH): + os.makedirs(FILE_PATH) + + fileIndex = 0 + assignmentFiles = [] + + for file in files: + fileName, fileExtension = os.path.splitext(file.filename) + hashedFileName = sha1( + (fileName + str(fileIndex)).encode(encoding="UTF-8", errors="strict") + ) + fileIndex = fileIndex + 1 + filename = f"{FILE_PATH}/{hashedFileName.hexdigest()}{fileExtension}" + async with aiofiles.open(filename, mode="wb") as f: + content = await file.read() + await f.write(content) + assignmentFiles.append( + { + "path": f"{FILE_ASSIGNMENT_PATH}/{hashedFileName.hexdigest()}{fileExtension}", + "name": file.filename, + } + ) + + assignmentUpload = crud_assignment_upload.get_by_assignment_id( + db=db, assignmentId=assignmentid, studentId=current_user.id + ) + + if assignmentUpload: + db_obj = assignmentUpload + obj_in = AssignmentUploadUpdate( + files=assignmentFiles, + submission_date=datetime.utcnow(), + marks_obtained=None, + ) + assignmentUploadX = crud_assignment_upload.update( + db=db, db_obj=db_obj, obj_in=obj_in + ) + else: + obj_in = AssignmentUploadCreate( + files=assignmentFiles, + assignment_id=assignmentid, + student_id=current_user.id, + submission_date=datetime.utcnow(), + marks_obtained=None, + ) + assignmentUploadX = crud_assignment_upload.create(db=db, obj_in=obj_in) + + return assignmentUploadX + + +@router.delete("/{assignmentid}/files") +async def post_files( + db: Session = Depends(deps.get_db), + current_user=Depends(deps.get_current_active_user), + *, + assignmentid: int, +): + + hashedAssignmentId = sha1( + str(assignmentid).encode(encoding="UTF-8", errors="strict") + ) + hashedUserId = sha1(str(current_user.id).encode(encoding="UTF-8", errors="strict")) + + FILE_ASSIGNMENT_PATH = os.path.join( + assignment_upload_ROUTE, + hashedUserId.hexdigest(), + hashedAssignmentId.hexdigest(), + ) + + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, + FILE_ASSIGNMENT_PATH, + ) + + if os.path.exists(FILE_PATH): + shutil.rmtree(FILE_PATH) + + assignmentUpload = crud_assignment_upload.get_by_assignment_id( + db=db, assignmentId=assignmentid, studentId=current_user.id + ) + + if assignmentUpload: + crud_assignment_upload.remove(db=db, id=assignmentUpload.id) + return {"message": "Success"} + + raise HTTPException(status_code=404, detail="Error ID: 149") + + +@router.post("/{assignmentuploadid}/mark") +async def post_files( + db: Session = Depends(deps.get_db), + current_user=Depends(deps.get_current_active_teacher_or_above), + *, + assignmentuploadid: int, + marks_obtained: int, +): + + assignmentUpload = crud_assignment_upload.get(db=db, id=assignmentuploadid) + + if assignmentUpload: + obj_in = AssignmentUploadUpdate(marks_obtained=marks_obtained) + db_obj = assignmentUpload + updated = crud_assignment_upload.update(db=db, db_obj=db_obj, obj_in=obj_in) + + return updated + + raise HTTPException(status_code=404, detail="Error ID: 150") diff --git a/api/endpoints/auth.py b/api/endpoints/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..cb3e0fa5fedfc3097d16b8a22eaa389c9b8bc1c8 --- /dev/null +++ b/api/endpoints/auth.py @@ -0,0 +1,332 @@ +import json +import os +from typing import Any, List, Optional + +import aiofiles +from fastapi import APIRouter, Body +from fastapi import Cookie as ReqCookie +from fastapi import Depends, File, HTTPException, Request, UploadFile, Form +from fastapi.params import Cookie +from sqlalchemy.orm import Session +from sqlalchemy.sql.expression import update +from sqlalchemy.sql.functions import current_user +from starlette.responses import JSONResponse, Response +from starlette.status import HTTP_401_UNAUTHORIZED + +import cruds +import models +import schemas +from core import throttle +from core.config import settings +from core.db import redis_session_client +from core.security import ( + create_sesssion_token, + get_password_hash, + create_2fa_temp_token, + create_passwordless_create_token, + authorize_passwordless_token, + verify_passwordless_token, +) +from cruds import group +from schemas.user import UserUpdate, VerifyUser +from utils import deps +from utils.utils import ( + expire_web_session, + generate_password_reset_token, + send_reset_password_email, + send_verification_email, + verify_password_reset_token, + verify_user_verify_token, +) + +router = APIRouter() + + +@router.post( + "/web/", + response_model=Optional[schemas.user.UserLoginReturn], + response_model_exclude_none=True, +) +async def login_web_session( + db: Session = Depends(deps.get_db), + *, + form_data: schemas.LoginData, + request: Request, + response: Response, +) -> Any: + if not form_data.username: + form_data.username = form_data.email + + user = cruds.crud_user.authenticate( + db, email=form_data.username, password=form_data.password + ) + + if not user: + raise HTTPException( + status_code=401, detail="Error ID: 111" + ) # Incorrect email or password + elif not user.is_active: + raise HTTPException( + status_code=401, detail="Error ID: 112") # Inactive user + + if user.two_fa_secret: + temp_token = await create_2fa_temp_token(user, form_data.remember_me) + response.set_cookie("temp_session", temp_token, httponly=True) + return { + "msg": "2FA required before proceeding!", + "two_fa_required": True, + "user": None, + } + else: + session_token = await create_sesssion_token( + user, form_data.remember_me, request + ) + response.set_cookie("session", session_token, httponly=True) + return { + "msg": "Logged in successfully!", + "user": user, + "two_fa_required": False, + } + + +@router.get("/password-less/create") +async def generate_passwordless_login_token( + db: Session = Depends(deps.get_db), +): + token = await create_passwordless_create_token() + + return {"token": token} + + +@router.post("/password-less/authorize") +async def authorize_passwordless_login( + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_active_user), + token: str = Form(None), +): + _ = await authorize_passwordless_token(current_user, token) + + return {"msg": "Success"} + + +@router.post( + "/password-less/verify", + response_model=Optional[schemas.user.UserLoginReturn], + response_model_exclude_none=True, +) +async def verify_passwordless_login( + response: Response, + request: Request, + db: Session = Depends(deps.get_db), + token: str = Form(None), +): + user_id = await verify_passwordless_token(token) + + user = cruds.crud_user.get_by_id(db, id=user_id) + + if not user: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail="Invalid user!") + + session_token = await create_sesssion_token(user, True, request) + response.set_cookie("session", session_token, httponly=True) + return {"msg": "Logged in successfully!", "user": user, "two_fa_required": False} + + +@router.post("/signup/", response_model=schemas.Msg) +async def sign_up( + *, + db: Session = Depends(deps.get_db), + user_in: schemas.UserSignUp, +) -> Any: + if not settings.USERS_OPEN_REGISTRATION: + raise HTTPException( + status_code=403, + detail="Error ID: 129", + ) # Open user registration is forbidden on this server + user = cruds.crud_user.get_by_email(db, email=user_in.email) + if user: + raise HTTPException( + status_code=400, + detail="Email is associated with another user!", + ) # The user with this username already exists in the system + email_host = user_in.email[user_in.email.index("@") + 1:] + + if email_host not in settings.ALLOWED_EMAIL_HOST: + raise HTTPException( + status_code=403, + # TODO: Reflected XSS test + detail=f"Email of host {email_host} not allowed!", + ) + + user = cruds.crud_user.create( + db, obj_in=schemas.UserCreate(**user_in.dict(), profile_pic="") + ) + await send_verification_email(user=user) + return schemas.Msg(msg="Success") + + +@router.post("/resend-verification-email/") +async def resend_verification_email( + email: str, + current_user: models.User = Depends(deps.get_current_admin_or_above), + db: Session = Depends(deps.get_db), +): + user = cruds.crud_user.get_by_email(db=db, email=email) + if not user: + raise HTTPException(status_code="404", detail="User doesn't exist") + + await send_verification_email(user) + return schemas.Msg(msg="Success") + + +@router.post("/change-password/") +async def change_password( + current_password: str = Body(...), + new_password: str = Body(...), + current_user: models.User = Depends(deps.get_current_user), + db: Session = Depends(deps.get_db), +) -> Any: + user = cruds.crud_user.authenticate( + db, email=current_user.email, password=current_password + ) + + if not user: + raise HTTPException( + status_code=403, detail="Error ID: 111" + ) # Incorrect email or password + + data = schemas.user.PasswordUpdate( + password=new_password, + ) + + cruds.crud_user.update(db=db, db_obj=current_user, obj_in=data) + + +@router.get("/active-sessions/", response_model=List[schemas.auth.ActiveSession]) +async def get_active_sessions( + current_user: models.User = Depends(deps.get_current_user), +) -> Any: + active_sessions = json.loads( + await redis_session_client.client.get( + f"user_{current_user.id}_sessions", encoding="utf-8" + ) + ) + return active_sessions.get("sessions") + + +@router.get("/logout-all-sessions/") +async def logout_all_sessions( + current_user: models.User = Depends(deps.get_current_user), +) -> Any: + active_sessions = json.loads( + await redis_session_client.client.get( + f"user_{current_user.id}_sessions", encoding="utf-8" + ) + ) + + for session in active_sessions.get("sessions"): + await redis_session_client.client.expire(session.get("token"), 0) + + await redis_session_client.client.expire(f"user_{current_user.id}_sessions", 0) + + resp = JSONResponse({"status": "success"}) + resp.delete_cookie("session") + return resp + + +@router.post("/password-recovery/", response_model=schemas.Msg) +# @throttle.ip_throttle(rate=3, per=1 * 60 * 60) +# @throttle.ip_throttle(rate=1, per=20) +async def recover_password( + request: Request, + email: str, + db: Session = Depends(deps.get_db), +) -> Any: + """ + Password Recovery + """ + user = cruds.crud_user.get_by_email(db, email=email) + + if not user: + raise HTTPException( + status_code=404, + detail="Error ID: 113", + ) # The user with this username does not exist in the system. + await send_reset_password_email(user=user) + return {"msg": "Password recovery email sent"} + + +@router.post("/reset-password/", response_model=schemas.Msg) +async def reset_password( + request: Request, + token: str = Body(...), + new_password: str = Body(...), + db: Session = Depends(deps.get_db), +) -> Any: + """ + Reset password + """ + uid = await verify_password_reset_token(token) + user = cruds.crud_user.get_by_id(db, id=uid) + if not user: + raise HTTPException( + status_code=404, + detail="Error ID: 114", + ) # The user with this username does not exist in the system. + elif not cruds.crud_user.is_active(user): + raise HTTPException( + status_code=400, detail="Error ID: 115") # Inactive user + hashed_password = get_password_hash(new_password) + user.hashed_password = hashed_password + db.add(user) + db.commit() + return {"msg": "Password updated successfully"} + + +@router.post("/verify/", response_model=schemas.Msg) +async def verify_account( + token: str, + db: Session = Depends(deps.get_db), +) -> Any: + uid = await verify_user_verify_token(token) + user = cruds.crud_user.get_by_id(db, id=uid) + if not user: + raise HTTPException( + status_code=404, + detail="Error ID: 146", + ) # The user with this username does not exist in the system. + cruds.crud_user.verify_user(db=db, db_obj=user) + return {"msg": "Verified successfully"} + + +@router.get("/logout/", response_model=schemas.Token) +async def session_logout( + session: str = ReqCookie(None), +) -> Any: + if not session: + raise HTTPException(status_code=401, detail="Invalid session token!") + await expire_web_session(session) + resp = JSONResponse({"status": "success"}) + resp.delete_cookie("session") + return resp + + +@router.get("/thtest1") +@throttle.ip_throttle(rate=10, per=60) +async def throttle_test( + request: Request, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +): + return "Throttle test endpoint 1 Hello" + + +@router.get("/thtest2") +@throttle.user_throttle(rate=20, per=60) +async def throttle_test1( + request: Request, + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_user), +): + return "Throttle test endpoint 2" diff --git a/api/endpoints/class_session.py b/api/endpoints/class_session.py new file mode 100644 index 0000000000000000000000000000000000000000..2466ee327a91ca81c7731a0eef960baa85a01810 --- /dev/null +++ b/api/endpoints/class_session.py @@ -0,0 +1,300 @@ +import json +import os +from hashlib import sha256 +from typing import Any, List + +import aiofiles +from fastapi import ( + APIRouter, + Depends, + FastAPI, + File, + Form, + HTTPException, + UploadFile, + WebSocket, + WebSocketDisconnect, +) +from fastapi.encoders import jsonable_encoder +from fastapi.responses import FileResponse, HTMLResponse +from sqlalchemy.orm import Session +from starlette.status import HTTP_406_NOT_ACCEPTABLE + +from core.config import settings +from core.security import get_uid_hash +from core.websocket import ws, ChatMessageTypes +from cruds import crud_class_session, crud_file, crud_user +from forms.class_session import ClassSessionCreateForm +from models import ClassSession as ClassSessionModel +from models import File as FileModel +from schemas.class_session import ( + ClassSession, + ClassSessionCreate, + ClassSessionReturn, + ClassSessionUpdate, + ClassSessionTeacherReturn, + AttendanceUpdate, + ParticipantOfClassSession, +) +from schemas.file import FileCreate +from schemas.group import GroupStudentReturn +from utils import deps +from utils.deps import get_current_active_teacher_or_above, get_current_active_user, get_current_active_ws_user +import datetime +import cruds + +router = APIRouter() + + +@router.get("/", response_model=List[ClassSessionReturn]) +def get_class_session( + db: Session = Depends(deps.get_db), + user=Depends(get_current_active_user), + skip: int = 0, + limit: int = 100, +) -> Any: + class_sessions = crud_class_session.get_user_class_session(db, user=user) + return class_sessions + + +@router.get("/active", response_model=List[ClassSessionReturn]) +def get_active_class_session( + db: Session = Depends(deps.get_db), + user=Depends(get_current_active_user), +) -> Any: + class_sessions = crud_class_session.get_user_class_session(db, user=user) + active_class_sessions = [] + + for class_session in class_sessions: + if(class_session.start_time < datetime.datetime.now() and class_session.end_time > datetime.datetime.now()): + active_class_sessions.append(class_session) + + return active_class_sessions + + +@router.post("/", response_model=ClassSession) +async def create_class_session( + db: Session = Depends(deps.get_db), + user=Depends(get_current_active_teacher_or_above), + *, + form: ClassSessionCreateForm = Depends(), +) -> Any: + course_id = None + for item in user.teacher_group: + course_id = item.course.id if item.group.id == form.group else course_id + + if(course_id == None): + raise HTTPException( + status_code=HTTP_406_NOT_ACCEPTABLE, detail="Invalid group id!") + + data = ClassSessionCreate( + start_time=form.start_time, + end_time=form.end_time, + instructor=[user.id]+(form.instructor or []), + description=form.description, + group_id=form.group, + course_id=course_id, + ) + + class_session = crud_class_session.create(db, obj_in=data) + + hasher = sha256() + hasher.update(bytes(f"{class_session.id}_{settings.SECRET_KEY}", "utf-8")) + db_folder_path = os.path.join("class_files", hasher.hexdigest()) + folder_path = os.path.join(settings.UPLOAD_DIR_ROOT, db_folder_path) + + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + if form.file: + for file in form.file: + file_path = os.path.join(folder_path, file.filename) + async with aiofiles.open(file_path, mode="wb") as f: + content = await file.read() + await f.write(content) + + db.add( + FileModel( + name=file.filename, + path=db_folder_path, + file_type=file.content_type, + description=None, + class_session=class_session, + ) + ) + db.commit() + + return class_session + + +@router.get("/{id}/", response_model=ClassSessionReturn) +def get_specific_class_session( + db: Session = Depends(deps.get_db), + user=Depends(get_current_active_user), + *, + id: int, +) -> Any: + class_session = crud_class_session.get_user_class_session( + db=db, user=user, id=id) + return class_session + + +@router.get("/{id}/participants", response_model=List[ParticipantOfClassSession]) +def get_specific_class_session( + db: Session = Depends(deps.get_db), + current_user=Depends(get_current_active_user), + *, + id: int, +) -> Any: + class_session = crud_class_session.get(db, id) + + group = cruds.crud_group.get(db, class_session.group_id) + + participants = group.student + class_session.instructor + + return participants + + +@router.get("/{id}/attendance", response_model=ClassSessionTeacherReturn) +def get_class_session_with_attendance( + db: Session = Depends(deps.get_db), + user=Depends(get_current_active_teacher_or_above), + *, + id: int, +) -> Any: + class_session = crud_class_session.get_user_class_session( + db=db, user=user, id=id) + return class_session + + +@router.put("/{id}/", response_model=ClassSession) +def update_class_session( + db: Session = Depends(deps.get_db), *, id: int, obj_in: ClassSessionUpdate +) -> Any: + class_session = crud_class_session.get(db, id) + class_session = crud_class_session.update( + db, db_obj=class_session, obj_in=obj_in) + return class_session + + +@router.put("/{class_id}/files") +async def update_class_session( + db: Session = Depends(deps.get_db), + *, + class_id: int, + files: List[UploadFile] = File(None), +) -> Any: + class_session = crud_class_session.get(db, class_id) + + hasher = sha256() + hasher.update(bytes(f"{class_id}_{settings.SECRET_KEY}", "utf-8")) + db_folder_path = os.path.join("class_files", hasher.hexdigest()) + folder_path = os.path.join(settings.UPLOAD_DIR_ROOT, db_folder_path) + + if not os.path.exists(folder_path): + os.makedirs(folder_path) + + for file in files: + file_path = os.path.join(folder_path, file.filename) + async with aiofiles.open(file_path, mode="wb") as f: + content = await file.read() + await f.write(content) + + db.add( + FileModel( + name=file.filename, + path=db_folder_path, + file_type=file.content_type, + description=None, + class_session=class_session, + ) + ) + db.commit() + return {"msg": "success"} + + +@router.put("/{id}/attendance") +def attendance_of_class_session( + db: Session = Depends(deps.get_db), + *, + id: int, + obj_in: AttendanceUpdate, + current_teacher=Depends(get_current_active_teacher_or_above), +) -> Any: + class_session = crud_class_session.get_user_class_session( + db=db, user=current_teacher, id=id + ) + if not class_session: + raise HTTPException( + status_code=403, detail="Class session access denied!") + class_session = crud_class_session.attendance_update( + db, db_obj=class_session, obj_in=obj_in + ) + return {"msg": "success"} + + +# @router.post("/{id}/file/") +# async def create_upload_files( +# db: Session = Depends(deps.get_db), +# files: List[UploadFile] = File(...), +# current_teacher=Depends( +# get_current_active_teacher_or_above +# ), # FIXME : Get current user ? +# *, +# id: int, +# ): +# class_session = crud_class_session.get_user_class_session( +# db=db, user=current_teacher, id=id +# ) + +# if not class_session: +# raise HTTPException(status_code=403, detail="Error ID: 100") # Access denied! + +# FILE_PATH = os.path.join("static", settings.UPLOAD_DIR_ROOT) +# working_directory = os.getcwd() +# FILE_PATH = os.path.join(working_directory, FILE_PATH) + +# for file in files: +# filename = f"{FILE_PATH}/{id}/{file.filename}" +# async with aiofiles.open(filename, mode="wb") as f: +# content = await file.read() +# await f.write(content) + +# obj_in = ClassSessionUpdate(file=[file.filename for file in files]) +# crud_class_session.update(db=db, db_obj=class_session, obj_in=obj_in) + +# return {"msg": "success"} + + +@router.websocket("/ws/{id}/") +async def websocket_endpoint( + db: Session = Depends(deps.get_db), + *, + websocket: WebSocket, + req_user=Depends(get_current_active_ws_user), + id: int, +): + user_id = req_user.id + class_session = crud_class_session.get_user_class_session( + db=db, user=req_user, id=id + ) + if not class_session: + raise HTTPException( + status_code=403, detail="Error Code: 144" + ) # User doesn't have access to classsession + await ws.connect(websocket=websocket, class_session_id=id, user_id=user_id) + try: + while True: + data = await websocket.receive_json() + if data.get("msg_type") == ChatMessageTypes.MESSAGE_HISTORY.value: + await ws.send_history(websocket, id) + else: + await ws.message( + websocket=websocket, + user_id=user_id, + class_session_id=id, + message=data.get("message"), + anon=data.get("anon"), + ) + except WebSocketDisconnect: + await ws.disconnect(websocket, class_session_id=id, user_id=user_id) diff --git a/api/endpoints/course.py b/api/endpoints/course.py new file mode 100644 index 0000000000000000000000000000000000000000..fbe24e552907b9c7da77aecfac19d8f66a9d80ff --- /dev/null +++ b/api/endpoints/course.py @@ -0,0 +1,75 @@ +from manage import crud +from typing import Any, List + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from utils import deps +from cruds import crud_course +from schemas.course import Course, CourseCreate, CourseUpdate +from core import settings +from models import User +from fastapi import HTTPException + +router = APIRouter() + + +# get course, can be called by any user (1 through 4) +@router.get("/", response_model=List[Course]) +def get_course( + current_user: User = Depends(deps.get_current_active_user), + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, +) -> Any: + course = crud_course.get_multi(db, skip=skip, limit=limit) + return course + + +# add a new course, only executed if the user is either a super admin or admin +@router.post("/") +def create_course( + db: Session = Depends(deps.get_db), + *, + obj_in: CourseCreate, + current_user: User = Depends(deps.get_current_admin_or_above), +) -> Any: + crud_course.create(db, obj_in=obj_in) + return {"status": "success"} + + +# get a specific course, can be called by any user (1 through 4) +@router.get("/{id}/", response_model=Course) +def get_specific_course( + current_user: User = Depends(deps.get_current_active_user), + db: Session = Depends(deps.get_db), + *, + id: int, +) -> Any: + course = crud_course.get(db, id) + return course + + +# update a specific user, can be called by only admin and superadmin +@router.put("/{id}/") +def update_course( + db: Session = Depends(deps.get_db), + *, + id: int, + obj_in: CourseUpdate, + current_user: User = Depends(deps.get_current_admin_or_above), +) -> Any: + course = crud_course.get(db, id) + crud_course.update(db, db_obj=course, obj_in=obj_in) + return {"status": "success"} + + +@router.delete("/{course_id}/") +async def delete_course( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_admin_or_above), + *, + course_id: int, +): + crud_course.remove(db=db, id=course_id) + return {"msg": "success"} diff --git a/api/endpoints/department.py b/api/endpoints/department.py new file mode 100644 index 0000000000000000000000000000000000000000..1de99206e3afeac35af2cc8cdf52312e742093bc --- /dev/null +++ b/api/endpoints/department.py @@ -0,0 +1,95 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from utils import deps +from cruds import crud_department +from schemas import Department, DepartmentUpdate, Course +from models import User +from fastapi import HTTPException +from core import settings + +router = APIRouter() + +# get all Departments, can be called by all users (1 through 4) +@router.get("/", response_model=List[Department]) +def get_department( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + department = crud_department.get_multi(db, skip=skip, limit=limit) + return department + + +# create a new deparment, can be only created by admin and superadmin +@router.post("/", response_model=Department) +def create_department( + db: Session = Depends(deps.get_db), + *, + obj_in: DepartmentUpdate, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + if current_user.user_type > settings.UserType.ADMIN.value: + raise HTTPException( + status_code=403, detail="Error ID: 104" + ) # user has no authorization for creating departments + else: + department = crud_department.create(db, obj_in=obj_in) + return department + + +# get a specific department, can be called by all user types (1 through 4) +@router.get("/{id}/", response_model=Department) +def get_specific_department( + db: Session = Depends(deps.get_db), + *, + id: int, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + department = crud_department.get(db, id) + return department + + +# update a specific department, can be called by only superadmin and admin +@router.put("/{id}/") +def update_department( + db: Session = Depends(deps.get_db), + *, + id: int, + obj_in: DepartmentUpdate, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + if current_user.user_type > settings.UserType.ADMIN.value: + raise HTTPException( + status_code=403, detail="Error ID: 105" + ) # user has no authorization for updating departments + else: + department = crud_department.get(db, id) + crud_department.update(db, db_obj=department, obj_in=obj_in) + return {"status": "success"} + + +@router.get("/{id}/courses/", response_model=List[Course]) +def get_department_course( + db: Session = Depends(deps.get_db), + *, + id: int, + current_user: User = Depends(deps.get_current_admin_or_above), +) -> Any: + department = crud_department.get(db, id) + courses = department.courses + return courses + + +@router.delete("/{department_id}/") +async def delete_department( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_admin_or_above), + *, + department_id: int, +): + crud_department.remove(db=db, id=department_id) + return {"msg": "success"} diff --git a/api/endpoints/group.py b/api/endpoints/group.py new file mode 100644 index 0000000000000000000000000000000000000000..958687afcd28e020fc26fe4bbf5af452ce2733b3 --- /dev/null +++ b/api/endpoints/group.py @@ -0,0 +1,130 @@ +from schemas.group import GroupReturn +from typing import Any, List + +from fastapi import APIRouter, Depends +from fastapi.encoders import jsonable_encoder +from sqlalchemy.orm import Session + +from utils import deps +from cruds import crud_group, crud_department, crud_course +from schemas.group import ( + Group, + GroupUpdate, + GroupCreate, + GroupStudentReturn, + GroupWithProgram, +) +from models import User +from core import settings +from fastapi import HTTPException + +router = APIRouter() + +# get group: +# can be called by student to get their group, +# can be called by teacher to get the group under their depart +# can be called by admin and super admin to get all the departs +@router.get("/", response_model=List[GroupWithProgram]) +async def get_group( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + if current_user.user_type == settings.UserType.STUDENT.value: + got_group = crud_group.get(db, current_user.group_id) + group = [] + group.append(got_group) + return group + + if current_user.user_type == settings.UserType.TEACHER.value: + return [ + teacher_group_link.group + for teacher_group_link in current_user.teacher_group + ] + + if current_user.user_type <= settings.UserType.ADMIN.value: + group = crud_group.get_multi(db, skip=skip, limit=limit) + return group + + +# create new group, can be done by only admin and super admin +@router.post("/", response_model=Group) +async def create_group( + db: Session = Depends(deps.get_db), + *, + obj_in: GroupCreate, + current_user: User = Depends(deps.get_current_admin_or_above), +) -> Any: + return crud_group.create(db, obj_in=obj_in) + + +# get a specific group by id +# student: cannot get by id, can get their own group by directly calling "/" +# teacher: can get a specific group only if it exists in their groups_list +# superadmin and admin, no restriction, can get any group by id +@router.get("/{id}", response_model=Group, summary="Get specific group") +@router.get( + "/{id}/student/", + response_model=GroupStudentReturn, + summary="Get students of specific group", +) +async def get_specific_group( + db: Session = Depends(deps.get_db), + *, + id: int, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + if not current_user: + raise HTTPException(status_code=404, detail="Error ID: 107") # user not found! + + if current_user.user_type == settings.UserType.STUDENT.value: + if current_user.group_id == id: + return crud_group.get(db, id=id) + else: + raise HTTPException( + status_code=403, + detail="Error ID: 108", + ) # user has no authorization to access this group + + if current_user.user_type == settings.UserType.TEACHER.value: + for group in current_user.teacher_group: + if group.teacher_id == current_user.id: + return group.group + raise HTTPException( + status_code=403, + detail="Error ID: 109", + ) # user has no authorization to access this group + + if current_user.user_type >= settings.UserType.ADMIN.value: + group = crud_group.get(db, id) + return group + + +# update group, can be called by only the superadmin and admin +@router.put("/{id}", response_model=GroupUpdate) +async def update_group( + db: Session = Depends(deps.get_db), + *, + id: int, + obj_in: GroupUpdate, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + + if current_user.user_type >= settings.UserType.TEACHER.value: + raise HTTPException( + status_code=403, + detail="Error ID: 110", + ) # user has no authorization for updating groups + else: + group = crud_group.get(db, id) + crud_group.update(db, db_obj=group, obj_in=obj_in) + return {"status": "success"} + + +@router.get("/all/") +async def get_all_groups( + db: Session = Depends(deps.get_db), +) -> Any: + group = crud_group.get_multi(db, limit=-1) + return group diff --git a/api/endpoints/personal_note.py b/api/endpoints/personal_note.py new file mode 100644 index 0000000000000000000000000000000000000000..7f59da9cf514effa48b033404d45b4e6884ef740 --- /dev/null +++ b/api/endpoints/personal_note.py @@ -0,0 +1,195 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from utils import deps +from cruds import crud_personal_note +from schemas import PersonalNote, PersonalNoteUpdate, PersonalNoteCreate +from models import User +from core import settings +from fastapi import HTTPException + +router = APIRouter() + + +# get personal note: +# student: get only theirs +# teacher: get only theirs +# admin: none +# super admin: all +@router.get("/", response_model=List[PersonalNote]) +def get_personal_note( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + + if not current_user: + # user not found! + raise HTTPException(status_code=404, detail="Error ID: 116") + + if current_user.user_type >= settings.UserType.TEACHER.value: + personal_note_list = [] + personal_notes = current_user.personalnote + for note in personal_notes: + personal_note = crud_personal_note.get(db, id=note.id) + personal_note_list.append(personal_note) + return personal_note_list + + if current_user.user_type == settings.UserType.ADMIN.value: + raise HTTPException( + status_code=403, + detail="Error ID: 117", + ) # user has no authorization for retrieving personal notes, cause they personal fam! + + if current_user.user_type == settings.UserType.SUPERADMIN.value: + personal_note = crud_personal_note.get_multi(db, skip=skip, limit=limit) + return personal_note + + +# Create new personal note +# student: can create only theirs +# teacher: can create only theirs +# admin: no create previlege +# superadmin: can create all +@router.post("/", response_model=PersonalNote) +def create_personal_note( + db: Session = Depends(deps.get_db), + *, + obj_in: PersonalNoteCreate, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + if not current_user: + # user not found! + raise HTTPException(status_code=404, detail="Error ID: 119") + + if current_user.user_type >= settings.UserType.TEACHER.value: + if obj_in.user_id != current_user.id: + raise HTTPException( + status_code=403, + detail="Error ID: 118", + ) # user has no authorization to create personal note for another user + else: + personal_note = crud_personal_note.create(db, obj_in=obj_in) + return personal_note + + if current_user.user_type == settings.UserType.ADMIN.value: + raise HTTPException( + status_code=403, + detail="Error ID: 120", + ) # user has no authorization to create personal notes + + if current_user.user_type == settings.UserType.SUPERADMIN.value: + personal_note = crud_personal_note.create(db, obj_in=obj_in) + return personal_note + + +# get specific personal note, +# student and teacher can only get that specific note if they own it +# admin can has no permission +# superadmin can get it +@router.get("/{id}/", response_model=PersonalNote) +def get_specific_personal_note( + db: Session = Depends(deps.get_db), + *, + id: int, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + if not current_user: + # user not found! + raise HTTPException(status_code=404, detail="Error ID: 121") + + if current_user.user_type == settings.UserType.ADMIN.value: + raise HTTPException( + status_code=403, + detail="Error ID: 122", + ) # user has no authorization to get personal notes + + if current_user.user_type >= settings.UserType.TEACHER.value: + personal_notes = get_personal_note(db, current_user=current_user) + for notes in personal_notes: + if id == notes.id: + personal_note = crud_personal_note.get(db, id) + return personal_note + + raise HTTPException( + status_code=403, + detail="Error ID: 123", + ) # user has no authorization to get other user's personal notes + + if current_user.user_type == settings.UserType.SUPERADMIN.value: + personal_note = crud_personal_note.get(db, id) + return personal_note + + +@router.put("/{id}/", response_model=PersonalNote) +def update_personal_note( + db: Session = Depends(deps.get_db), + *, + id: int, + obj_in: PersonalNoteUpdate, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + if not current_user: + # user not found! + raise HTTPException(status_code=404, detail="Error ID: 124") + + if current_user.user_type == settings.UserType.ADMIN.value: + raise HTTPException( + status_code=403, + detail="Error ID: 125", + ) # user has no authorization to edit personal notes + + if current_user.user_type >= settings.UserType.TEACHER.value: + if obj_in.user_id == current_user.id: + + personal_note = crud_personal_note.get(db, id) + return crud_personal_note.update(db, db_obj=personal_note, obj_in=obj_in) + + else: + raise HTTPException( + status_code=403, + detail="Error ID: 126", + ) # user has no authorization to get other user's personal notes + + if current_user.user_type == settings.UserType.SUPERADMIN.value: + personal_note = crud_personal_note.get(db, id) + return crud_personal_note.update(db, db_obj=personal_note, obj_in=obj_in) + + +# XXX: For deleting all, is this needed? + +# @router.delete("/{}") +# def deletePersonalNotes( +# db: Session = Depends(deps.get_db), +# *, +# current_user: User = Depends(deps.get_current_active_superuser); +# )->Any: +# crud_personal_note.delete + + +@router.delete("/{id}/") +def deleteSpecificPersonalNote( + db: Session = Depends(deps.get_db), + *, + id: int, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + + if current_user.user_type == settings.UserType.SUPERADMIN.value: + personalNote = crud_personal_note.remove(db, id=id) + return personalNote + + if current_user.user_type == settings.UserType.ADMIN.value: + raise HTTPException( + status_code=403, + detail="Error ID: 142", # user has no authorization to delete notes of other users + ) + + personalNote = get_specific_personal_note(db, id=id, current_user=current_user) + + personalNote = crud_personal_note.remove(db, id=personalNote.id) + + return personalNote diff --git a/api/endpoints/program.py b/api/endpoints/program.py new file mode 100644 index 0000000000000000000000000000000000000000..50bb313bbef8d2ab049e7b53739e64b4d915e724 --- /dev/null +++ b/api/endpoints/program.py @@ -0,0 +1,78 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from starlette.responses import Response +from core.cache import cache + +from cruds.program import crud_program +from cruds.group import crud_group +from schemas import Program, ProgramCreate, ProgramUpdate, GroupCreate +from schemas import program +from utils import deps + +router = APIRouter() + + +@router.get("/", response_model=List[Program]) +async def get_programs( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 500, +) -> Any: + programs = crud_program.get_multi(db, skip=skip, limit=limit) + return programs + + +@router.post("/", response_model=Program) +async def create_program( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_admin_or_above), + *, + program_in: ProgramCreate, +) -> Any: + program = crud_program.create(db, obj_in=Program(**program_in.dict())) + for sem_iter in range(program_in.max_sems): + group = GroupCreate( + program_id=program.id, + sem=sem_iter + 1, + ) + print(group.dict()) + crud_group.create(db=db, obj_in=group) + return program + + +@router.get("/{program_id}/", response_model=Program) +@router.get("/{program_id}/group", response_model=program.ProgramGroupReturn) +async def get_program( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_active_user), + *, + program_id: int, +) -> Any: + program = crud_program.get(db, program_id) + return program + + +@router.put("/{program_id}/") +def update_program( + db: Session = Depends(deps.get_db), + *, + program_id: int, + obj_in: ProgramUpdate, + current_user=Depends(deps.get_current_admin_or_above), +) -> Any: + department = crud_program.get(db, program_id) + crud_program.update(db, db_obj=department, obj_in=obj_in) + return {"status": "success"} + + +@router.delete("/{program_id}/") +async def delete_program( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_admin_or_above), + *, + program_id: int, +): + crud_program.remove(db=db, id=program_id) + return {"msg": "success"} diff --git a/api/endpoints/quiz.py b/api/endpoints/quiz.py new file mode 100644 index 0000000000000000000000000000000000000000..51eb5aab4ebe768c07e9dd3f1f9eae25107c63ea --- /dev/null +++ b/api/endpoints/quiz.py @@ -0,0 +1,456 @@ +from datetime import datetime +from typing import Any, List, Dict + +from hashlib import sha1 + +import os +import shutil + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core.config import settings +import json +from models import User +from utils import deps +from cruds import crud_quiz, crud_question +from schemas import ( + Quiz, + QuizCreate, + QuizUpdate, + QuizAnswer, + QuizAnswerCreate, + QuizAnswerUpdate, + QuizQuestion, + QuizQuestionCreate, + QuizQuestionUpdate, + QuizQuestionwoutAnswer, +) + +from fastapi import FastAPI, File, UploadFile, HTTPException +from fastapi.responses import FileResponse +import aiofiles +from core.config import settings + +router = APIRouter() + +QUIZ_ROUTE: str = "quiz" +QUIZ_QUESTION_UPLOAD_DIR: str = "question_image" +QUIZ_OPTION_UPLOAD_DIR: str = "option_image" +hashedQuestionRoute = sha1( + QUIZ_QUESTION_UPLOAD_DIR.encode(encoding="UTF-8", errors="strict") +) +hashedOptionRoute = sha1( + QUIZ_OPTION_UPLOAD_DIR.encode(encoding="UTF-8", errors="strict") +) + + +@router.get("/", response_model=List[Quiz]) +async def get_quiz( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = -1, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + quiz = crud_quiz.get_multi(db, skip=skip, limit=limit) + + if current_user.user_type == settings.UserType.STUDENT.value: + quiz_list = [] + for quizItem in quiz: + for group in quizItem.group: + if group.id == current_user.group.id: + quiz_list.append(quizItem) + + return quiz_list + + if current_user.user_type == settings.UserType.TEACHER.value: + + quiz_list = [] + for quizItem in quiz: + for instructor in quizItem.instructor: + if current_user.id == instructor.id: + quiz_list.append(quizItem) + return quiz_list + + if current_user.user_type <= settings.UserType.ADMIN.value: + return quiz + + +@router.post("/") +async def create_quiz( + db: Session = Depends(deps.get_db), + *, + obj_in: QuizCreate, + current_user: User = Depends(deps.get_current_active_teacher_or_above), +) -> Any: + quiz = crud_quiz.create(db, obj_in=obj_in) + return {"msg": "success", "id": quiz.id} + + +@router.get("/{id}", response_model=Quiz) +async def get_specific_quiz( + db: Session = Depends(deps.get_db), + *, + id: int, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + if current_user.user_type == settings.UserType.STUDENT.value: + quiz_list = await get_quiz(db=db, current_user=current_user) + for quiz in quiz_list: + if quiz.id == id: + return quiz + raise HTTPException( + status_code=403, detail="Error ID: 133" + ) # not accessible by the Student user + + if current_user.user_type == settings.UserType.TEACHER.value: + quiz_list = await get_quiz(db=db, current_user=current_user) + for quiz in quiz_list: + if quiz.id == id: + return quiz + raise HTTPException( + status_code=403, detail="Error ID: 134" + ) # not accessible by the Teacher user + + if current_user.user_type <= settings.UserType.ADMIN.value: + quiz = crud_quiz.get(db, id) + return quiz + + +@router.put("/{id}", response_model=QuizUpdate) +async def update_quiz( + db: Session = Depends(deps.get_db), + *, + id: int, + obj_in: QuizUpdate, + current_user: User = Depends(deps.get_current_active_teacher_or_above), +) -> Any: + quiz = crud_quiz.get(db, id) + quiz = crud_quiz.update(db, db_obj=quiz, obj_in=obj_in) + return quiz + + +@router.get("/{quizid}/question", response_model=List[QuizQuestionwoutAnswer]) +async def get_question( + db: Session = Depends(deps.get_db), + *, + quizid: int, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + quiz = await get_specific_quiz(db, id=quizid, current_user=current_user) + if not quiz: + raise HTTPException( + status_code=404, detail="Error ID = 135" + ) # quiz not found in database + + questions = crud_question.get_all_by_quiz_id(db, quiz_id=quiz.id) + return questions + + +@router.get("/{quizid}/question/{id}", response_model=QuizQuestionwoutAnswer) +async def get_specific_question( + db: Session = Depends(deps.get_db), + *, + quizid: int, + id: int, + current_user: User = Depends(deps.get_current_active_user), +) -> Any: + + question = crud_question.get_by_quiz_id_question_id( + db=db, quiz_id=quizid, questionid=id + ) + + if not question: + raise HTTPException( + status_code=404, detail="Error ID: 136" + ) # question specific to that id not found + + return question + + +@router.post("/{quizid}/question") +async def create_question( + db: Session = Depends(deps.get_db), + *, + quizid: int, + obj_in: QuizQuestionCreate, + current_user: User = Depends(deps.get_current_active_teacher_or_above), +) -> Any: + obj_in.quiz_id = quizid + # obj_in.question_image = "question" + # obj_in.options = [ + # {"image": eachDict["image"] if eachDict["image"] == "" else f"Options{index+1}"} + # for index, eachDict in enumerate(obj_in.options) + # ] + + question = crud_question.create(db, obj_in=obj_in) + quiz = crud_quiz.get(db=db, id=quizid) + + newMarks = quiz.total_marks + question.marks + newQuiz = QuizUpdate(total_marks=newMarks) + + crud_quiz.update(db=db, db_obj=quiz, obj_in=newQuiz) + + hashedQuizId = sha1(str(quizid).encode(encoding="UTF-8", errors="strict")) + + hashedQuestionId = sha1(str(question.id).encode(encoding="UTF-8", errors="strict")) + + # if the question is said to have a IMAGE then only create the folder to store the image + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, + QUIZ_ROUTE, + hashedQuizId.hexdigest(), + hashedQuestionId.hexdigest(), + ) + + FILE_PATH_QUESTION = os.path.join(FILE_PATH, hashedQuestionRoute.hexdigest()) + + FILE_PATH_OPTION = os.path.join(FILE_PATH, hashedOptionRoute.hexdigest()) + + if not os.path.exists(FILE_PATH_QUESTION): + os.makedirs(FILE_PATH_QUESTION) + + if not os.path.exists(FILE_PATH_OPTION): + os.makedirs(FILE_PATH_OPTION) + + return {"msg": "success", "id": question.id} + + +@router.put("/{quizid}/question/{id}") +async def update_question( + db: Session = Depends(deps.get_db), + *, + quizid: int, + obj_in: QuizQuestionUpdate, + id: int, + current_user: User = Depends(deps.get_current_active_teacher_or_above), +) -> Any: + + question = crud_question.get(db, id) + + # on question_type update, create folder to store image if not already present + FILE_PATH_QUESTION = os.path.join( + settings.UPLOAD_DIR_ROOT, + QUIZ_QUESTION_UPLOAD_DIR, + f"{quizid}/{question.id}", + ) + + if not os.path.exists(FILE_PATH_QUESTION): + os.makedirs(FILE_PATH_QUESTION) + + # on option_type update, create folder to store image if not already present + # if (obj_in.answer_type == AnswerType.IMAGE_OPTIONS.value) and ( + # question.answer_type != obj_in.answer_type + # ): + # FILE_PATH_OPTION = os.path.join( + # "static", QUIZ_OPTION_UPLOAD_DIR, f"{quizid}/{question.id}" + # ) + # FILE_PATH_OPTION = os.path.join(current_directory, FILE_PATH_OPTION) + + # if not os.path.exists(FILE_PATH_OPTION): + # os.makedirs(FILE_PATH_OPTION) + + if question.quiz_id == quizid == obj_in.quiz_id: + question = crud_question.update(db, db_obj=question, obj_in=obj_in) + return question + else: + raise HTTPException( + status_code=403, detail="Error ID = 137" + ) # noqa Access Denied! + + +# XXX: ENDPOINTS for questions to write and read files and answers to those files + + +# FIXME: Uploaded files directory fix it +@router.post("/{quizid}/question/{id}/question_image/") +async def create_question_files( + db: Session = Depends(deps.get_db), + files: List[UploadFile] = File(...), + current_user=Depends(deps.get_current_active_teacher_or_above), + *, + quizid: int, + id: int, +): + question = await get_specific_question( + db, quizid=quizid, id=id, current_user=current_user + ) + + hashedQuizId = sha1(str(quizid).encode(encoding="UTF-8", errors="strict")) + + hashedQuestionId = sha1(str(id).encode(encoding="UTF-8", errors="strict")) + + FILE_QUESTION_PATH = os.path.join( + QUIZ_ROUTE, + hashedQuizId.hexdigest(), + hashedQuestionId.hexdigest(), + hashedQuestionRoute.hexdigest(), + ) + + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, + FILE_QUESTION_PATH, + ) + + questionImages = [] + fileIndex = 0 + for file in files: + fileName, fileExtension = os.path.splitext(file.filename) + hashedFileName = sha1( + (fileName + str(fileIndex)).encode(encoding="UTF-8", errors="strict") + ) + fileIndex = fileIndex + 1 + filename = f"{FILE_PATH}/{hashedFileName.hexdigest()}{fileExtension}" + async with aiofiles.open(filename, mode="wb") as f: + content = await file.read() + await f.write(content) + questionImages.append( + f"{FILE_QUESTION_PATH}/{hashedFileName.hexdigest()}{fileExtension}" + ) + + obj_in = QuizQuestionUpdate(quiz_id=quizid, question_image=questionImages) + updated = crud_question.update(db=db, db_obj=question, obj_in=obj_in) + + return updated + + +@router.post("/{quizid}/question/{id}/option_image/") +async def create_option_files( + options: List, + db: Session = Depends(deps.get_db), + files: List[UploadFile] = File(...), + current_user=Depends(deps.get_current_active_teacher_or_above), + *, + quizid: int, + id: int, +): + options = json.loads(options[0]) + + question = await get_specific_question( + db, quizid=quizid, id=id, current_user=current_user + ) + + hashedQuizId = sha1(str(quizid).encode(encoding="UTF-8", errors="strict")) + + hashedQuestionId = sha1(str(id).encode(encoding="UTF-8", errors="strict")) + FILE_OPTION_PATH = os.path.join( + QUIZ_ROUTE, + hashedQuizId.hexdigest(), + hashedQuestionId.hexdigest(), + hashedOptionRoute.hexdigest(), + ) + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, + FILE_OPTION_PATH, + ) + fileIndex = 0 + for file in files: + fileName, fileExtension = os.path.splitext(file.filename) + hashedFileName = sha1( + (fileName + str(fileIndex)).encode(encoding="UTF-8", errors="strict") + ) + fileIndex = fileIndex + 1 + filename = f"{FILE_PATH}/{hashedFileName.hexdigest()}{fileExtension}" + async with aiofiles.open(filename, mode="wb") as f: + content = await file.read() + await f.write(content) + alreadyModified = [] + for index, eachDict in enumerate(options): + if eachDict["image"] == file.filename: + if index not in alreadyModified: + eachDict[ + "image" + ] = f"{FILE_OPTION_PATH}/{hashedFileName.hexdigest()}{fileExtension}" + alreadyModified.append(index) + break + + obj_in = QuizQuestionUpdate(quiz_id=quizid, options=json.dumps(options)) + updated = crud_question.update(db=db, db_obj=question, obj_in=obj_in) + + return {"msg": "success"} + + +@router.get("/{quizid}/question/{id}/{type}/{filename}") +async def get_image( + db: Session = Depends(deps.get_db), + *, + quizid: int, + id: int, + filename: str, + type: int, + current_user: User = Depends(deps.get_current_active_user), +): + question = await get_specific_question( + db, quizid=quizid, id=id, current_user=current_user + ) + + if not question: + raise HTTPException(status_code=404, detail="Error ID: 138") + # question not found error + + if type == 1: + if filename in question.question_image: + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, QUIZ_QUESTION_UPLOAD_DIR, f"{quizid}/{id}" + ) + else: + raise HTTPException( + status_code=403, detail="Error ID: 139" + ) # file not of that question + + if type == 2: + if filename in question.option_image: + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, QUIZ_OPTION_UPLOAD_DIR, f"{quizid}/{id}" + ) + else: + raise HTTPException( + status_code=403, detail="Error ID: 140" + ) # file not of that question + + FILE_PATH = os.path.join(FILE_PATH, filename) + + if os.path.isfile(FILE_PATH): + file = FileResponse(f"{FILE_PATH}") + return file + else: + raise HTTPException( + status_code=404, detail="Error ID: 141" + ) # no file exist in the path + + +@router.delete("/{quizid}/") +async def delete_quiz( + db: Session = Depends(deps.get_db), + *, + quizid=int, + current_user: User = Depends(deps.get_current_active_teacher), +): + quiz = crud_quiz.get(db, id=quizid) + + if not quiz: + raise HTTPException(status_code=404, detail="Error ID: 143") + + for instructor in quiz.instructor: + + if instructor.id == current_user.id: + + print(instructor.id, current_user.id) + quiz = crud_quiz.remove(db, id=quizid) + + hashedQuizId = sha1(str(quizid).encode(encoding="UTF-8", errors="strict")) + + FILE_PATH = os.path.join( + settings.UPLOAD_DIR_ROOT, + QUIZ_ROUTE, + hashedQuizId.hexdigest(), + ) + + if os.path.exists(FILE_PATH): + shutil.rmtree(FILE_PATH) + + return {"msg": "delete success"} + + raise HTTPException( + status_code=403, + detail="Error ID: 142", + ) # teacher not associated with the quiz diff --git a/api/endpoints/quiz_answer.py b/api/endpoints/quiz_answer.py new file mode 100644 index 0000000000000000000000000000000000000000..a589fff6ae5963c675dbdd2ef853484f4ebc63d6 --- /dev/null +++ b/api/endpoints/quiz_answer.py @@ -0,0 +1,149 @@ +import math +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from models import User +from utils import deps +from datetime import datetime, timedelta +from cruds import crud_quiz_answer, crud_question, crud_quiz +from schemas import ( + QuizAnswer, + QuizAnswerCreate, + QuizAnswerUpdate, + QuizAnsweronlySelected, + QuizAnswerwithName, +) + +from typing import Any, Optional, List, Dict # noqa + + +router = APIRouter() + + +@router.get("/") +async def get_answers( + db: Session = Depends(deps.get_db), + *, + current_user: User = Depends(deps.get_current_active_user), +): + pass + + +@router.get("/{quizid}", response_model=QuizAnswer, response_model_exclude_none=True) +async def get_answers_quiz( + db: Session = Depends(deps.get_db), + *, + quizid: int, + current_user: User = Depends(deps.get_current_active_user), +): + answer = crud_quiz_answer.get_by_quiz_id( + db=db, quizId=quizid, studentId=current_user.id + ) + + if answer: + marks = answer.marks_obtained + answer.marks_obtained = None + + quiz = crud_quiz.get(db=db, id=quizid) + + if ( + quiz + and quiz.end_time + and quiz.end_time <= (datetime.utcnow() - timedelta(seconds=15)) + ): + answer.marks_obtained = marks + + return answer + + raise HTTPException(status_code=404, detail="Error ID: 144") + + +@router.get("/{quizid}/getAnswersAsTeacher/", response_model=List[QuizAnswerwithName]) +async def get_quiz_answers_as_teacher( + db: Session = Depends(deps.get_db), + *, + quizid: int, + current_user: User = Depends(deps.get_current_active_teacher_or_above), +): + if current_user.quiz: + for quiz in current_user.quiz: + if quiz.id == quizid: + answers = crud_quiz_answer.get_all_by_quiz_id_as_teacher( + db=db, quizId=quizid + ) + if len(answers) >= 1: + return answers + + raise HTTPException( + status_code=404, + detail="Error ID: 143", # could not populate answer + ) + + +@router.get("/{quizid}/exists/") +async def check_existence( + db: Session = Depends(deps.get_db), + *, + quizid: int, + current_user: User = Depends(deps.get_current_active_user), +): + answer = crud_quiz_answer.get_by_quiz_id( + db=db, quizId=quizid, studentId=current_user.id + ) + if not answer: + return {"exists": False} + else: + return {"exists": True} + + +@router.get("/{id}") +async def get_specific_answer(): + pass + + +@router.post("/{quiz_id}", response_model=QuizAnsweronlySelected) +async def submit_answer( + db: Session = Depends(deps.get_db), + *, + questionAnswer: Dict[int, Any], + quiz_id: int, + current_user: User = Depends(deps.get_current_active_user), +): + questions = crud_question.get_all_by_quiz_id(db, quiz_id=quiz_id) + + marksObtained = 0 + correctCount = 0 + for question in questions: + if question.id in questionAnswer.keys(): + questionOption = questionAnswer[question.id] + if question.multiple: + if len(question.answer) >= len(questionOption): + for answer in questionOption: + if answer in question.answer: + correctCount = correctCount + 1 + + correctCount = correctCount / len(question.answer) + + else: + questionAnswer[question.id] = questionOption + + if questionOption == question.answer[0]: + correctCount = 1 + + marksObtained = marksObtained + correctCount * question.marks + correctCount = 0 + + questionAnswer = QuizAnswerCreate( + marks_obtained=math.ceil(marksObtained), + options_selected=questionAnswer, + quiz_id=quiz_id, + student_id=current_user.id, + ) + + try: + questionAnswer = crud_quiz_answer.create(db, obj_in=questionAnswer) + return questionAnswer + except Exception: + raise HTTPException( + status_code=400, + detail="Error ID: 142", # could not populate answer + ) diff --git a/api/endpoints/school.py b/api/endpoints/school.py new file mode 100644 index 0000000000000000000000000000000000000000..78ae4d1023167f9ae3f128720173cd561f6cecf6 --- /dev/null +++ b/api/endpoints/school.py @@ -0,0 +1,67 @@ +from typing import List + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from cruds.school import crud_school +from schemas import School, SchoolCreate, SchoolUpdate +from utils import deps + +router = APIRouter() + + +@router.get("/", response_model=List[School]) +async def get_schools( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_active_user), + skip: int = 0, + limit: int = 100, +): + schools = crud_school.get_multi(db, skip=skip, limit=limit) + return schools + + +@router.post("/", response_model=School) +async def create_school( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_admin_or_above), + *, + school_in: SchoolCreate, +): + school = crud_school.create(db, obj_in=school_in) + return school + + +@router.get("/{school_id}/", response_model=School) +async def get_school( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_active_user), + *, + school_id: int, +): + school = crud_school.get(db, school_id) + return school + + +@router.put("/{school_id}/", response_model=School) +async def update_school( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_admin_or_above), + *, + school_id: int, + school_update: SchoolUpdate, +): + school = crud_school.get(db, school_id) + school = crud_school.update(db, db_obj=school, obj_in=school_update) + return school + + +@router.delete("/{school_id}/") +async def delete_school( + db: Session = Depends(deps.get_db), + user=Depends(deps.get_current_admin_or_above), + *, + school_id: int, +): + crud_school.remove(db=db, id=school_id) + return {"msg": "success"} diff --git a/api/endpoints/teacher_note.py b/api/endpoints/teacher_note.py new file mode 100644 index 0000000000000000000000000000000000000000..7ade3aca9a82bf3e332d186df9883a7ff728bbd1 --- /dev/null +++ b/api/endpoints/teacher_note.py @@ -0,0 +1,61 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from utils import deps +from cruds import crud_teacher_note +from schemas import TeacherNote, TeacherNoteUpdate, TeacherNoteCreate + + +router = APIRouter() + + +# TODO: Search by student ?? +@router.get("/teacher_note/", response_model=List[TeacherNote]) +async def get_teacher_note( + user=Depends(deps.get_current_active_teacher), + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, +) -> Any: + teacher_note = crud_teacher_note.get_user_teacher_note(db, user=user) + return teacher_note + + +# TODO: Teacher can only post notes on students that are his student +@router.post("/teacher_note/", response_model=TeacherNote) +async def create_teacher_note( + user=Depends(deps.get_current_active_teacher), + db: Session = Depends(deps.get_db), + *, + obj_in: TeacherNoteCreate +) -> Any: + teacher_note = crud_teacher_note.create(db, obj_in=obj_in) + return teacher_note + + +@router.get("/teacher_note/{id}/", response_model=TeacherNote) +async def get_specific_teacher_note( + user=Depends(deps.get_current_active_teacher), + db: Session = Depends(deps.get_db), + *, + id: int +) -> Any: + teacher_note = crud_teacher_note.get_user_teacher_note(db=db, user=user, id=id) + return teacher_note + + +@router.put("/teacher_note/{id}/", response_model=TeacherNote) +async def update_teacher_note( + user=Depends(deps.get_current_active_teacher), + db: Session = Depends(deps.get_db), + *, + id: int, + obj_in: TeacherNoteUpdate +) -> Any: + teacher_note = crud_teacher_note.get_user_teacher_note(db=db, user=user, id=id) + if not teacher_note: + raise HTTPException(status_code=403, detail="Error ID: 127") # Access denied! + teacher_note = crud_teacher_note.update(db, db_obj=teacher_note, obj_in=obj_in) + return teacher_note diff --git a/api/endpoints/two_fa.py b/api/endpoints/two_fa.py new file mode 100644 index 0000000000000000000000000000000000000000..b89ffe6eebd641627681b77d08c95a070a119096 --- /dev/null +++ b/api/endpoints/two_fa.py @@ -0,0 +1,151 @@ +import json +import os +from typing import Any, List, Optional + +import aiofiles +from fastapi import APIRouter, Body +from fastapi import Cookie as ReqCookie +from fastapi import Depends, File, HTTPException, Request, UploadFile +from fastapi.params import Cookie +from sqlalchemy.orm import Session +from sqlalchemy.sql.expression import update +from sqlalchemy.sql.functions import current_user +from starlette.responses import JSONResponse, Response + +import cruds +import models +import schemas +from core import throttle +from core.config import settings +from core.db import redis_session_client +from core.security import ( + create_sesssion_token, + get_password_hash, + create_2fa_temp_token, + create_2fa_enable_temp_token +) +from cruds import group +from schemas.user import UserUpdate, VerifyUser +from utils import deps +from utils.utils import ( + expire_web_session, + generate_password_reset_token, + send_reset_password_email, + send_verification_email, + verify_password_reset_token, + verify_user_verify_token, +) +import pyotp +from cruds import crud_user + + +router = APIRouter() + + +@router.get("/enable/request") +async def two_fa_enable_request( + db: Session = Depends(deps.get_db), + *, + current_user: models.User = Depends(deps.get_current_user), + request: Request, + response: Response, +) -> Any: + if current_user.two_fa_secret != None: + raise HTTPException( + status_code=409, detail="2FA is already enabled!" + ) + + totp_secret = pyotp.random_base32() + await create_2fa_enable_temp_token(current_user, totp_secret) + + totp_url = pyotp.totp.TOTP(totp_secret).provisioning_uri( + name=current_user.email, + issuer_name=settings.PROJECT_NAME + ) + + return {"msg": "2FA enable requested!", "uri": totp_url, "secret": totp_secret} + + +@router.post("/enable/confirm") +async def two_fa_enable_confirm( + db: Session = Depends(deps.get_db), + *, + form_data: schemas.Two_FA_Confirm, + current_user: models.User = Depends(deps.get_current_user), +) -> Any: + totp_secret = await redis_session_client.client.get( + f"two_fa_enable_temp_{current_user.id}" + ) + totp_secret = totp_secret.decode("utf-8") + if not totp_secret: + raise HTTPException( + status_code=403, detail="Invalid or expired TOTP" + ) + + totp = pyotp.TOTP(totp_secret) + totp_valid = totp.verify(str(form_data.totp), valid_window=1) + + if totp_valid: + crud_user.enable_2fa(db, secret=totp_secret, db_obj=current_user) + await redis_session_client.client.delete( + f"two_fa_enable_temp_{current_user.id}" + ) + return {"msg": "2FA successfully enabled!"} + else: + return {"msg": "Invalid TOTP!"} + + +@router.post("/login/confirm", response_model=Optional[schemas.user.UserLoginReturn], response_model_exclude_none=True) +async def two_fa_login_confirm( + db: Session = Depends(deps.get_db), + *, + form_data: schemas.Two_FA_Confirm, + request: Request, + response: Response +) -> Any: + token = request.cookies.get("temp_session") + + if token == None: + raise HTTPException( + status_code=403, detail="Invalid token!" + ) + + data = json.loads(await redis_session_client.client.get( + f"two_fa_temp_{token}", + )) + # json.dumps({"user": user.id, "remember_me": remember_me}), + + user = crud_user.get(db, id=data.get("user")) + + totp = pyotp.TOTP(user.two_fa_secret) + totp_valid = totp.verify(str(form_data.totp), valid_window=1) + + if not totp_valid: + raise HTTPException( + status_code=403, detail="Invalid TOTP!" + ) + + session_token = await create_sesssion_token(user, data.get("remember_me"), request) + + response.delete_cookie("temp_session") + response.set_cookie("session", session_token, httponly=True) + + await redis_session_client.client.delete(f"two_fa_temp_{token}") + return {"msg": "Logged in successfully!", "user": user, "two_fa_required": None} + + +@router.delete( + "/disable", +) +async def two_fa_disable( + db: Session = Depends(deps.get_db), + *, + current_user: models.User = Depends(deps.get_current_user), +) -> Any: + if current_user.two_fa_secret == None: + raise HTTPException( + status_code=409, detail="2FA is already disabled!" + ) + crud_user.disable_2fa(db, db_obj=current_user) + + return {"msg": "2FA successfully disabled!"} diff --git a/api/endpoints/user_permission.py b/api/endpoints/user_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..25d4997e936be3f996017f620006e3b0ee9cbc37 --- /dev/null +++ b/api/endpoints/user_permission.py @@ -0,0 +1,49 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from utils import deps +from cruds import crud_user_permission +from schemas.user_permission import ( + UserPermission, + UserPermissionCreate, + UserPermissionUpdate, +) + +router = APIRouter() + + +@router.get("/", response_model=List[UserPermission]) +async def get_user_permission( + db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 100 +) -> Any: + user_permission = crud_user_permission.get_multi(db, skip=skip, limit=limit) + return user_permission + + +@router.post("/", response_model=UserPermission) +async def create_user_permission( + db: Session = Depends(deps.get_db), *, obj_in: UserPermissionCreate +) -> Any: + user_permission = crud_user_permission.create(db, obj_in=obj_in) + return user_permission + + +@router.get("/{id}", response_model=UserPermission) +async def get_specific_user_permission( + db: Session = Depends(deps.get_db), *, id: int +) -> Any: + user_permission = crud_user_permission.get(db, id) + return user_permission + + +@router.put("/{id}", response_model=UserPermission) +async def update_user_permission( + db: Session = Depends(deps.get_db), *, id: int, obj_in: UserPermissionUpdate +) -> Any: + user_permission = crud_user_permission.get(db, id) + user_permission = crud_user_permission.update( + db, db_obj=user_permission, obj_in=obj_in + ) + return user_permission diff --git a/api/endpoints/users.py b/api/endpoints/users.py new file mode 100644 index 0000000000000000000000000000000000000000..6d136a89167f5288693448cc9aba93fb7cd0f519 --- /dev/null +++ b/api/endpoints/users.py @@ -0,0 +1,261 @@ +import secrets +from schemas.user import UserCreate +from typing import Any, List +import aiofiles +from hashlib import sha1 + +import os + +from fastapi import APIRouter, Body, Depends, HTTPException, UploadFile, File +from fastapi.encoders import jsonable_encoder +from pydantic.networks import EmailStr +from sqlalchemy.orm import Session +from core import settings + + +import cruds +import models +import schemas +from utils import deps +from core.config import settings +from utils.utils import send_reset_password_email + +from fastapi import FastAPI, File, Form, UploadFile +from typing import List, Optional +from datetime import date + +router = APIRouter() + + +@router.get("/", response_model=List[schemas.user.UserReturn]) +def read_users( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 100, + current_user: models.User = Depends(deps.get_current_admin_or_above), +) -> Any: + """ + Retrieve users. + """ + users = cruds.crud_user.get_multi(db, skip=skip, limit=limit) + return users + + +@router.get("/teacher/", response_model=List[schemas.user.TeacherShort]) +def get_teachers( + db: Session = Depends(deps.get_db), + skip: int = 0, + limit: int = 200, + current_user: models.User = Depends(deps.get_current_active_teacher_or_above), +) -> Any: + teachers = ( + db.query(models.User) + .filter(models.User.user_type == settings.UserType.TEACHER.value) + .all() + ) + return teachers + + +@router.post("/", response_model=schemas.User) +async def create_user( + *, + db: Session = Depends(deps.get_db), + user_in: schemas.user.AdminUserCreate, + current_user: models.User = Depends(deps.get_current_active_superuser), +) -> Any: + user = cruds.crud_user.get_by_email(db, email=user_in.email) + if user: + raise HTTPException( + status_code=400, + detail="Error ID: 128", + ) # The user with this username already exists in the system. + user_create = schemas.UserCreate( + email=user_in.email, + full_name=user_in.full_name, + address=user_in.address, + group_id=user_in.group_id, + contact_number=user_in.contact_number, + dob=user_in.dob, + join_year=user_in.join_year, + password=settings.SECRET_KEY, + ) + user = cruds.crud_user.create(db, obj_in=user_create) + cruds.crud_user.verify_user(db=db, db_obj=user) + await send_reset_password_email(user=user) + return user + + +@router.put("/me/", response_model=schemas.user.UserReturn) +async def update_user_me( + *, + db: Session = Depends(deps.get_db), + full_name: Optional[str] = Form(None), + address: Optional[str] = Form(None), + dob: Optional[date] = Form(None), + contact_number: Optional[str] = Form(None), + profile_photo: Optional[UploadFile] = File(None), + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Update own user. + """ + profile_db_path = None + if profile_photo: + profiles_path = os.path.join(settings.UPLOAD_DIR_ROOT, "profiles") + content_type = profile_photo.content_type + file_extension = content_type[content_type.index("/") + 1 :] + new_profile_image = f"{secrets.token_hex(nbytes=16)}.{file_extension}" + profile_db_path = os.path.join("profiles", new_profile_image) + new_profile_image_file_path = os.path.join( + settings.UPLOAD_DIR_ROOT, profile_db_path + ) + + if not os.path.exists(profiles_path): + os.makedirs(profiles_path) + + async with aiofiles.open(new_profile_image_file_path, mode="wb") as f: + content = await profile_photo.read() + await f.write(content) + + try: + if current_user.profile_image != None: + os.remove( + os.path.join(settings.UPLOAD_DIR_ROOT, current_user.profile_image) + ) + except Exception: + pass + + user_in = schemas.UserUpdate( + full_name=full_name, + address=address, + dob=dob, + contact_number=contact_number, + profile_image=profile_db_path, + ) + print(jsonable_encoder(user_in)) + + user = cruds.crud_user.update( + db, db_obj=current_user, obj_in=user_in.dict(exclude_none=True) + ) + + return user + + +@router.get( + "/me/", response_model=schemas.user.UserReturn, response_model_exclude_none=True +) +# @router.get("/me/teacher_group", response_model=schemas.user.UserReturn) +async def read_user_me( + db: Session = Depends(deps.get_db), + current_user: models.User = Depends(deps.get_current_active_user), +) -> Any: + """ + Get current user. + """ + return current_user + + +# @router.put("/me/profile/") +# async def update_my_profile_photo( +# db: Session = Depends(deps.get_db), +# *, +# current_user: models.User = Depends(deps.get_current_active_user), +# profile_photo: UploadFile = File(...), +# ): + +# cruds.crud_user.update( +# db, +# db_obj=current_user, +# obj_in=schemas.user.ImageUpdate(profile_image=profile_db_path), +# ) + +# return {"msg": "success", "profile": new_profile_image} + + +@router.get("/{user_id}/", response_model=schemas.user.UserReturn) +async def read_user_by_id( + user_id: int, + current_user: models.User = Depends(deps.get_current_active_user), + db: Session = Depends(deps.get_db), +) -> Any: + """ + Get a specific user by id. + """ + user = cruds.crud_user.get(db, id=user_id) + if user == current_user: + return user + + if current_user.user_type > settings.UserType.ADMIN.value: + raise HTTPException( + status_code=400, detail="Error ID: 131" + ) # The user doesn't have enough privileges + # if not cruds.crud_user.is_superuser(current_user): + # raise HTTPException( + # status_code=400, detail="Error ID: 131" + # ) # The user doesn't have enough privileges + return user + + +@router.put("/{user_id}/", response_model=schemas.User) +async def update_user( + *, + db: Session = Depends(deps.get_db), + user_id: int, + user_in: schemas.UserUpdate, + current_user: models.User = Depends(deps.get_current_admin_or_above), +) -> Any: + """ + Update a user. + """ + user = cruds.crud_user.get(db, id=user_id) + if not user: + raise HTTPException( + status_code=404, + detail="Error ID: 132", + ) # The user with this username does not exist in the system + user = cruds.crud_user.update(db, db_obj=user, obj_in=user_in) + return user + + +@router.delete("/{user_id}/") +async def delete_user( + *, + db: Session = Depends(deps.get_db), + user_id: int, + current_user: models.User = Depends(deps.get_current_admin_or_above), +) -> Any: + cruds.crud_user.remove(db, id=user_id) + return {"msg": "success"} + + +@router.put("/{user_id}/profile") +async def update_profile_photo( + db: Session = Depends(deps.get_db), + *, + user_id: int, + current_user: models.User = Depends(deps.get_current_admin_or_above), + profile_photo: UploadFile = File(...), +): + + user = cruds.crud_user.get_by_id(db, id=user_id) + profile_image_path = os.path.join("uploaded_files", "profiles") + profile_image = f"{abs(hash(str(user.id)))}.jpg" + profile_image_file_path = os.path.join(profile_image_path, profile_image) + + if not os.path.exists(profile_image_path): + os.makedirs(profile_image_path) + else: + if os.path.exists(os.path.join(profile_image_path, f"{user.profile_image}")): + os.remove(os.path.join(profile_image_path, f"{user.profile_image}")) + + async with aiofiles.open(profile_image_file_path, mode="wb") as f: + content = await profile_photo.read() + await f.write(content) + + user = cruds.crud_user.update( + db, + db_obj=user, + obj_in=schemas.UserUpdate(profile_image=profile_image), + ) + + return user diff --git a/api/endpoints/utils.py b/api/endpoints/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..f421aed841b2c5ba850f1c9c34a112350abffb99 --- /dev/null +++ b/api/endpoints/utils.py @@ -0,0 +1,16 @@ +from typing import Any + +from fastapi import APIRouter, Depends +from fastapi.responses import FileResponse +from pydantic.networks import EmailStr + +import models +import schemas +from utils import deps + +router = APIRouter() + + +@router.get("/ping") +async def ping() -> Any: + return {"msg": "pong"} diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..3a4b39b8631861e2ff478569fe5557f79a627781 --- /dev/null +++ b/app.py @@ -0,0 +1,100 @@ +import uvicorn +import os +from fastapi import FastAPI +from fastapi.openapi.docs import ( + get_swagger_ui_html, + get_swagger_ui_oauth2_redirect_html, +) +from pyngrok import ngrok +from starlette.middleware.cors import CORSMiddleware + +from core.config import settings +from core.db import ( + init, + redis_cache_client, + redis_chat_client, + redis_general, + redis_session_client, + redis_throttle_client, +) +from api import router + +app = FastAPI( + title=settings.PROJECT_NAME, + #openapi_url=f"{settings.API_V1_STR}/openapi.json", + # docs_url=None, +) + + +@app.on_event("startup") +async def startup(): + await redis_cache_client.initialize() + await redis_chat_client.initialize() + await redis_throttle_client.initialize() + await redis_session_client.initialize() + await redis_general.initialize() + init.init_db() + + +@app.on_event("shutdown") +async def shutdown(): + await redis_cache_client.close() + await redis_chat_client.close() + await redis_throttle_client.close() + await redis_session_client.close() + await redis_general.close() + + +"""@app.get("/docs", include_in_schema=False) +async def custom_swagger_ui_html(): + return get_swagger_ui_html( + openapi_url=app.openapi_url, + title=app.title + " - API Documentaion", + oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, + swagger_js_url=f"{settings.STATIC_URL_BASE}/static/swagger-ui-bundle.js", + swagger_css_url=f"{settings.STATIC_URL_BASE}/static/swagger-ui.css", + )""" + + +"""@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False) +async def swagger_ui_redirect(): + return get_swagger_ui_oauth2_redirect_html()""" + + +if settings.BACKEND_CORS_ORIGINS: + app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + pass + +app.include_router(router, prefix=settings.API_V1_STR) + + +def run(): + reload_blacklist = ["tests", ".pytest_cache"] + reload_dirs = os.listdir() + + for dir in reload_blacklist: + try: + reload_dirs.remove(dir) + except: + pass + public_url = ngrok.connect(addr=f"http://localhost:{settings.BACKEND_PORT}") + print("Public URL:", public_url) + uvicorn.run( + "app:app", + host=settings.BACKEND_HOST, + port=settings.BACKEND_PORT, + reload=settings.DEV_MODE, + reload_dirs=reload_dirs, + debug=settings.DEV_MODE, + workers=settings.WORKERS, + ) + + +if __name__ == "__main__": + run() diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0f2514010caac6ad298fc97bc1b98c2b3d9bf8d7 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1 @@ +from .config import settings diff --git a/core/__pycache__/__init__.cpython-310.pyc b/core/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c43238b90f4e8f2b5be206fc6f9dd8d4d1ecf479 Binary files /dev/null and b/core/__pycache__/__init__.cpython-310.pyc differ diff --git a/core/__pycache__/cache.cpython-310.pyc b/core/__pycache__/cache.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cdd7818c5c56f9570d3cb499a4654982b872619 Binary files /dev/null and b/core/__pycache__/cache.cpython-310.pyc differ diff --git a/core/__pycache__/config.cpython-310.pyc b/core/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fddec7d0a5a69dffc7c47b000c16ad563ac31335 Binary files /dev/null and b/core/__pycache__/config.cpython-310.pyc differ diff --git a/core/__pycache__/security.cpython-310.pyc b/core/__pycache__/security.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..161e37985e7179f4bbfd776441ded9156a268483 Binary files /dev/null and b/core/__pycache__/security.cpython-310.pyc differ diff --git a/core/__pycache__/throttle.cpython-310.pyc b/core/__pycache__/throttle.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9be173383706305ceaa583a9eca0098c13cbb462 Binary files /dev/null and b/core/__pycache__/throttle.cpython-310.pyc differ diff --git a/core/__pycache__/websocket.cpython-310.pyc b/core/__pycache__/websocket.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45f58bc4bb2190287b87a841bb902dc7592b7adc Binary files /dev/null and b/core/__pycache__/websocket.cpython-310.pyc differ diff --git a/core/cache.py b/core/cache.py new file mode 100644 index 0000000000000000000000000000000000000000..2ca3ed54e083e9cd0565e5d1d9d7bbe93423d695 --- /dev/null +++ b/core/cache.py @@ -0,0 +1,54 @@ +from functools import wraps +import json + +from starlette.responses import Response + +from core.db import redis_cache_client +from fastapi import HTTPException +from fastapi.encoders import jsonable_encoder +from typing import Callable + + +def cache(timeout: int = 600) -> Callable: + def outer_wrapper(func): + @wraps(func) + async def inner_wrapper(*args, **kwargs): + identifier = f"ec_{func.__name__}_{func.__module__}" + value = await redis_cache_client.client.get(identifier) + + if value: + return json.loads(value) + else: + ret_val = await func(*args, **kwargs) + ret_val = jsonable_encoder(ret_val) + await redis_cache_client.client.set(identifier, json.dumps(ret_val)) + await redis_cache_client.client.expire(identifier, timeout) + return ret_val + + return inner_wrapper + + return outer_wrapper + + +# TODO: Response Cache Headers + +# def cache_headers(timeout: int = 600, no_cache=False, ) -> Callable: +# def outer_wrapper(func): +# @wraps(func) +# async def inner_wrapper(*args, **kwargs): +# print(func.__module__) +# identifier = f"ec_{func.__name__}_{func.__module__}" +# value = await redis_cache_client.client.get(identifier) + +# if value: +# return json.loads(value) +# else: +# ret_val = await func(*args, **kwargs) +# ret_val = jsonable_encoder(ret_val) +# await redis_cache_client.client.set(identifier, json.dumps(ret_val)) +# await redis_cache_client.client.expire(identifier, timeout) +# return ret_val + +# return inner_wrapper + +# return outer_wrapper diff --git a/core/celery_app.py b/core/celery_app.py new file mode 100644 index 0000000000000000000000000000000000000000..0477d145894dd87a24f343ca71bc4634147790f4 --- /dev/null +++ b/core/celery_app.py @@ -0,0 +1,5 @@ +from celery import Celery + +celery_app = Celery("worker", broker="amqp://guest@queue//") + +celery_app.conf.task_routes = {"app.worker.test_celery": "main-queue"} diff --git a/core/config.py b/core/config.py new file mode 100644 index 0000000000000000000000000000000000000000..139835ccff9685fe19c4f69b59c6690a9feaee1f --- /dev/null +++ b/core/config.py @@ -0,0 +1,143 @@ +import enum +import os +from typing import List, Optional +import yaml + +from pydantic import ( + AnyHttpUrl, + BaseSettings, + EmailStr, + PostgresDsn, +) + + +class Settings(BaseSettings): + API_V1_STR: str + SECRET_KEY: str + SESSION_EXPIRE_TIME: int + SESSION_EXPIRE_TIME_EXTENDED: int + TWO_FA_TIMEOUT: int + PASSWORD_LESS_CREATE_TIMEOUT: int + SERVER_NAME: str + PROTOCOL = "http" + + #PROTOCAL: str + MODE: str + + BACKEND_HOST: str + BACKEND_PORT: int + + @property + def BACKEND_URL_BASE(self): + if self.BACKEND_PORT == 80: + return f"{self.PROTOCOL}://{self.BACKEND_HOST}" + else: + return f"{self.PROTOCOL}://{self.BACKEND_HOST}:{self.BACKEND_PORT}" + + STATIC_HOST: str + STATIC_PORT: int + + + @property + def STATIC_URL_BASE(self): + if self.STATIC_PORT == 80: + return f"{self.PROTOCOL}://{self.STATIC_HOST}" + else: + return f"{self.PROTOCOL}://{self.STATIC_HOST}:{self.STATIC_PORT}" + + FRONTEND_HOST: str + FRONTEND_PORT: int + + @property + def FRONTEND_URL_BASE(self): + if self.FRONTEND_PORT == 80: + return f"{self.PROTOCOL}://{self.FRONTEND_HOST}" + else: + return f"{self.PROTOCOL}://{self.FRONTEND_HOST}:{self.FRONTEND_PORT}" + + UPLOAD_DIR_ROOT: str + + WORKERS: int + + @property + def DEV_MODE(self): + if self.MODE == "dev": + return True + else: + return False + + BACKEND_CORS_ORIGINS: List[AnyHttpUrl] + + ALLOWED_EMAIL_HOST: List[str] + + PROJECT_NAME: str + + POSTGRES_SERVER: str + POSTGRES_USER: str + POSTGRES_PASSWORD: str + POSTGRES_DB: str + + @property + def POSTGRES_DATABASE_URI(self): + return PostgresDsn.build( + scheme="postgresql", + user=self.POSTGRES_USER, + password=self.POSTGRES_PASSWORD, + host=self.POSTGRES_SERVER, + path=f"/{self.POSTGRES_DB or ''}", + ) + + REDIS_HOST: str + REDIS_PORT: str + REDIS_PASSWORD: str + + SMTP_TLS: bool + SMTP_PORT: Optional[int] + SMTP_HOST: Optional[str] + SMTP_USER: Optional[str] + SMTP_PASSWORD: Optional[str] + + EMAILS_FROM_EMAIL: Optional[EmailStr] + + @property + def EMAILS_FROM_NAME(self): + return self.PROJECT_NAME + + EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48 + EMAIL_VERIFY_EXPIRE_HOURS: int = 48 + EMAIL_TEMPLATES_DIR: str + + @property + def EMAILS_ENABLED(self): + return bool( + self.SMTP_HOST + and self.SMTP_PORT + and self.EMAILS_FROM_EMAIL + ) + + FIRST_SUPERUSER: EmailStr + FIRST_SUPERUSER_PASSWORD: str + USERS_OPEN_REGISTRATION: bool + + class UserType(enum.Enum): + SUPERADMIN: int = 1 + ADMIN: int = 2 + TEACHER: int = 3 + STUDENT: int = 4 + + class Config: + case_sensitive = True + + +configs = {} + +with open("etc/base.yml", "r") as base_config_file: + configs = yaml.load(base_config_file.read(), yaml.Loader) + +config_path = os.environ.get("CONFIG_PATH") or "etc/dev.yml" + +with open(config_path, "r") as config_file: + custom_configs = yaml.load(config_file.read(), yaml.Loader) + configs.update(custom_configs) + +settings = Settings(**configs) diff --git a/core/db/__init__.py b/core/db/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..33b7c6647e9278a697293f8c01324b320c13d450 --- /dev/null +++ b/core/db/__init__.py @@ -0,0 +1,9 @@ +from .base import Base +from .redis_session import redis_chat_client +from .redis_session import redis_cache_client +from .redis_session import redis_general +from .redis_session import redis_session_client +from .redis_session import redis_throttle_client +from .session import SessionLocal +from .session import engine +from .init import init_db diff --git a/core/db/__pycache__/__init__.cpython-310.pyc b/core/db/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c723c374d4b80fcdb3f09265d4377d06da11ad3 Binary files /dev/null and b/core/db/__pycache__/__init__.cpython-310.pyc differ diff --git a/core/db/__pycache__/base.cpython-310.pyc b/core/db/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d68afd72a5a5488c8b47d61ca8f5e51780e6f4c Binary files /dev/null and b/core/db/__pycache__/base.cpython-310.pyc differ diff --git a/core/db/__pycache__/init.cpython-310.pyc b/core/db/__pycache__/init.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c395627931e18befa6ca180cbcaec794ecaed197 Binary files /dev/null and b/core/db/__pycache__/init.cpython-310.pyc differ diff --git a/core/db/__pycache__/redis_session.cpython-310.pyc b/core/db/__pycache__/redis_session.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4764c9443c4a0afc5cbf847246117bfeac202000 Binary files /dev/null and b/core/db/__pycache__/redis_session.cpython-310.pyc differ diff --git a/core/db/__pycache__/session.cpython-310.pyc b/core/db/__pycache__/session.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e4fda9fac17e69fefc9df950c4347af3f3adceb Binary files /dev/null and b/core/db/__pycache__/session.cpython-310.pyc differ diff --git a/core/db/base.py b/core/db/base.py new file mode 100644 index 0000000000000000000000000000000000000000..25cf33ac5b90db0a27588e3d0d821aaeec5e8cfd --- /dev/null +++ b/core/db/base.py @@ -0,0 +1,14 @@ +from typing import Any + +from sqlalchemy.ext.declarative import as_declarative, declared_attr + + +@as_declarative() +class Base: + id: Any + __name__: str + # Generate __tablename__ automatically + @declared_attr + def __tablename__(cls) -> str: + return cls.__name__.lower() + diff --git a/core/db/init.py b/core/db/init.py new file mode 100644 index 0000000000000000000000000000000000000000..f88c5e420b07d14ab908cec40f2f9db72d92bb49 --- /dev/null +++ b/core/db/init.py @@ -0,0 +1,86 @@ +import re + +from sqlalchemy.orm import Session + +import cruds +import schemas +from core import settings +from core.db import Base, SessionLocal +from cruds import crud_user +from models import * # noqa: F401 + + +def pascal_case_to_snake(name): + name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower() + + +def init_db(db: Session = SessionLocal()) -> None: + super_user = cruds.crud_user.get_by_email(db, email=settings.FIRST_SUPERUSER) + if not super_user: + user_in = schemas.UserCreate( + full_name="Yugesh", + dob="2021-04-14", + address="Dhulikhel", + contact_number="986152526272", + email=settings.FIRST_SUPERUSER, + password=settings.FIRST_SUPERUSER_PASSWORD, + is_superuser=True, + user_type=1, + ) + super_user = cruds.crud_user.create(db, obj_in=user_in) # noqa: F841 + + +# def init_permissions(db: Session = SessionLocal()) -> None: +# super_user = crud_user.get_by_id(db, id=1) +# for model in Base.__subclasses__(): +# try: +# name = pascal_case_to_snake(model.__name__) +# permission_create = schemas.UserPermissionCreate(name=f"{name}_create") +# permission_create = cruds.crud_user_permission.create( +# db, +# obj_in=permission_create, +# ) +# except Exception: # noqa +# pass + +# try: +# name = pascal_case_to_snake(model.__name__) +# permission_update = schemas.UserPermissionCreate(name=f"{name}_update") +# permission_update = cruds.crud_user_permission.create( +# db, +# obj_in=permission_update, +# ) +# except Exception: # noqa +# pass + +# try: +# name = pascal_case_to_snake(model.__name__) +# permission_retrieve = schemas.UserPermissionCreate(name=f"{name}_get") +# permission_retrieve = cruds.crud_user_permission.create( +# db, +# obj_in=permission_retrieve, +# ) +# except Exception: # noqa +# pass +# try: +# name = pascal_case_to_snake(model.__name__) +# permission_retrieve = schemas.UserPermissionCreate(name=f"{name}_get_self") +# permission_retrieve = cruds.crud_user_permission.create( +# db, +# obj_in=permission_retrieve, +# ) +# except Exception: # noqa +# pass + +# try: +# name = pascal_case_to_snake(model.__name__) +# permission_retrieve = schemas.UserPermissionCreate( +# name=f"{name}_update_self" +# ) +# permission_retrieve = cruds.crud_user_permission.create( +# db, +# obj_in=permission_retrieve, +# ) +# except Exception: # noqa +# pass diff --git a/core/db/redis_session.py b/core/db/redis_session.py new file mode 100644 index 0000000000000000000000000000000000000000..7c3a9d94f7c0358ca296261b4afa8a13289ac285 --- /dev/null +++ b/core/db/redis_session.py @@ -0,0 +1,51 @@ +import aioredis + +from core import settings + + +class RedisClient: + def __init__(self, db: int = 0): + self.client = None + self.db = db + + async def initialize(self): + self.client = await aioredis.create_redis_pool( + f"redis://{settings.REDIS_HOST}", + db=self.db, + ) + + async def close(self): + self.client.close() + await self.client.wait_closed() + + +class RedisChatClient(RedisClient): + def __init__(self): + super().__init__(db=0) + + +class RedisSessionClient(RedisClient): + def __init__(self): + super().__init__(db=1) + + +class RedisThrottleClient(RedisClient): + def __init__(self): + super().__init__(db=2) + + +class RedisCacheClient(RedisClient): + def __init__(self): + super().__init__(db=3) + + +class RedisGeneral(RedisClient): + def __init__(self): + super().__init__(db=4) + + +redis_session_client = RedisSessionClient() +redis_chat_client = RedisChatClient() +redis_throttle_client = RedisThrottleClient() +redis_cache_client = RedisCacheClient() +redis_general = RedisGeneral() diff --git a/core/db/session.py b/core/db/session.py new file mode 100644 index 0000000000000000000000000000000000000000..268a57de48eac3b935cb8bded6e0e4848072024c --- /dev/null +++ b/core/db/session.py @@ -0,0 +1,7 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from core import settings + +engine = create_engine(settings.POSTGRES_DATABASE_URI, pool_pre_ping=True) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/core/permission/__init__.py b/core/permission/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/core/permission/__pycache__/__init__.cpython-310.pyc b/core/permission/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5118f3c091fab2d19d8d3b7ec69e520bfcb0fc36 Binary files /dev/null and b/core/permission/__pycache__/__init__.cpython-310.pyc differ diff --git a/core/permission/__pycache__/permission.cpython-310.pyc b/core/permission/__pycache__/permission.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b20a0f4f87e536bd9aade4079fe0795933ff2aa Binary files /dev/null and b/core/permission/__pycache__/permission.cpython-310.pyc differ diff --git a/core/permission/default_permission.py b/core/permission/default_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..901c2c75ebf61ce729ab724e932aa33f271e5fe7 --- /dev/null +++ b/core/permission/default_permission.py @@ -0,0 +1,50 @@ +from core.config import settings +from core.db import SessionLocal +from cruds import crud_user_permission +from utils import get_super_admin + + +def default_permission(user_type: int): + if (user_type == settings.UserType.SUPERADMIN.value): + permissions_list = crud_user_permission.get_multi(db=SessionLocal(), limit=-1) + permissions_list = list(map(lambda x: x.id, permissions_list)) + return permissions_list + + if (user_type == settings.UserType.ADMIN.value): + permissions_list_name = crud_user_permission.get_multi(db=SessionLocal(), limit=-1) + permissions_list = list(map(lambda x: x.name, permissions_list_name)) + nono_list = [ + "personal_note_get", + "personal_note_post", + "personal_note_update", + "teacher_note_get", + "teacher_note_post", + "teacher_note_update", + ] + for item in nono_list: + permissions_list.pop(nono_list.index(item)) + permissions_list = list(map(lambda x: crud_user_permission.get_by_name(db=SessionLocal(), name=x), permissions_list)) + permissions_list = list(map(lambda x: x.id, permissions_list)) + return permissions_list + + if (user_type == settings.UserType.TEACHER.value): + yesyes_list = [ + "class_session_get_self", + "course_get", + "personal_note_post", + "personal_note_update", + "class_session_get", + "teacher_note_get_self", + "teacher_note_post", + "teacher_note_update_self", + "user_get_self", + "user_get_self", + ] + permissions_list = list(map(lambda x: crud_user_permission.get_by_name(db=SessionLocal(), name=x), yesyes_list)) + permissions_list = list(map(lambda x: x.id, permissions_list)) + return permissions_list + + if (user_type == settings.UserType.STUDENT.value): + permissions_list = crud_user_permission.get_multi(db=SessionLocal(), limit=-1) + permissions_list = list(map(lambda x: x.id, permissions_list)) + return permissions_list diff --git a/core/permission/permission.py b/core/permission/permission.py new file mode 100644 index 0000000000000000000000000000000000000000..a962b3e01c05b9f2513dabfae4a5dc5e1cb1a9a8 --- /dev/null +++ b/core/permission/permission.py @@ -0,0 +1,37 @@ +from functools import wraps + +from typing import Callable +from core.config import settings + + +def check_permission(func) -> Callable: + @wraps(func) + def inner_wrapper(*args, **kwargs): + # from cruds import crud_user + from cruds import crud_user_permission + + model_name = args[0].model.__name__ + req_user = kwargs.get("req_user") + + inner_func_name = func.__name__ + + if req_user.user_type == settings.UserType.SUPERADMIN.value: + return func(*args, **kwargs) + + else: + for permission in req_user.permission: + + permission_name = crud_user_permission.get_by_id(args[1], id=permission) + permission_name_sub = permission_name[-8:] + + if model_name == permission_name[: len(model_name) + 1]: + + operation = inner_func_name[0:4] + if operation == permission_name_sub: + return func(*args, **kwargs) + + operation = inner_func_name[0:7] + if operation == permission_name_sub: + return func(*args, **kwargs) + + return inner_wrapper diff --git a/core/security.py b/core/security.py new file mode 100644 index 0000000000000000000000000000000000000000..fb0b0f248f657d8c4f36b39d78074ac57fdf2400 --- /dev/null +++ b/core/security.py @@ -0,0 +1,138 @@ +import binascii +import json +import os +import secrets +from hashlib import sha1 + +from fastapi import Request +from passlib.context import CryptContext +from starlette.exceptions import HTTPException +from starlette.status import HTTP_102_PROCESSING, HTTP_404_NOT_FOUND, HTTP_425_TOO_EARLY + +from core.config import settings +from core.db import redis_session_client +from models import User + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +async def create_sesssion_token(user: User, remember_me: bool, request: Request) -> str: + session_token = secrets.token_hex(nbytes=16) + expire_time = ( + settings.SESSION_EXPIRE_TIME_EXTENDED + if remember_me + else settings.SESSION_EXPIRE_TIME + ) + + active_sessions = await redis_session_client.client.get( + f"user_{user.id}_sessions", encoding="utf-8" + ) + + if active_sessions: + active_sessions = json.loads(active_sessions) + else: + active_sessions = {"uid": user.id, "sessions": []} + + active_sessions["sessions"].append( + { + "token": session_token, + "ua": request.headers.get("user-agent"), + "ip": request.client.host, + } + ) + + data = { + session_token: user.id, + f"user_{user.id}_sessions": json.dumps(active_sessions), + } + + await redis_session_client.client.mset(data) + await redis_session_client.client.expire(session_token, expire_time) + return session_token + + +async def create_2fa_temp_token(user: User, remember_me: bool) -> str: + session_token = secrets.token_hex(nbytes=16) + + await redis_session_client.client.setex( + f"two_fa_temp_{session_token}", + settings.TWO_FA_TIMEOUT * 1000, + json.dumps({"user": user.id, "remember_me": remember_me}), + ) + + return session_token + + +async def create_passwordless_create_token() -> str: + token = secrets.token_hex(nbytes=16) + + await redis_session_client.client.setex( + f"password_less_{token}", + settings.PASSWORD_LESS_CREATE_TIMEOUT * 1000, + "-1", + ) + + return token + + +async def authorize_passwordless_token(user: User, token: str) -> bool: + value = await redis_session_client.client.get( + f"password_less_{token}", + ) + + if value == None: + raise HTTPException(status_code=HTTP_404_NOT_FOUND, + detail="Invalid token!") + elif int(value) == -1: + await redis_session_client.client.setex( + f"password_less_{token}", + settings.PASSWORD_LESS_CREATE_TIMEOUT * 1000, + user.id, + ) + return True + + +async def verify_passwordless_token(token: str) -> int: + value = (await redis_session_client.client.get( + f"password_less_{token}", + )).decode("UTF-8") + + if value == None: + raise HTTPException( + status_code=HTTP_404_NOT_FOUND, + detail="Invalid token!" + ) + + elif value == "-1": + raise HTTPException( + status_code=HTTP_425_TOO_EARLY, + detail="Waiting for authorization!" + ) + else: + await redis_session_client.client.delete( + f"password_less_{token}", + ) + return int(value) + + +async def create_2fa_enable_temp_token(user: User, totp_secret: str): + await redis_session_client.client.setex( + f"two_fa_enable_temp_{user.id}", + settings.TWO_FA_TIMEOUT * 1000, + totp_secret + ) + return + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +def get_uid_hash(uid: str) -> str: + hasher = sha1() + hasher.update(bytes(f"uid_{uid}", "utf-8")) + return hasher.hexdigest()[3:10] diff --git a/core/throttle.py b/core/throttle.py new file mode 100644 index 0000000000000000000000000000000000000000..9295c8805843d9c973804e444c63d668cf20ce89 --- /dev/null +++ b/core/throttle.py @@ -0,0 +1,96 @@ +from functools import wraps + +from core.db import redis_throttle_client +from fastapi import HTTPException +from typing import Callable + + +def ip_throttle(rate: int, per: int = 86400) -> Callable: + """ + Decorater used to throttle incoming requests based on IP Address + + Parameters + ---------- + rate : int, required + Rate value for the throttling. + + per : int, optional + Rate limit expiry time (in seconds) (default is 86400 ie. 1 day) + + + Raises + ------ + HTTPException(status_code=429) + If throttle limit is reached and the request is blocked + """ + + def outer_wrapper(func): + @wraps(func) + async def inner_wrapper(*args, **kwargs): + client_ip = kwargs.get( + "request" + ).client.host # FIXME - Proxy might mess this up. Might need to look X-HTTP-FORWARDED. Works locally + identifier = f"ip_th_{client_ip}_{func.__name__}_{per}" + current_count = await redis_throttle_client.client.get(identifier) + + if not current_count: + await redis_throttle_client.client.set(identifier, 1, expire=per) + + elif not int(current_count.decode("utf-8")) <= rate: + raise HTTPException( + status_code=429, detail="Error ID: 133" + ) # Too many requests! + + ret_val = await func(*args, **kwargs) + await redis_throttle_client.client.incr(identifier) + return ret_val + + return inner_wrapper + + return outer_wrapper + + +def user_throttle(rate: int, per: int = 86400) -> Callable: + """ + Decorater used to throttle incoming requests based on User + + Parameters + ---------- + rate : int, required + Rate value for the throttling. + + per : int, optional + Rate limit expiry time (in seconds) (default is 86400 ie. 1 day) + + + Raises + ------ + HTTPException(status_code=429) + If throttle limit is reached and the request is blocked + """ + + def outer_wrapper(func): + @wraps(func) + async def inner_wrapper(*args, **kwargs): + client = kwargs.get("current_user") + + if not client: + raise HTTPException( + status_code=429, detail="Error ID: 134" + ) # User not logged in! + + identifier = f"user_th_{client}_{func.__name__}" + current_count = await redis_throttle_client.client.get(identifier) + if current_count == None: + await redis_throttle_client.client.set(identifier, 1, ex=per) + elif int(current_count.decode("utf-8")) < rate: + await redis_throttle_client.client.incr(identifier, amount=1) + else: + raise HTTPException( + status_code=429, detail="Error ID: 135" + ) # Too many requests! + return await func(*args, **kwargs) + + return inner_wrapper + + return outer_wrapper diff --git a/core/websocket.py b/core/websocket.py new file mode 100644 index 0000000000000000000000000000000000000000..bd3f6df1fa3fd47d13679d884e939750ca649ba9 --- /dev/null +++ b/core/websocket.py @@ -0,0 +1,177 @@ +import enum +import json +from datetime import datetime +from typing import Dict, List, Optional + +from fastapi import WebSocket, websockets +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel + +from core.db.redis_session import redis_chat_client +from core.security import get_uid_hash +from models import User + + +class ChatMessageTypes(enum.Enum): + MESSAGE_HISTORY: int = 1 + PUBLIC_MESSAGE: int = 2 + ANON_MESSAGE: int = 3 + USER_JOINED: int = 4 + USER_LEFT: int = 5 + ACTIVE_USER_LIST: int = 6 + + +class Message(BaseModel): + msg_type: int + data: Optional[str] + user: Optional[str] + time: datetime + + +class WebSocketManager: + def __init__(self): + self.connections: Dict = {} + + async def update(self, data, key): + msg = await redis_chat_client.client.get(key) + if msg: + msg = json.loads(msg) + else: + msg = [] + msg.append(data) + await redis_chat_client.client.set( + key, json.dumps(msg, separators=(",", ":")), expire=60 * 60 * 1000 + ) + + async def send_history(self, websocket: WebSocket, class_session_id: int): + chat_history = await redis_chat_client.client.get( + f"chat_class_sess_{class_session_id}", encoding="UTF-8" + ) + + msg_history_instance = Message( + msg_type=ChatMessageTypes.MESSAGE_HISTORY.value, + data=chat_history, + time=datetime.utcnow(), + ) + + await websocket.send_json( + jsonable_encoder(msg_history_instance.dict(exclude_none=True)) + ) + + async def connect(self, websocket: WebSocket, user_id: int, class_session_id: int): + await websocket.accept() + try: + self.connections[class_session_id].append(websocket) + except: + self.connections.update({class_session_id: [websocket]}) + + msg_instance = Message( + msg_type=ChatMessageTypes.USER_JOINED.value, + time=datetime.utcnow(), + user=user_id, + ) + + # self.send_history(websocket=websocket, class_session_id=class_session_id) + + await self.broadcast( + msg_instance.dict(exclude_none=True), user_id, class_session_id, save=False + ) + + pre_status = await redis_chat_client.client.get( + f"active_status_{class_session_id}", encoding="UTF-8" + ) + + active_user_instance = Message( + msg_type=ChatMessageTypes.ACTIVE_USER_LIST.value, + data=pre_status, + time=datetime.utcnow(), + ) + + # print(active_user_instance.dict(exclude_none=True)) + await websocket.send_json( + jsonable_encoder(active_user_instance.dict(exclude_none=True)) + ) + + if not pre_status: + pre_status_obj = [] + else: + pre_status_obj = json.loads(pre_status) + + pre_status_obj.append(user_id) + pre_status_obj = list(set(pre_status_obj)) + await redis_chat_client.client.set( + f"active_status_{class_session_id}", + json.dumps(pre_status_obj, separators=(",", ":")), + ) + + await redis_chat_client.client.expire( + f"active_status_{class_session_id}", + 60 * 60 * 1000, + ) + + async def disconnect( + self, websocket: WebSocket, user_id: int, class_session_id: int + ): + self.connections[class_session_id].remove(websocket) + msg_instance = Message( + msg_type=ChatMessageTypes.USER_LEFT.value, + time=datetime.utcnow(), + user=user_id, + ) + + await self.broadcast(msg_instance, user_id, class_session_id, save=False) + pre_status = json.loads( + await redis_chat_client.client.get(f"active_status_{class_session_id}") + ) + + pre_status.remove(user_id) + await redis_chat_client.client.set( + f"active_status_{class_session_id}", + json.dumps(pre_status, separators=(",", ":")), + ) + + await redis_chat_client.client.expire( + f"active_status_{class_session_id}", + 60 * 60 * 1000, + ) + + async def broadcast( + self, data: any, user_id: int, class_session_id: int, save: bool = True + ): + encoded_data = jsonable_encoder(data) + for connection in self.connections.get(class_session_id): + try: + await connection.send_json(encoded_data) + except Exception as e: + pass + + if save: + await self.update(encoded_data, f"chat_class_sess_{class_session_id}") + + async def message( + self, + websocket: WebSocket, + message: str, + user_id: int, + class_session_id: int, + anon: bool = False, + ): + msg_type = ChatMessageTypes.PUBLIC_MESSAGE.value + user = user_id + + if anon: + msg_type = ChatMessageTypes.ANON_MESSAGE.value + user = get_uid_hash(user_id) + + msg_instance = Message( + msg_type=msg_type, + data=message, + user=user, + time=datetime.utcnow(), + ) + await self.broadcast( + msg_instance.dict(exclude_none=True), user_id, class_session_id + ) + + +ws = WebSocketManager() diff --git a/cruds/__init__.py b/cruds/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f8cf34da85583b04f2bc738ab57f8af7cf62e889 --- /dev/null +++ b/cruds/__init__.py @@ -0,0 +1,24 @@ +from .user import crud_user + +from .course import crud_course +from .school import crud_school +from .group import crud_group +from .department import crud_department +from .personal_note import crud_personal_note +from .program import crud_program +from .teacher_note import crud_teacher_note +from .class_session import crud_class_session +from .quiz_answer import crud_quiz_answer + +# For a new basic set of CRUD operations you could just do + +# from .base import CRUDBase +# from app.models.item import Item +# from app.schemas.item import ItemCreate, ItemUpdate + +# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item) + +from .quiz import crud_quiz, crud_question +from .file import crud_file +from .assignment import crud_assignment +from .assignment_upload import crud_assignment_upload diff --git a/cruds/__pycache__/__init__.cpython-310.pyc b/cruds/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..deeaa45f05b8da491d6c2c9875002bb4c740c6a7 Binary files /dev/null and b/cruds/__pycache__/__init__.cpython-310.pyc differ diff --git a/cruds/__pycache__/assignment.cpython-310.pyc b/cruds/__pycache__/assignment.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4bdb104d92090e1cf8ca4140e602412bf91992d Binary files /dev/null and b/cruds/__pycache__/assignment.cpython-310.pyc differ diff --git a/cruds/__pycache__/assignment_upload.cpython-310.pyc b/cruds/__pycache__/assignment_upload.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52a0fbec2df792b1d13dbfe75d931db6bd1a8294 Binary files /dev/null and b/cruds/__pycache__/assignment_upload.cpython-310.pyc differ diff --git a/cruds/__pycache__/base.cpython-310.pyc b/cruds/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6ec2baf4cbdb6971773bf27a4bb8e3cc999c223c Binary files /dev/null and b/cruds/__pycache__/base.cpython-310.pyc differ diff --git a/cruds/__pycache__/class_session.cpython-310.pyc b/cruds/__pycache__/class_session.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eae5d0e82c8499067630681e7b723e2643ab6e14 Binary files /dev/null and b/cruds/__pycache__/class_session.cpython-310.pyc differ diff --git a/cruds/__pycache__/course.cpython-310.pyc b/cruds/__pycache__/course.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d718a6c6dbe2b33a2143126b18c99085a6affc41 Binary files /dev/null and b/cruds/__pycache__/course.cpython-310.pyc differ diff --git a/cruds/__pycache__/department.cpython-310.pyc b/cruds/__pycache__/department.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15de73ad30ed595a860703c122697f53ccbee33b Binary files /dev/null and b/cruds/__pycache__/department.cpython-310.pyc differ diff --git a/cruds/__pycache__/file.cpython-310.pyc b/cruds/__pycache__/file.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c105bb69093dfd9659471947bac97031c00abe48 Binary files /dev/null and b/cruds/__pycache__/file.cpython-310.pyc differ diff --git a/cruds/__pycache__/group.cpython-310.pyc b/cruds/__pycache__/group.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1fbe5927e0caf6c11051070f4407e97d1c3639c4 Binary files /dev/null and b/cruds/__pycache__/group.cpython-310.pyc differ diff --git a/cruds/__pycache__/personal_note.cpython-310.pyc b/cruds/__pycache__/personal_note.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7741b117ddb3517b0cb33bd72ce90b0de2aa83b Binary files /dev/null and b/cruds/__pycache__/personal_note.cpython-310.pyc differ diff --git a/cruds/__pycache__/program.cpython-310.pyc b/cruds/__pycache__/program.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83888c547024596a17f42deb5777fa7cc0dc8551 Binary files /dev/null and b/cruds/__pycache__/program.cpython-310.pyc differ diff --git a/cruds/__pycache__/quiz.cpython-310.pyc b/cruds/__pycache__/quiz.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..06fbfe3eb3dbdea521e3319f68f4c940bc4db172 Binary files /dev/null and b/cruds/__pycache__/quiz.cpython-310.pyc differ diff --git a/cruds/__pycache__/quiz_answer.cpython-310.pyc b/cruds/__pycache__/quiz_answer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d538d684fc9586678c4da10e9a9d2197c68e2bf0 Binary files /dev/null and b/cruds/__pycache__/quiz_answer.cpython-310.pyc differ diff --git a/cruds/__pycache__/school.cpython-310.pyc b/cruds/__pycache__/school.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49fb75bc0dbfcbb673d658a95a36703a87c76224 Binary files /dev/null and b/cruds/__pycache__/school.cpython-310.pyc differ diff --git a/cruds/__pycache__/teacher_note.cpython-310.pyc b/cruds/__pycache__/teacher_note.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7c801be9f0ee5584e612c869f12067396e7f7fa Binary files /dev/null and b/cruds/__pycache__/teacher_note.cpython-310.pyc differ diff --git a/cruds/__pycache__/user.cpython-310.pyc b/cruds/__pycache__/user.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..343bc8c6a3bcad2299b4f08dff421adf65ba01e0 Binary files /dev/null and b/cruds/__pycache__/user.cpython-310.pyc differ diff --git a/cruds/assignment.py b/cruds/assignment.py new file mode 100644 index 0000000000000000000000000000000000000000..51b89a16ee6b3fecc2ca4bc0a7321bfc3fd7bc41 --- /dev/null +++ b/cruds/assignment.py @@ -0,0 +1,44 @@ +from cruds.base import CRUDBase +from schemas.assignment import AssignmentCreate, AssignmentUpdate +from models.assignment import Assignment +from sqlalchemy.orm import Session +from cruds import crud_user, crud_group +from models import User +from typing import Any + + +class CRUDAssignment(CRUDBase[Assignment, AssignmentCreate, AssignmentUpdate]): + def create(self, db: Session, *, obj_in: AssignmentCreate) -> Any: + if obj_in.instructor: + instructor = [crud_user.get(db=db, id=id) for id in obj_in.instructor] + else: + instructor = [] + + if obj_in.group: + group = [crud_group.get(db=db, id=id) for id in obj_in.group] + else: + group = [] + + db_obj = Assignment( + due_date=obj_in.due_date, + marks=obj_in.marks, + title=obj_in.title, + contents=obj_in.contents, + instructor=instructor, + group=group, + course_id=obj_in.course_id, + ) + + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_quiz_by_group_id(self, db: Session, *, group: int) -> Any: + return db.query(self.model).filter(self.model.group.contains(group)).all() + + def get_quiz_by_instructor_id(self, db: Session, *, user: User) -> Any: + return db.query(self.model).filter(self.model.instructor.contains(user)).all() + + +crud_assignment = CRUDAssignment(Assignment) diff --git a/cruds/assignment_upload.py b/cruds/assignment_upload.py new file mode 100644 index 0000000000000000000000000000000000000000..b60ad2aee54c61ea4f092009bbf7a892a3e182c0 --- /dev/null +++ b/cruds/assignment_upload.py @@ -0,0 +1,25 @@ +from cruds.base import CRUDBase +from schemas.assignment_upload import AssignmentUploadCreate, AssignmentUploadUpdate +from models.assignment_upload import AssignmentUpload +from sqlalchemy.orm import Session +from cruds import crud_user, crud_group +from models import User +from typing import Any + + +class CRUDAssignmentUpload( + CRUDBase[AssignmentUpload, AssignmentUploadCreate, AssignmentUploadUpdate] +): + def get_by_assignment_id(self, db: Session, *, assignmentId: int, studentId: int): + return ( + db.query(self.model) + .filter_by(assignment_id=assignmentId) + .filter_by(student_id=studentId) + .first() + ) + + def get_all_by_assignment_id_as_teacher(self, db: Session, *, assignmentId: int): + return db.query(self.model).filter_by(assignment_id=assignmentId).all() + + +crud_assignment_upload = CRUDAssignmentUpload(AssignmentUpload) diff --git a/cruds/base.py b/cruds/base.py new file mode 100644 index 0000000000000000000000000000000000000000..b0846ac199c1e9e5916385cd7248280ea4bcabbe --- /dev/null +++ b/cruds/base.py @@ -0,0 +1,95 @@ +from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union + +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel +from sqlalchemy.orm import Session + +from core.db import Base +from core.permission.permission import check_permission +from models import User + +ModelType = TypeVar("ModelType", bound=Base) +CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) +UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) + + +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + def __init__(self, model: Type[ModelType]): + """ + CRUD object with default methods to Create, Read, Update, Delete (CRUD). + + **Parameters** + + * `model`: A SQLAlchemy model class + * `schema`: A Pydantic model (schema) class + """ + self.model = model + + def get( + self, + db: Session, + id: Any, + ) -> Optional[ModelType]: + return db.query(self.model).filter(self.model.id == id).first() + + def get_self( + self, + db: Session, + ) -> Optional[ModelType]: + return db.query(self.model).filter(self.model.id == req_user.id).first() + + def get_multi( + self, + db: Session, + *, + skip: int = 0, + limit: int = 100, + ) -> List[ModelType]: + if limit == -1: + return db.query(self.model).offset(skip).all() + else: + return db.query(self.model).offset(skip).limit(limit).all() + + def create( + self, + db: Session, + *, + obj_in: CreateSchemaType, + ) -> ModelType: + obj_in_data = jsonable_encoder(obj_in) + db_obj = self.model(**obj_in_data) # type: ignore + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def update( + self, + db: Session, + *, + db_obj: ModelType, + obj_in: Union[UpdateSchemaType, Dict[str, Any]], + ) -> ModelType: + obj_data = jsonable_encoder(db_obj) + if isinstance(obj_in, dict): + update_data = obj_in + else: + update_data = obj_in.dict(exclude_unset=True) + for field in obj_data: + if field in update_data: + setattr(db_obj, field, update_data[field]) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def remove( + self, + db: Session, + *, + id: int, + ) -> ModelType: + obj = db.query(self.model).get(id) + db.delete(obj) + db.commit() + return obj diff --git a/cruds/class_session.py b/cruds/class_session.py new file mode 100644 index 0000000000000000000000000000000000000000..0904bb16253a77fd78d482ac6342ed25c4071aba --- /dev/null +++ b/cruds/class_session.py @@ -0,0 +1,75 @@ +from typing import Any, List + +from cruds.base import CRUDBase +from cruds.user import crud_user +from models import ClassSession +from models import User +from schemas import ClassSessionCreate, ClassSessionUpdate, AttendanceUpdate +from sqlalchemy.orm import Session +from core.config import settings + + +class CRUDClassSession(CRUDBase[ClassSession, ClassSessionCreate, ClassSessionUpdate]): + def create(self, db: Session, *, obj_in: ClassSessionCreate) -> Any: + if obj_in.instructor: + instructor = [crud_user.get(db=db, id=id) for id in obj_in.instructor] + + else: + instructor = [] + + db_obj = ClassSession( + start_time=obj_in.start_time, # noqa + end_time=obj_in.end_time, # noqa + instructor=instructor, # noqa + course_id=obj_in.course_id, # noqa + description=obj_in.description, # noqa + group_id=obj_in.group_id, + ) + + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def attendance_update( + self, db: Session, *, db_obj: ClassSession, obj_in: AttendanceUpdate + ) -> Any: + students = [crud_user.get_by_id(db=db, id=item) for item in obj_in.attendant] + db_obj.attendant = students + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def get_user_class_session( + self, db: Session, user: User, id: int = None + ) -> List[ClassSession]: + if user.user_type == settings.UserType.STUDENT.value: + class_sessions = db.query(self.model) + + if id: + class_sessions = class_sessions.filter(ClassSession.id == id) + + class_sessions = class_sessions.filter( + ClassSession.group_id == user.group_id + ) + + if id: + return class_sessions.first() + else: + return class_sessions.all() + + else: # if user.user_type == settings.UserType.TEACHER.value: + # TODO Fix ^^ + class_sessions = db.query(self.model).filter( + ClassSession.instructor.contains(user) + ) + if not id: + return class_sessions.all() + else: + return class_sessions.filter(ClassSession.id == id).first() + + return class_sessions + + +crud_class_session = CRUDClassSession(ClassSession) diff --git a/cruds/course.py b/cruds/course.py new file mode 100644 index 0000000000000000000000000000000000000000..26d9da1e18be7af7fac01014ff3e7ed77a48d259 --- /dev/null +++ b/cruds/course.py @@ -0,0 +1,10 @@ +from cruds.base import CRUDBase +from schemas.course import CourseCreate, CourseUpdate +from models.course import Course + + +class CRUDCourse(CRUDBase[Course, CourseCreate, CourseUpdate]): + pass + + +crud_course = CRUDCourse(Course) diff --git a/cruds/department.py b/cruds/department.py new file mode 100644 index 0000000000000000000000000000000000000000..c2e6cc030914b0fd352669ba683db6756a3d5ef7 --- /dev/null +++ b/cruds/department.py @@ -0,0 +1,10 @@ +from cruds.base import CRUDBase +from models.department import Department +from schemas.department import DepartmentCreate, DepartmentUpdate + + +class CRUDDepartment(CRUDBase[Department, DepartmentCreate, DepartmentUpdate]): + pass + + +crud_department = CRUDDepartment(Department) diff --git a/cruds/file.py b/cruds/file.py new file mode 100644 index 0000000000000000000000000000000000000000..3ad07a6814de36657351fb22b49324b60f01da21 --- /dev/null +++ b/cruds/file.py @@ -0,0 +1,10 @@ +from cruds.base import CRUDBase +from schemas.file import FileCreate, FileUpdate +from models.file import File + + +class CRUDFile(CRUDBase[File, FileCreate, FileUpdate]): + pass + + +crud_file = CRUDFile(File) diff --git a/cruds/group.py b/cruds/group.py new file mode 100644 index 0000000000000000000000000000000000000000..1e0a62ec015898b234cf23bddc85c1f4a3e96289 --- /dev/null +++ b/cruds/group.py @@ -0,0 +1,54 @@ +from cruds.base import CRUDBase +from models.group import Group +from schemas.group import GroupCreate, GroupUpdate +from schemas.group import Group as GroupSchema +from sqlalchemy.orm import Session +from cruds.course import crud_course +from fastapi.encoders import jsonable_encoder + + +class CRUDGroup(CRUDBase[Group, GroupCreate, GroupUpdate]): + def get_by_program_and_sem(self, db: Session, *, program: int, sem: int): + return ( + db.query(self.model) + .filter_by(program_id=program) + .filter_by(sem=sem) + .first() + ) + + def update( + self, + db: Session, + *, + db_obj: Group, + obj_in: GroupCreate, + ) -> Group: + if obj_in.course: + course = [crud_course.get(db=db, id=id) for id in obj_in.course] + else: + course = [] + db_obj.course = course + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + def create( + self, + db: Session, + *, + obj_in: GroupCreate, + ) -> Group: + if obj_in.course: + course = [crud_course.get(db=db, id=id) for id in obj_in.course] + else: + course = [] + + db_obj = Group(program_id=obj_in.program_id, sem=obj_in.sem, course=course) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + +crud_group = CRUDGroup(Group) diff --git a/cruds/personal_note.py b/cruds/personal_note.py new file mode 100644 index 0000000000000000000000000000000000000000..ec21e9b45826cb32b3116a9a392653573fadc361 --- /dev/null +++ b/cruds/personal_note.py @@ -0,0 +1,10 @@ +from cruds.base import CRUDBase +from schemas.personal_note import PersonalNoteCreate, PersonalNoteUpdate +from models.personal_note import PersonalNote + + +class CRUDPersonalNote(CRUDBase[PersonalNote, PersonalNoteCreate, PersonalNoteUpdate]): + pass + + +crud_personal_note = CRUDPersonalNote(PersonalNote) diff --git a/cruds/program.py b/cruds/program.py new file mode 100644 index 0000000000000000000000000000000000000000..88c7157ded10626f0baed01837eea373a6fdf71b --- /dev/null +++ b/cruds/program.py @@ -0,0 +1,10 @@ +from cruds.base import CRUDBase +from schemas.program import ProgramCreate, ProgramUpdate +from models.program import Program + + +class CRUDProgram(CRUDBase[Program, ProgramCreate, ProgramUpdate]): + pass + + +crud_program = CRUDProgram(Program) diff --git a/cruds/quiz.py b/cruds/quiz.py new file mode 100644 index 0000000000000000000000000000000000000000..d2c6394a704f923efe84afafdda15c19aba8f322 --- /dev/null +++ b/cruds/quiz.py @@ -0,0 +1,63 @@ +from cruds.base import CRUDBase +from schemas.quiz import QuizCreate, QuizUpdate, QuizQuestionCreate, QuizQuestionUpdate +from models.quiz import Quiz, QuizQuestion +from cruds import crud_user, crud_group +from typing import Any, Optional, List +from sqlalchemy.orm import Session + + +class CRUDQuiz(CRUDBase[Quiz, QuizCreate, QuizUpdate]): + def create(self, db: Session, *, obj_in: QuizCreate) -> Any: + if obj_in.instructor: + instructor = [crud_user.get(db=db, id=id) + for id in obj_in.instructor] + else: + instructor = [] + + if obj_in.group: + group = [crud_group.get(db=db, id=id) for id in obj_in.group] + else: + group = [] + + db_obj = Quiz( + end_time=obj_in.end_time, + start_time=obj_in.start_time, + title=obj_in.title, + description=obj_in.description, + is_randomized=obj_in.is_randomized, + display_individual=obj_in.display_individual, + group=group, + instructor=instructor, + course_id=obj_in.course_id, + total_marks=obj_in.total_marks, + ) + + db.add(db_obj) + db.commit() + db.refresh(db_obj) + return db_obj + + +crud_quiz = CRUDQuiz(Quiz) + + +class CRUDQuizQuestion(CRUDBase[QuizQuestion, QuizQuestionCreate, QuizQuestionUpdate]): + def get_all_by_quiz_id(self, db: Session, *, quiz_id: int) -> List[QuizQuestion]: + return ( + db.query(self.model) + .filter(self.model.quiz_id == quiz_id) + .order_by(self.model.id.asc()) + .all() + ) + + def get_by_quiz_id_question_id( + self, db: Session, *, quiz_id: int, questionid: int + ) -> QuizQuestion: + return ( + db.query(self.model) + .filter(self.model.quiz_id == quiz_id, self.model.id == questionid) + .first() + ) + + +crud_question = CRUDQuizQuestion(QuizQuestion) diff --git a/cruds/quiz_answer.py b/cruds/quiz_answer.py new file mode 100644 index 0000000000000000000000000000000000000000..54e6003483b89667b1694e88d859f19d65b641f8 --- /dev/null +++ b/cruds/quiz_answer.py @@ -0,0 +1,20 @@ +from cruds.base import CRUDBase +from schemas import QuizAnswerCreate, QuizAnswerUpdate +from models import QuizAnswer +from sqlalchemy.orm import Session + + +class CRUDQuizAnswer(CRUDBase[QuizAnswer, QuizAnswerCreate, QuizAnswerUpdate]): + def get_by_quiz_id(self, db: Session, *, quizId: int, studentId: int): + return ( + db.query(self.model) + .filter_by(quiz_id=quizId) + .filter_by(student_id=studentId) + .first() + ) + + def get_all_by_quiz_id_as_teacher(self, db: Session, *, quizId: int): + return db.query(self.model).filter_by(quiz_id=quizId).all() + + +crud_quiz_answer = CRUDQuizAnswer(QuizAnswer) diff --git a/cruds/school.py b/cruds/school.py new file mode 100644 index 0000000000000000000000000000000000000000..b54c623583235d735c3656f75325bcf9658f49d9 --- /dev/null +++ b/cruds/school.py @@ -0,0 +1,10 @@ +from cruds.base import CRUDBase +from schemas.school import SchoolCreate, SchoolUpdate +from models.school import School + + +class CRUDSchool(CRUDBase[School, SchoolCreate, SchoolUpdate]): + pass + + +crud_school = CRUDSchool(School) diff --git a/cruds/teacher_note.py b/cruds/teacher_note.py new file mode 100644 index 0000000000000000000000000000000000000000..aac9da9ec94a1d07b111447dd9dd08a831fab89d --- /dev/null +++ b/cruds/teacher_note.py @@ -0,0 +1,35 @@ +from cruds.base import CRUDBase +from schemas import TeacherNoteCreate, TeacherNoteUpdate +from schemas.teacher_note import TeacherNoteBase as TeacherNoteSchema +from models.teacher_note import TeacherNote +from typing import Any, List + +from cruds.base import CRUDBase +from cruds.user import crud_user +from models import ClassSession +from models import User +from schemas import ClassSessionCreate, ClassSessionUpdate +from sqlalchemy.orm import Session +from core.config import settings + + +class CRUDTeacherNote(CRUDBase[TeacherNote, TeacherNoteCreate, TeacherNoteUpdate]): + def create_with_user(self, db: Session, *, obj_in: TeacherNoteCreate, user: User): + obj_in = TeacherNoteSchema(user_id=user.id, **obj_in) + return self.create(db=db, obj_in=obj_in) + + def get_user_teacher_note(self, db: Session, user: User, id: int = None): + teacher_notes = db.query(self.model) + if id: + teacher_notes = ( + teacher_notes.filter(TeacherNote.id == id) + .filter(TeacherNote.user_id == user.id) + .first() + ) + return teacher_notes + else: + teacher_notes = teacher_notes.filter(TeacherNote.user_id == user.id).all() + return teacher_notes + + +crud_teacher_note = CRUDTeacherNote(TeacherNote) diff --git a/cruds/user.py b/cruds/user.py new file mode 100644 index 0000000000000000000000000000000000000000..76daa1adc93b4d5d7a2b672c0ec52d5d1e06f4a2 --- /dev/null +++ b/cruds/user.py @@ -0,0 +1,137 @@ +from typing import Any, Dict, Optional, Union + +from fastapi import HTTPException +from sqlalchemy.orm import Session + +from core.config import settings +from core.permission.permission import check_permission +from core.security import get_password_hash, verify_password +from cruds.base import CRUDBase +from cruds.group import crud_group +from models import association_tables +from models.user import User +from schemas.user import UserCreate, UserUpdate +from models.association_tables import TeacherGroupCourseAssociation + + +class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): + def get_by_email(self, db: Session, *, email: str) -> Optional[User]: + return db.query(User).filter(User.email == email).first() + + def get_by_email_test( + self, + db: Session, + *, + email: str, + ) -> Optional[User]: + return db.query(User).filter(User.email == email).first() + + def get_by_id(self, db: Session, *, id: int) -> Optional[User]: + return db.query(User).filter(User.id == id).first() + + def create( + self, + db: Session, + *, + obj_in: UserCreate, + ) -> User: + db_obj = User( + email=obj_in.email, # noqa + hashed_password=get_password_hash(obj_in.password), # noqa + roll=obj_in.roll, + full_name=obj_in.full_name, # noqa + dob=obj_in.dob, # noqa + teacher_department_id=obj_in.teacher_department_id, # noqa + group_id=obj_in.group_id, # noqa + user_type=obj_in.user_type, # noqa + contact_number=obj_in.contact_number, # noqa + address=obj_in.address, # noqa + join_year=obj_in.join_year, # noqa + ) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + + if obj_in.teacher_group: + for item in obj_in.teacher_group: + association_obj = TeacherGroupCourseAssociation( + teacher_id=db_obj.id, + group_id=item[0], + course_id=item[1], + ) + db.add(association_obj) + db.commit() + db.refresh(association_obj) + # teacher_group = [ + # crud_group.get(db=db, id=id) for id in obj_in.teacher_group + # ] + db.refresh(db_obj) + return db_obj + + def verify_user( + self, + db: Session, + *, + db_obj: User, + ): + super().update(db=db, db_obj=db_obj, obj_in={"is_active": True}) + + def enable_2fa( + self, + db: Session, + *, + secret: str, + db_obj: User, + ): + super().update(db=db, db_obj=db_obj, obj_in={"two_fa_secret": secret}) + + def disable_2fa( + self, + db: Session, + *, + db_obj: User, + ): + super().update(db=db, db_obj=db_obj, obj_in={"two_fa_secret": None}) + + def update( + self, + db: Session, + *, + db_obj: User, + obj_in: Union[UserUpdate, Dict[str, Any]], + ) -> User: + if isinstance(obj_in, dict): + update_data = obj_in + else: + update_data = obj_in.dict(exclude_unset=True) + + if "password" in update_data: + hashed_password = get_password_hash(update_data["password"]) + del update_data["password"] + update_data["hashed_password"] = hashed_password + # if ( + # update_data.get("permissions") + # and db_obj.user_type > settings.UserType.ADMIN.value + # ): + # raise HTTPException(403, detail="Error ID: 136") # Request denied + return super().update(db, db_obj=db_obj, obj_in=update_data) + + def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]: + user = self.get_by_email(db, email=email) + if not user: + return None + if not verify_password(password, user.hashed_password): + return None + return user + + def is_active(self, user: User) -> bool: + return user.is_active + + def is_superuser(self, user: User) -> bool: + if user.user_type == settings.UserType.SUPERADMIN.value: + return True + else: + return False + + +crud_user = CRUDUser(User) diff --git a/cruds/user_permission.py b/cruds/user_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..8fddecc627316f40a177bdfdcfdffccda3934e60 --- /dev/null +++ b/cruds/user_permission.py @@ -0,0 +1,17 @@ +from cruds.base import CRUDBase +from schemas.user_permission import UserPermissionCreate, UserPermissionUpdate +from models.user_permission import UserPermission +from models import User +from sqlalchemy.orm import Session + + +class CRUDUserPermission( + CRUDBase[UserPermission, UserPermissionCreate, UserPermissionUpdate] +): + def get_by_name( + self, db: Session, *, name: str + ): + return db.query(self.model).filter(self.model.name == name).first() + + +crud_user_permission = CRUDUserPermission(UserPermission) diff --git a/etc/base.yml b/etc/base.yml new file mode 100644 index 0000000000000000000000000000000000000000..4891cea8e36512700dfcafe0a8567564ce4d1d76 --- /dev/null +++ b/etc/base.yml @@ -0,0 +1,57 @@ +MODE: dev +PROJECT_NAME: MyProject +SERVER_NAME: MyServer + +#PROTOCOL: https +BACKEND_HOST: localhost +BACKEND_PORT: 8080 +API_V1_STR: "/api/v1" + +WORKERS: 8 + +FIRST_SUPERUSER: "ashwin.rachha@gmail.com" +FIRST_SUPERUSER_PASSWORD: "123456" + +SECRET_KEY: 28472B4B6250655368566D5971337436763979244226452948404D635166546A + +SESSION_EXPIRE_TIME: 3600 +SESSION_EXPIRE_TIME_EXTENDED: 2592000 +TWO_FA_TIMEOUT: 12000 +PASSWORD_LESS_CREATE_TIMEOUT: 360 + +FRONTEND_HOST: localhost +FRONTEND_PORT: 3001 + +STATIC_HOST: localhost +STATIC_PORT: 8081 + +UPLOAD_DIR_ROOT: "../file_server" + +BACKEND_CORS_ORIGINS: + - http://localhost:3001 + - http://localhost + +ALLOWED_EMAIL_HOST: + - gmail.com + +POSTGRES_SERVER: localhost +POSTGRES_USER: postadmin +POSTGRES_PASSWORD: postpass +POSTGRES_DB: siksalaya + +REDIS_HOST : localhost +REDIS_PORT : 6379 +REDIS_PASSWORD : redispass + +SMTP_TLS: False +SMTP_PORT: 1025 +SMTP_HOST: localhost +SMTP_USER: smptuser +SMTP_PASSWORD: smtppass + +EMAILS_FROM_EMAIL: "ashwin.rachha@gmail.com" +EMAIL_RESET_TOKEN_EXPIRE_HOURS: 48 +EMAIL_VERIFY_EXPIRE_HOURS: 48 +EMAIL_TEMPLATES_DIR: templates/email-templates/ + +USERS_OPEN_REGISTRATION: True diff --git a/etc/dev.yml b/etc/dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..eada2886af35736177929554c717863402ab2fe3 --- /dev/null +++ b/etc/dev.yml @@ -0,0 +1,59 @@ +MODE: dev +PROJECT_NAME : Sikshyalaya +SERVER_NAME : Sikshyalaya + +#PROTOCAL : http +BACKEND_HOST : localhost +BACKEND_PORT : 8000 +API_V1_STR : "/api/v1" + +WORKERS : 16 + +FIRST_SUPERUSER : "ashwin.rachha@gmail.com" +FIRST_SUPERUSER_PASSWORD : "password" + +SECRET_KEY : 28472B4B6250655368566D5971337436763979244226452948404D635166546A + +SESSION_EXPIRE_TIME : 3600 +SESSION_EXPIRE_TIME_EXTENDED : 2592000 + +FRONTEND_HOST : localhost +FRONTEND_PORT : 3001 + +STATIC_HOST : localhost +STATIC_PORT : 8081 + +UPLOAD_DIR_ROOT : ../file_server + +BACKEND_CORS_ORIGINS : + - http://localhost:3001 + - http://localhost:3000 + - http://localhost + +ALLOWED_EMAIL_HOST: + - ku.edu.np + - student.ku.edu.np + - gmail.com + - test.local + +POSTGRES_SERVER: localhost +POSTGRES_USER: postadmin +POSTGRES_PASSWORD: postpass +POSTGRES_DB: siksalaya + +REDIS_HOST : localhost +REDIS_PORT : 6379 +REDIS_PASSWORD : "" + +SMTP_TLS : False +SMTP_PORT : 1025 +SMTP_HOST : localhost +SMTP_USER : smptuser +SMTP_PASSWORD : smtppass + +EMAILS_FROM_EMAIL : "ashwin.rachha@gmail.com" +EMAIL_RESET_TOKEN_EXPIRE_HOURS : 48 +EMAIL_VERIFY_EXPIRE_HOURS : 48 +EMAIL_TEMPLATES_DIR : templates/email-templates/ + +USERS_OPEN_REGISTRATION : True diff --git a/etc/dock.yml b/etc/dock.yml new file mode 100644 index 0000000000000000000000000000000000000000..9950e4daa2fab850b9d3653481441da7cce87334 --- /dev/null +++ b/etc/dock.yml @@ -0,0 +1,58 @@ +MODE: docker +PROJECT_NAME : Sikshyalaya +SERVER_NAME : Sikshyalaya + +PROTOCAL : http +BACKEND_HOST : localhost +BACKEND_PORT : 8080 +API_V1_STR : "/api/v1" + +WORKERS : 16 + +FIRST_SUPERUSER : "admin@sikshyalaya.local" +FIRST_SUPERUSER_PASSWORD : "password" + +SECRET_KEY : 28472B4B6250655368566D5971337436763979244226452948404D635166546A + +SESSION_EXPIRE_TIME : 3600 +SESSION_EXPIRE_TIME_EXTENDED : 2592000 + +FRONTEND_HOST : localhost +FRONTEND_PORT : 3001 + +STATIC_HOST : localhost +STATIC_PORT : 8081 + +UPLOAD_DIR_ROOT : ../file_server + +BACKEND_CORS_ORIGINS : + - http://localhost:3001 + - http://localhost + +ALLOWED_EMAIL_HOST: + - ku.edu.np + - student.ku.edu.np + - gmail.com + - test.local + +POSTGRES_SERVER : postgres +POSTGRES_USER : postadmin +POSTGRES_PASSWORD : postpass +POSTGRES_DB : sikshyalaya + +REDIS_HOST : redis +REDIS_PORT : 6379 +REDIS_PASSWORD : redispass + +SMTP_TLS : False +SMTP_PORT : 1025 +SMTP_HOST : mailhog +SMTP_USER : smptuser +SMTP_PASSWORD : smtppass + +EMAILS_FROM_EMAIL : "noreply@app.local" +EMAIL_RESET_TOKEN_EXPIRE_HOURS : 48 +EMAIL_VERIFY_EXPIRE_HOURS : 48 +EMAIL_TEMPLATES_DIR : templates/email-templates/ + +USERS_OPEN_REGISTRATION : True diff --git a/forms/__pycache__/class_session.cpython-310.pyc b/forms/__pycache__/class_session.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..577ac9775de493e27546903dc6755f43b483f82e Binary files /dev/null and b/forms/__pycache__/class_session.cpython-310.pyc differ diff --git a/forms/class_session.py b/forms/class_session.py new file mode 100644 index 0000000000000000000000000000000000000000..478f3a3941d3dd13bde12202382b5b73ad3b9fbb --- /dev/null +++ b/forms/class_session.py @@ -0,0 +1,27 @@ +from fastapi import FastAPI, File, Form, UploadFile +from schemas.class_session import ClassSessionCreate +from typing import List, Optional +from datetime import datetime + + +class ClassSessionCreateForm: + def __init__( + self, + start_time: datetime = Form(...), + end_time: datetime = Form(...), + instructor: Optional[str] = Form(None), + group: int = Form(...), + description: str = Form(...), + file: Optional[List[UploadFile]] = File(None), + ): + self.start_time = start_time + self.end_time = end_time + + if instructor: + self.instructor = instructor.split(",") + else: + self.instructor = [] + + self.group = group + self.description = description + self.file = file diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 0000000000000000000000000000000000000000..0ce30776e92903441fc07551865becd5b83795c4 --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,7 @@ +from core import settings + +workers = settings.WORKERS +bind = f"{settings.BACKEND_HOST}:{settings.BACKEND_PORT}" +worker_tmp_dir = "/dev/shm" +graceful_timeout = 15 +timeout = 30 diff --git a/manage.py b/manage.py new file mode 100644 index 0000000000000000000000000000000000000000..d390bb787b01bec03878b8adc31fda171b1eba95 --- /dev/null +++ b/manage.py @@ -0,0 +1,308 @@ +import os +import sys +from time import sleep +import click + +# RCOUNT: 145 + + +class CommandDefinition: + def generator(self, name, det): # noqa + from utils import generator + + if det == "all": + if not name: + print("Enter at least one name!") + for item in name: + generator.create_model(item) + generator.create_endpoint(item) + generator.create_crud(item) + generator.create_schema(item) + + elif det == "model": + if not name: + print("Enter at least one model name!") + for item in name: + generator.create_model(item) + + elif det == "schema": + if not name: + print("Enter at least one schema name!") + for item in name: + generator.create_schema(item) + + elif det == "endpoint": + if not name: + print("Enter at least one endpoint name!") + for item in name: + generator.create_endpoint(item) + + elif det == "crud": + if not name: + print("Enter at least one CRUD name!") + for item in name: + generator.create_crud(item) + + def start(self): + from misc.scripts import launch + + launch.run() + + def mkmig(self): + from alembic import command + from alembic.config import Config + + alembic_cfg = Config("alembic.ini") + + msg = input("Enter a message: ") + command.revision(config=alembic_cfg, autogenerate=True, message=msg) + click.echo("Inside migrate") + + def mig(self): + from alembic import command + from alembic.config import Config + + alembic_cfg = Config("alembic.ini") + command.upgrade(alembic_cfg, "head") + + def cleanmig(self): + for file in os.listdir("migrations/versions/"): + if file != ".keep": + if os.path.isfile(f"migrations/versions/{file}"): + os.remove(f"migrations/versions/{file}") + + def cleanredis(self): + from core.config import settings + + os.system( + f"docker-compose exec redis redis-cli -a {settings.SECRET_KEY} FLUSHALL" + ) + + def logs(self): + os.system(f"docker-compose logs -f -t") + + def remake(self): + self.cleandb() + self.populate() + + def remakeall(self): + from alembic import command + from alembic.config import Config + + try: + os.system(f"docker-compose down -v -t 5") + os.system( + f"cd .. && docker-compose up -d postgres redis pgadmin mailhog file_server" + ) + except Exception as e: + print(e) + + self.cleanmig() + + alembic_cfg = Config("alembic.ini") + + rev_created = False + + while True: + try: + if not rev_created: + command.revision( + config=alembic_cfg, autogenerate=True, message="Remake All" + ) + rev_created = True + + command.upgrade(alembic_cfg, "head") + break + except Exception as e: + print(e) + print("Waiting for containers to boot!") + sleep(3) + + try: + self.populate() + except Exception as e: + print(e) + + def dcstart(self): + os.system( + "cd .. && docker-compose up -d postgres file_server redis pgadmin mailhog" + ) + + def cleandb(self): + try: + from alembic import command + from alembic.config import Config + + from core.db import engine + + self.cleanmig() + engine.execute("DROP schema public CASCADE") + engine.execute("CREATE schema public") + alembic_cfg = Config("alembic.ini") + command.revision(config=alembic_cfg, autogenerate=True, message="cleandb") + command.upgrade(alembic_cfg, "head") + except Exception as e: + print(e) + + def populate(self): + from utils import populate as db_populate + + sleep(5) + db_populate.populate_all() + + def pytest(self): + ec = int( + os.system( + "pytest --verbose --color=yes tests/api/api_v1/") + / 256 + ) + sys.exit(ec) + + def build_push(self): + ec = int( + os.system( + "docker build -t registry.gitlab.com/arpandaze/sikshyalaya/backend . && docker push registry.gitlab.com/arpandaze/sikshyalaya/backend") + / 256 + ) + sys.exit(ec) + + +commands = CommandDefinition() + + +@click.group() +def main(): + pass + + +@click.group("create") +@click.pass_context +def create(context): + if not context.invoked_subcommand: + pass + + +@click.group("clean") +@click.pass_context +def clean(context): + if not context.invoked_subcommand: + pass + + +@click.command() +@click.argument("name", nargs=-1) +def all(name): # noqa + commands.generator(name, "all") + + +@click.command() +@click.argument("name", nargs=-1) +def model(name): + commands.generator(name, "model") + + +@click.command() +@click.argument("name", nargs=-1) +def schema(name): + commands.generator(name, "schema") + + +@click.command() +@click.argument("name", nargs=-1) +def endpoint(name): + commands.generator(name, "endpoint") + + +@click.command() +@click.argument("name", nargs=-1) +def crud(name): + commands.generator(name, "crud") + + +@click.command() +def start(): + commands.start() + + +@click.command() +def dcstart(): + commands.dcstart() + + +@click.command() +def pytest(): + commands.pytest() + +@click.command() +def build_push(): + commands.build_push() + + +@click.command() +def mkmig(): + commands.mkmig() + + +@click.command() +def mig(): + commands.mig() + + +@click.command(name="mig") +def clean_mig(): + commands.cleanmig() + + +@click.command() +def redis(): + commands.cleanredis() + + +@click.command() +def logs(): + commands.logs() + + +@click.command() +def remakeall(): + commands.remakeall() + + +@click.command() +def remake(): + commands.remake() + + +@click.command() +def db(): + commands.cleandb() + + +@click.command() +def populate(): + commands.populate() + + +main.add_command(create) +main.add_command(clean) +main.add_command(start) +main.add_command(mkmig) +main.add_command(mig) +main.add_command(populate) +main.add_command(logs) +main.add_command(remakeall) +main.add_command(remake) +main.add_command(dcstart) +main.add_command(pytest) +main.add_command(build_push) +clean.add_command(db) +clean.add_command(clean_mig) +clean.add_command(redis) +create.add_command(model) +create.add_command(schema) +create.add_command(crud) +create.add_command(endpoint) +create.add_command(all) + +if __name__ == "__main__": + main() diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000000000000000000000000000000000000..98e4f9c44effe479ed38c66ba922e7bcc672916f --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/__pycache__/env.cpython-310.pyc b/migrations/__pycache__/env.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e08656b64d75c20226b8721c24c0326a6ff6e24 Binary files /dev/null and b/migrations/__pycache__/env.cpython-310.pyc differ diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000000000000000000000000000000000000..dfbbdd095ff7abb6a8a218b33434982b68cdd63a --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,83 @@ +import os +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +import os +import sys + +# Get the absolute path to the project root directory +project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + +# Add the project root directory to the Python path +sys.path.insert(0, project_root) + +config = context.config + +fileConfig(config.config_file_name) + +from core.db import Base + +target_metadata = Base.metadata + + +def get_url(): + from core import settings + return settings.POSTGRES_DATABASE_URI + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + config_data = config.get_section(config.config_ini_section) + config_data["sqlalchemy.url"] = get_url() + + connectable = engine_from_config( + config_data, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000000000000000000000000000000000000..2c0156303a8df3ffdc9de87765bf801bf6bea4a5 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/.keep b/migrations/versions/.keep new file mode 100644 index 0000000000000000000000000000000000000000..71cb815fdd5273f32c6d0b75eaa616a3c4a941fd --- /dev/null +++ b/migrations/versions/.keep @@ -0,0 +1 @@ +KEEP: Migration fails without this folder \ No newline at end of file diff --git a/migrations/versions/__pycache__/c6c1acf911ad_migrations.cpython-310.pyc b/migrations/versions/__pycache__/c6c1acf911ad_migrations.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b78389eae4775956485102b73f9ea85c00b6769 Binary files /dev/null and b/migrations/versions/__pycache__/c6c1acf911ad_migrations.cpython-310.pyc differ diff --git a/migrations/versions/c6c1acf911ad_migrations.py b/migrations/versions/c6c1acf911ad_migrations.py new file mode 100644 index 0000000000000000000000000000000000000000..e7fc24cec2bc355d5c9cecb80c23a018c70d8dd9 --- /dev/null +++ b/migrations/versions/c6c1acf911ad_migrations.py @@ -0,0 +1,282 @@ +"""migrations + +Revision ID: c6c1acf911ad +Revises: +Create Date: 2023-07-29 04:34:07.378969 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c6c1acf911ad' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('school', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=128), nullable=False), + sa.Column('address', sa.String(length=64), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('userpermission', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_userpermission_name'), 'userpermission', ['name'], unique=True) + op.create_table('department', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=128), nullable=True), + sa.Column('school_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['school_id'], ['school.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('course', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('course_code', sa.String(), nullable=True), + sa.Column('course_name', sa.String(length=128), nullable=False), + sa.Column('course_credit', sa.SmallInteger(), nullable=True), + sa.Column('department_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['department_id'], ['department.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_course_course_code'), 'course', ['course_code'], unique=True) + op.create_table('program', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=64), nullable=True), + sa.Column('department_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['department_id'], ['department.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('assignment', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('due_date', sa.DateTime(), nullable=True), + sa.Column('marks', sa.Integer(), nullable=True), + sa.Column('title', sa.String(length=2048), nullable=True), + sa.Column('contents', sa.String(length=32168), nullable=True), + sa.Column('files', sa.ARRAY(sa.JSON()), nullable=True), + sa.Column('course_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['course_id'], ['course.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('group', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('program_id', sa.Integer(), nullable=True), + sa.Column('sem', sa.SmallInteger(), nullable=True), + sa.ForeignKeyConstraint(['program_id'], ['program.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('quiz', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('end_time', sa.DateTime(), nullable=True), + sa.Column('start_time', sa.DateTime(), nullable=True), + sa.Column('title', sa.String(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.Column('is_randomized', sa.Boolean(), nullable=True), + sa.Column('display_individual', sa.Boolean(), nullable=True), + sa.Column('total_marks', sa.Integer(), nullable=True), + sa.Column('course_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['course_id'], ['course.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('assignment_group_association', + sa.Column('group_id', sa.Integer(), nullable=True), + sa.Column('assignment_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['assignment_id'], ['assignment.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ondelete='CASCADE') + ) + op.create_table('class_session', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('start_time', sa.DateTime(), nullable=True), + sa.Column('end_time', sa.DateTime(), nullable=True), + sa.Column('course_id', sa.Integer(), nullable=True), + sa.Column('group_id', sa.Integer(), nullable=True), + sa.Column('description', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['course_id'], ['course.id'], ondelete='cascade'), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('group_course_association', + sa.Column('course_id', sa.Integer(), nullable=True), + sa.Column('group_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['course_id'], ['course.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ondelete='CASCADE') + ) + op.create_table('group_quiz_association', + sa.Column('group_id', sa.Integer(), nullable=True), + sa.Column('quiz_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['quiz_id'], ['quiz.id'], ondelete='CASCADE') + ) + op.create_table('quiz_question', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('question_text', sa.String(), nullable=True), + sa.Column('question_image', sa.ARRAY(sa.String()), nullable=True), + sa.Column('options', sa.JSON(), nullable=False), + sa.Column('marks', sa.Integer(), nullable=True), + sa.Column('answer', sa.ARRAY(sa.Integer()), nullable=True), + sa.Column('quiz_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['quiz_id'], ['quiz.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('profile_image', sa.String(length=100), nullable=True), + sa.Column('full_name', sa.String(), nullable=True), + sa.Column('email', sa.String(), nullable=False), + sa.Column('two_fa_secret', sa.String(), nullable=True), + sa.Column('roll', sa.SmallInteger(), nullable=True), + sa.Column('group_id', sa.Integer(), nullable=True), + sa.Column('teacher_department_id', sa.Integer(), nullable=True), + sa.Column('dob', sa.Date(), nullable=False), + sa.Column('address', sa.String(length=128), nullable=False), + sa.Column('contact_number', sa.String(length=32), nullable=False), + sa.Column('hashed_password', sa.String(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('user_type', sa.SmallInteger(), nullable=False), + sa.Column('join_year', sa.SmallInteger(), nullable=True), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ondelete='cascade'), + sa.ForeignKeyConstraint(['teacher_department_id'], ['department.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_user_contact_number'), 'user', ['contact_number'], unique=False) + op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True) + op.create_index(op.f('ix_user_full_name'), 'user', ['full_name'], unique=False) + op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False) + op.create_index(op.f('ix_user_user_type'), 'user', ['user_type'], unique=False) + op.create_table('assignment_instructor_association', + sa.Column('instructor_id', sa.Integer(), nullable=True), + sa.Column('assignment_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['assignment_id'], ['assignment.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['instructor_id'], ['user.id'], ondelete='CASCADE') + ) + op.create_table('assignment_upload', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('submission_date', sa.DateTime(), nullable=True), + sa.Column('marks_obtained', sa.Integer(), nullable=True), + sa.Column('files', sa.ARRAY(sa.JSON()), nullable=True), + sa.Column('assignment_id', sa.Integer(), nullable=True), + sa.Column('student_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['assignment_id'], ['assignment.id'], ondelete='cascade'), + sa.ForeignKeyConstraint(['student_id'], ['user.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('assignment_id', 'student_id', name='__student_assignment_uc') + ) + op.create_table('attendant_class_session_association', + sa.Column('class_session_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['class_session_id'], ['class_session.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE') + ) + op.create_table('file', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=True), + sa.Column('path', sa.String(), nullable=True), + sa.Column('file_type', sa.String(), nullable=True), + sa.Column('uploaded_datetime', sa.DateTime(), nullable=True), + sa.Column('class_session_id', sa.Integer(), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['class_session_id'], ['class_session.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('instructor_quiz_association', + sa.Column('instructor_id', sa.Integer(), nullable=True), + sa.Column('quiz_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['instructor_id'], ['user.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['quiz_id'], ['quiz.id'], ondelete='CASCADE') + ) + op.create_table('personalnote', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('last_updated_time', sa.DateTime(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('tags', sa.ARRAY(sa.String(length=32)), nullable=True), + sa.Column('title', sa.String(length=128), nullable=True), + sa.Column('content', sa.String(length=32768), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('quiz_answer', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('marks_obtained', sa.Integer(), nullable=True), + sa.Column('options_selected', sa.JSON(), nullable=True), + sa.Column('quiz_id', sa.Integer(), nullable=True), + sa.Column('student_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['quiz_id'], ['quiz.id'], ondelete='cascade'), + sa.ForeignKeyConstraint(['student_id'], ['user.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('quiz_id', 'student_id', name='__student_quiz_uc') + ) + op.create_table('teacher_group_course_association', + sa.Column('teacher_id', sa.Integer(), nullable=False), + sa.Column('group_id', sa.Integer(), nullable=False), + sa.Column('course_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['course_id'], ['course.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['teacher_id'], ['user.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('teacher_id', 'group_id') + ) + op.create_table('teachernote', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('student_id', sa.Integer(), nullable=True), + sa.Column('message', sa.String(length=512), nullable=True), + sa.ForeignKeyConstraint(['student_id'], ['user.id'], ondelete='cascade'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='cascade'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user_class_session_association', + sa.Column('class_session_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['class_session_id'], ['class_session.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE') + ) + op.create_table('user_permission_association', + sa.Column('permission_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['permission_id'], ['userpermission.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user_permission_association') + op.drop_table('user_class_session_association') + op.drop_table('teachernote') + op.drop_table('teacher_group_course_association') + op.drop_table('quiz_answer') + op.drop_table('personalnote') + op.drop_table('instructor_quiz_association') + op.drop_table('file') + op.drop_table('attendant_class_session_association') + op.drop_table('assignment_upload') + op.drop_table('assignment_instructor_association') + op.drop_index(op.f('ix_user_user_type'), table_name='user') + op.drop_index(op.f('ix_user_id'), table_name='user') + op.drop_index(op.f('ix_user_full_name'), table_name='user') + op.drop_index(op.f('ix_user_email'), table_name='user') + op.drop_index(op.f('ix_user_contact_number'), table_name='user') + op.drop_table('user') + op.drop_table('quiz_question') + op.drop_table('group_quiz_association') + op.drop_table('group_course_association') + op.drop_table('class_session') + op.drop_table('assignment_group_association') + op.drop_table('quiz') + op.drop_table('group') + op.drop_table('assignment') + op.drop_table('program') + op.drop_index(op.f('ix_course_course_code'), table_name='course') + op.drop_table('course') + op.drop_table('department') + op.drop_index(op.f('ix_userpermission_name'), table_name='userpermission') + op.drop_table('userpermission') + op.drop_table('school') + # ### end Alembic commands ### diff --git a/misc/__init__.py b/misc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/misc/etc/gunicorn.conf.py b/misc/etc/gunicorn.conf.py new file mode 100644 index 0000000000000000000000000000000000000000..e8e2d494b5c5cb90f4faf84c7cec3f57aa53bc81 --- /dev/null +++ b/misc/etc/gunicorn.conf.py @@ -0,0 +1,7 @@ +import os + +workers = os.environ.get("GUNICORN_WORKERS", 9) +bind = "0.0.0.0:80" +worker_tmp_dir = "/dev/shm" +graceful_timeout = int(os.environ.get("UVICORN_GRACEFUL_TIMEOUT",15)) +timeout = int(os.environ.get("UVICORN_TIMEOUT",30)) diff --git a/misc/scripts/__init__.py b/misc/scripts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/misc/scripts/docker-entrypoint.sh b/misc/scripts/docker-entrypoint.sh new file mode 100644 index 0000000000000000000000000000000000000000..7dad8b5231d352a7481e296e7080a797974d8546 --- /dev/null +++ b/misc/scripts/docker-entrypoint.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec gunicorn -k uvicorn.workers.UvicornWorker -c gunicorn.conf.py misc.scripts.launch:app diff --git a/misc/scripts/launch.py b/misc/scripts/launch.py new file mode 100644 index 0000000000000000000000000000000000000000..1786622d784146cdda0505abd09215f766dd54a6 --- /dev/null +++ b/misc/scripts/launch.py @@ -0,0 +1,107 @@ +import uvicorn +import os +from fastapi import FastAPI +from fastapi.openapi.docs import ( + get_swagger_ui_html, + get_swagger_ui_oauth2_redirect_html, +) + +from starlette.middleware.cors import CORSMiddleware + +from core import settings +from core.db import ( + init, + redis_cache_client, + redis_chat_client, + redis_general, + redis_session_client, + redis_throttle_client, +) +from api import router + +import traceback +import sentry_sdk + +sentry_sdk.init( + dsn="https://bf53e20b6458412485a7977eccd8f0db@o4505591045357568.ingest.sentry.io/4505591049420800", + traces_sample_rate=1.0, +) + +app = FastAPI( + title=settings.PROJECT_NAME, + openapi_url=f"{settings.API_V1_STR}/openapi.json", + docs_url=None, +) + + +@app.on_event("startup") +async def startup(): + await redis_cache_client.initialize() + await redis_chat_client.initialize() + await redis_throttle_client.initialize() + await redis_session_client.initialize() + await redis_general.initialize() + init.init_db() + + +@app.on_event("shutdown") +async def shutdown(): + await redis_cache_client.close() + await redis_chat_client.close() + await redis_throttle_client.close() + await redis_session_client.close() + await redis_general.close() + + +@app.get("/docs", include_in_schema=False) +async def custom_swagger_ui_html(): + return get_swagger_ui_html( + openapi_url=app.openapi_url, + title=app.title + " - API Documentaion", + oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, + swagger_js_url=f"{settings.STATIC_URL_BASE}/static/swagger-ui-bundle.js", + swagger_css_url=f"{settings.STATIC_URL_BASE}/static/swagger-ui.css", + ) + + +@app.get(app.swagger_ui_oauth2_redirect_url, include_in_schema=False) +async def swagger_ui_redirect(): + return get_swagger_ui_oauth2_redirect_html() + + +if settings.BACKEND_CORS_ORIGINS: + app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + pass + +app.include_router(router, prefix=settings.API_V1_STR) + + +def run(): + reload_blacklist = ["tests", ".pytest_cache"] + reload_dirs = os.listdir() + + for dir in reload_blacklist: + try: + reload_dirs.remove(dir) + except: + pass + + uvicorn.run( + "misc.scripts.launch:app", + host=settings.BACKEND_HOST, + port=settings.BACKEND_PORT, + reload=settings.DEV_MODE, + reload_dirs=reload_dirs, + debug=settings.DEV_MODE, + workers=settings.WORKERS, + ) + + +if __name__ == "__main__": + run() diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..159ff8728c60d815563e53f4353b1390ef4bbff1 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,15 @@ +from .class_session import ClassSession # noqa: F401 +from .course import Course # noqa: F401 +from .department import Department # noqa: F401 +from .group import Group # noqa: F401 +from .personal_note import PersonalNote # noqa: F401 +from .program import Program # noqa: F401 +from .school import School # noqa: F401 +from .teacher_note import TeacherNote # noqa: F401 +from .user import User # noqa: F401 +from .user_permission import UserPermission +from .quiz import Quiz, QuizQuestion +from .quiz_answer import QuizAnswer # noqa: F401 +from .file import File +from .assignment import Assignment +from .assignment_upload import AssignmentUpload diff --git a/models/__pycache__/__init__.cpython-310.pyc b/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94c834c83ff5ad2d5207f23c264c618acb21e5ba Binary files /dev/null and b/models/__pycache__/__init__.cpython-310.pyc differ diff --git a/models/__pycache__/assignment.cpython-310.pyc b/models/__pycache__/assignment.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6da2c18d3deedc4c49e132368431debb078052b4 Binary files /dev/null and b/models/__pycache__/assignment.cpython-310.pyc differ diff --git a/models/__pycache__/assignment_upload.cpython-310.pyc b/models/__pycache__/assignment_upload.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..256dfdfb32cde829b35733d82d4117998761ad51 Binary files /dev/null and b/models/__pycache__/assignment_upload.cpython-310.pyc differ diff --git a/models/__pycache__/association_tables.cpython-310.pyc b/models/__pycache__/association_tables.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12c171b1d956659a9f861822289e7385ba0b5821 Binary files /dev/null and b/models/__pycache__/association_tables.cpython-310.pyc differ diff --git a/models/__pycache__/class_session.cpython-310.pyc b/models/__pycache__/class_session.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..521242ec1fb85fc45df3f0b573ddfdcd3a609caa Binary files /dev/null and b/models/__pycache__/class_session.cpython-310.pyc differ diff --git a/models/__pycache__/course.cpython-310.pyc b/models/__pycache__/course.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ef6a65ba617308b40c8de040b87899215a9721a Binary files /dev/null and b/models/__pycache__/course.cpython-310.pyc differ diff --git a/models/__pycache__/department.cpython-310.pyc b/models/__pycache__/department.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d664fc3a2c75480f74e7689e7d250f482fd5caa2 Binary files /dev/null and b/models/__pycache__/department.cpython-310.pyc differ diff --git a/models/__pycache__/file.cpython-310.pyc b/models/__pycache__/file.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6125a25a392523066cb2611a2309c94883cd6bf Binary files /dev/null and b/models/__pycache__/file.cpython-310.pyc differ diff --git a/models/__pycache__/group.cpython-310.pyc b/models/__pycache__/group.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33342d5ea51c7e9184a9ef0d254edf4c56170eeb Binary files /dev/null and b/models/__pycache__/group.cpython-310.pyc differ diff --git a/models/__pycache__/personal_note.cpython-310.pyc b/models/__pycache__/personal_note.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07a9691aa570fd964f36e358d62f5a44557d1fcc Binary files /dev/null and b/models/__pycache__/personal_note.cpython-310.pyc differ diff --git a/models/__pycache__/program.cpython-310.pyc b/models/__pycache__/program.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff04f7727fa423077ee47617e07bb0eebc9349f4 Binary files /dev/null and b/models/__pycache__/program.cpython-310.pyc differ diff --git a/models/__pycache__/quiz.cpython-310.pyc b/models/__pycache__/quiz.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0896ae5f6505d192b6f42b1965df3b9039c32dc Binary files /dev/null and b/models/__pycache__/quiz.cpython-310.pyc differ diff --git a/models/__pycache__/quiz_answer.cpython-310.pyc b/models/__pycache__/quiz_answer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0baab21f98d95d6bfc154eb4b4c94d61f1a41ba Binary files /dev/null and b/models/__pycache__/quiz_answer.cpython-310.pyc differ diff --git a/models/__pycache__/school.cpython-310.pyc b/models/__pycache__/school.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0fe37c395c1b2f9c145c77a1179e5fc54ebf0181 Binary files /dev/null and b/models/__pycache__/school.cpython-310.pyc differ diff --git a/models/__pycache__/teacher_note.cpython-310.pyc b/models/__pycache__/teacher_note.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f22cc4f8f286335cf3c997f3d8171da0dbe06ba Binary files /dev/null and b/models/__pycache__/teacher_note.cpython-310.pyc differ diff --git a/models/__pycache__/user.cpython-310.pyc b/models/__pycache__/user.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f090af7ef4fd457b763b037dd6a71970b919468 Binary files /dev/null and b/models/__pycache__/user.cpython-310.pyc differ diff --git a/models/__pycache__/user_permission.cpython-310.pyc b/models/__pycache__/user_permission.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d474385e5be6af6da5226edbee36bdd4372f6b0 Binary files /dev/null and b/models/__pycache__/user_permission.cpython-310.pyc differ diff --git a/models/assignment.py b/models/assignment.py new file mode 100644 index 0000000000000000000000000000000000000000..cbe723593325e8324169f7d1d8b4b432619c54a9 --- /dev/null +++ b/models/assignment.py @@ -0,0 +1,32 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import Column, Integer, String, DateTime, ARRAY, ForeignKey +from sqlalchemy.orm import relationship +from .association_tables import ( + assignment_group_association_table, + assignment_instructor_association_table, +) + +from sqlalchemy.sql.sqltypes import JSON + +from core.db import Base + +"""Assignments can have contents, files, due_date, marks, title, course, group, teacher,""" + + +class Assignment(Base): + id = Column(Integer, primary_key=True) + due_date = Column(DateTime, nullable=True) + marks = Column(Integer, nullable=True) + title = Column(String(length=2048)) + contents = Column(String(length=32168)) + files = Column(ARRAY(JSON), nullable=True) + instructor = relationship( + "User", secondary=assignment_instructor_association_table, backref="assignments" + ) + group = relationship( + "Group", secondary=assignment_group_association_table, backref="assignments" + ) + course_id = Column(Integer, ForeignKey("course.id", ondelete="cascade")) + course = relationship("Course", backref="assignment") + __tablename__ = "assignment" # noqa diff --git a/models/assignment_upload.py b/models/assignment_upload.py new file mode 100644 index 0000000000000000000000000000000000000000..a59790039d7d5cb22d9908b3d3c7461bb06857e1 --- /dev/null +++ b/models/assignment_upload.py @@ -0,0 +1,28 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import Column, Integer, String, DateTime, ARRAY, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql.schema import UniqueConstraint +from sqlalchemy.sql.sqltypes import JSON + + +from core.db import Base + + +class AssignmentUpload(Base): + id = Column(Integer, primary_key=True) + + submission_date = Column(DateTime, nullable=True) + marks_obtained = Column(Integer, nullable=True) + files = Column(ARRAY(JSON), nullable=True) + + assignment_id = Column(Integer, ForeignKey("assignment.id", ondelete="cascade")) + assignment = relationship("Assignment", backref="assignment_upload") + + student_id = Column(Integer, ForeignKey("user.id", ondelete="cascade")) + student = relationship("User", backref="assignment_upload") + __table_args__ = ( + UniqueConstraint("assignment_id", "student_id", name="__student_assignment_uc"), + ) + + __tablename__ = "assignment_upload" # noqa diff --git a/models/association_tables.py b/models/association_tables.py new file mode 100644 index 0000000000000000000000000000000000000000..2b7f31543b8651fcd46f2cbd45f1c6821cd55ce9 --- /dev/null +++ b/models/association_tables.py @@ -0,0 +1,101 @@ +from sqlalchemy import Column, Integer, ForeignKey, Table +from sqlalchemy.orm import relationship +from sqlalchemy.sql.expression import null + +from core.db import Base + +# XXX: previously needed for storing courses of user, now migrated to storing course in groups +# user_course_association_table = Table( +# "user_course_association", +# Base.metadata, +# Column("course_id", Integer, ForeignKey("course.id")), +# Column("user_id", Integer, ForeignKey("user.id")), +# ) + +user_class_session_association_table = Table( + "user_class_session_association", + Base.metadata, + Column( + "class_session_id", Integer, ForeignKey("class_session.id", ondelete="CASCADE") + ), + Column("user_id", Integer, ForeignKey("user.id", ondelete="CASCADE")), +) + +attendant_class_session_association_table = Table( + "attendant_class_session_association", + Base.metadata, + Column( + "class_session_id", Integer, ForeignKey("class_session.id", ondelete="CASCADE") + ), + Column("user_id", Integer, ForeignKey("user.id", ondelete="CASCADE")), +) + +user_permission_association_table = Table( + "user_permission_association", + Base.metadata, + Column( + "permission_id", Integer, ForeignKey("userpermission.id", ondelete="CASCADE") + ), + Column("user_id", Integer, ForeignKey("user.id", ondelete="CASCADE")), +) + +group_course_association_table = Table( + "group_course_association", + Base.metadata, + Column("course_id", Integer, ForeignKey("course.id", ondelete="CASCADE")), + Column("group_id", Integer, ForeignKey("group.id", ondelete="CASCADE")), +) + +# teacher_group_association_table = Table( +# "teacher_group_association", +# Base.metadata, +# Column("teacher_id", Integer, ForeignKey("user.id", ondelete="CASCADE")), +# Column("group_id", Integer, ForeignKey("group.id", ondelete="CASCADE")), +# ) + + +class TeacherGroupCourseAssociation(Base): + teacher_id = Column( + Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True + ) + group_id = Column( + Integer, ForeignKey("group.id", ondelete="CASCADE"), primary_key=True + ) + course_id = Column( + Integer, ForeignKey("course.id", ondelete="CASCADE"), nullable=False + ) + group = relationship("Group") + teacher = relationship("User") + course = relationship("Course") + __tablename__ = "teacher_group_course_association" + + +group_quiz_association_table = Table( + "group_quiz_association", + Base.metadata, + Column("group_id", Integer, ForeignKey("group.id", ondelete="CASCADE")), + Column("quiz_id", Integer, ForeignKey("quiz.id", ondelete="CASCADE")), +) + + +instructor_quiz_association_table = Table( + "instructor_quiz_association", + Base.metadata, + Column("instructor_id", Integer, ForeignKey("user.id", ondelete="CASCADE")), + Column("quiz_id", Integer, ForeignKey("quiz.id", ondelete="CASCADE")), +) + + +assignment_group_association_table = Table( + "assignment_group_association", + Base.metadata, + Column("group_id", Integer, ForeignKey("group.id", ondelete="CASCADE")), + Column("assignment_id", Integer, ForeignKey("assignment.id", ondelete="CASCADE")), +) + +assignment_instructor_association_table = Table( + "assignment_instructor_association", + Base.metadata, + Column("instructor_id", Integer, ForeignKey("user.id", ondelete="CASCADE")), + Column("assignment_id", Integer, ForeignKey("assignment.id", ondelete="CASCADE")) +) \ No newline at end of file diff --git a/models/class_session.py b/models/class_session.py new file mode 100644 index 0000000000000000000000000000000000000000..ae49139f7a39090da7a24c8e3afe5dfcb2c83205 --- /dev/null +++ b/models/class_session.py @@ -0,0 +1,28 @@ +from sqlalchemy import Boolean, Column, Integer, String, DateTime, ForeignKey, ARRAY +from sqlalchemy.orm import relationship + +from core.db import Base +from .association_tables import ( + user_class_session_association_table, + attendant_class_session_association_table, +) + + +class ClassSession(Base): + id = Column(Integer, primary_key=True) + start_time = Column(DateTime) + end_time = Column(DateTime) + instructor = relationship( + "User", secondary=user_class_session_association_table, backref="class_session" + ) + course_id = Column(Integer, ForeignKey("course.id", ondelete="cascade")) + course = relationship("Course", backref="session") + group_id = Column(Integer, ForeignKey("group.id", ondelete="cascade")) + group = relationship("Group", backref="class_session", uselist=False) + description = Column(String) + attendant = relationship( + "User", + secondary=attendant_class_session_association_table, + backref="attended_class_session", + ) + __tablename__ = "class_session" diff --git a/models/course.py b/models/course.py new file mode 100644 index 0000000000000000000000000000000000000000..a1a36e98279fe2b8893442292397f7937649f55e --- /dev/null +++ b/models/course.py @@ -0,0 +1,14 @@ +from sqlalchemy.sql.sqltypes import SmallInteger +from core.db import Base +from sqlalchemy import Column, Integer, String, ForeignKey, SmallInteger +from sqlalchemy.orm import relationship + + +class Course(Base): + id = Column(Integer, primary_key=True) + course_code = Column(String, index=True, unique=True) + course_name = Column(String(128), nullable=False) + course_credit = Column(SmallInteger) + department_id = Column(Integer, ForeignKey("department.id", ondelete="cascade")) + department = relationship("Department", backref="courses") + __tablename__ = "course" diff --git a/models/department.py b/models/department.py new file mode 100644 index 0000000000000000000000000000000000000000..4f270d5117daffa7e705cf595aee839f552bb793 --- /dev/null +++ b/models/department.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship + +from core.db import Base + + +class Department(Base): + id = Column(Integer, primary_key=True) + name = Column(String(length=128)) + school_id = Column( + Integer, + ForeignKey("school.id", ondelete="CASCADE"), nullable=True, + ) + school = relationship("School", backref="departments", passive_deletes=True) + __tablename__ = "department" diff --git a/models/file.py b/models/file.py new file mode 100644 index 0000000000000000000000000000000000000000..06c455198b620b30da9360a6d156fbf48ceff2b8 --- /dev/null +++ b/models/file.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from sqlalchemy.sql.sqltypes import Enum + +from core.db import Base +from datetime import datetime + + +class File(Base): + id = Column(Integer, primary_key=True) + name = Column(String) + path = Column(String) + file_type = Column(String) + uploaded_datetime = Column(DateTime, default=func.now()) + class_session_id = Column( + Integer, + ForeignKey("class_session.id", ondelete="CASCADE"), + nullable=False, + ) + class_session = relationship("ClassSession", backref="files", passive_deletes=True) + description = Column(String) + __tablename__ = "file" # noqa diff --git a/models/group.py b/models/group.py new file mode 100644 index 0000000000000000000000000000000000000000..353402f88eaf66cfce3ed9db7929ea9ca15be243 --- /dev/null +++ b/models/group.py @@ -0,0 +1,19 @@ +from core.db import Base +from sqlalchemy import Column, Integer, ForeignKey, SmallInteger +from sqlalchemy.orm import relationship +from .association_tables import ( + group_course_association_table, + group_quiz_association_table, +) + + +class Group(Base): + id = Column(Integer, primary_key=True) + program_id = Column(Integer, ForeignKey("program.id", ondelete="cascade")) + program = relationship("Program", backref="groups") + sem = Column(SmallInteger) + course = relationship( + "Course", secondary=group_course_association_table, backref="groups" + ) + + __tablename__ = "group" diff --git a/models/personal_note.py b/models/personal_note.py new file mode 100644 index 0000000000000000000000000000000000000000..1a3a62c1b59e54cee523402f5b101223e10f5567 --- /dev/null +++ b/models/personal_note.py @@ -0,0 +1,22 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import Column, Integer, String, ForeignKey, ARRAY, DateTime +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from core.db import Base + + +if TYPE_CHECKING: + from .user import User # noqa: F401 + from .course import Course # noqa: F401 + + +class PersonalNote(Base): + id = Column(Integer, primary_key=True) + last_updated_time = Column(DateTime, default=func.now(), onupdate=func.now()) + user_id = Column(Integer, ForeignKey("user.id", ondelete="cascade")) + user = relationship("User", backref="personalnote", foreign_keys=[user_id]) + tags = Column(ARRAY(String(length=32))) + title = Column(String(length=128)) + content = Column(String(length=32768)) + __tablename__ = "personalnote" diff --git a/models/program.py b/models/program.py new file mode 100644 index 0000000000000000000000000000000000000000..b5fe49f3a50234eb59125e592c7ff424deab67ef --- /dev/null +++ b/models/program.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship + +from core.db import Base + + +class Program(Base): + id = Column(Integer, primary_key=True) + name = Column(String(length=64)) + department_id = Column(Integer, ForeignKey("department.id", ondelete="cascade")) + department = relationship("Department", backref="programs") + __tablename__ = "program" diff --git a/models/quiz.py b/models/quiz.py new file mode 100644 index 0000000000000000000000000000000000000000..3871d27dcb9a4640fe45baa0555ebfaee4b49f11 --- /dev/null +++ b/models/quiz.py @@ -0,0 +1,68 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import ( + Column, + Integer, + ForeignKey, + String, + DateTime, + Boolean, + ARRAY, +) +import enum + +from sqlalchemy.orm import relationship + +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.sql.sqltypes import JSON +from .association_tables import ( + group_quiz_association_table, + instructor_quiz_association_table, +) + +from core.db import Base + + +class Quiz(Base): + id = Column(Integer, primary_key=True) + end_time = Column(DateTime) + start_time = Column(DateTime) + title = Column(String, nullable=True) + description = Column(String, nullable=True) + is_randomized = Column(Boolean, default=False) + display_individual = Column(Boolean, default=False) + group = relationship( + "Group", secondary=group_quiz_association_table, backref="quiz" + ) + instructor = relationship( + "User", secondary=instructor_quiz_association_table, backref="quiz" + ) + total_marks = Column(Integer, default=0) + course_id = Column(Integer, ForeignKey("course.id", ondelete="cascade")) + course = relationship("Course", backref="quiz") + __tablename__ = "quiz" # noqa + + +class QuizQuestion(Base): + id = Column(Integer, primary_key=True) + + question_text = Column(String, nullable=True) + question_image = Column(ARRAY(String), nullable=True) + + # if IMAGE_OPTIONS in combination with option_image is present then, we show all the image in option_image, and then show all the options present in options + options = Column(JSON, nullable=False) + marks = Column(Integer, default=0) + # if IMAGE_Options present and answer == 0, then check answer_image + answer = Column(ARRAY(Integer), nullable=True) + + quiz_id = Column(Integer, ForeignKey("quiz.id", ondelete="cascade")) + quiz = relationship("Quiz", backref="question") + + __tablename__ = "quiz_question" # noqa + + @hybrid_property + def multiple(self): + if len(self.answer) > 1: + return True + else: + return False diff --git a/models/quiz_answer.py b/models/quiz_answer.py new file mode 100644 index 0000000000000000000000000000000000000000..30555bdbe0cf461156eeba246bc40a96252a31c5 --- /dev/null +++ b/models/quiz_answer.py @@ -0,0 +1,29 @@ +from sqlalchemy import ( + Column, + Integer, + ForeignKey, +) +from sqlalchemy.orm import relationship +from sqlalchemy.sql.schema import UniqueConstraint + +from sqlalchemy.sql.sqltypes import JSON + +from core.db import Base + +# models for storing answers +class QuizAnswer(Base): + id = Column(Integer, primary_key=True) + marks_obtained = Column(Integer) + options_selected = Column(JSON) + + quiz_id = Column(Integer, ForeignKey("quiz.id", ondelete="cascade")) + quiz = relationship("Quiz", backref="quiz_answer") + + student_id = Column(Integer, ForeignKey("user.id", ondelete="cascade")) + student = relationship("User", backref="quiz_answer") + + __table_args__ = ( + UniqueConstraint("quiz_id", "student_id", name="__student_quiz_uc"), + ) + + __tablename__ = "quiz_answer" # noqa \ No newline at end of file diff --git a/models/school.py b/models/school.py new file mode 100644 index 0000000000000000000000000000000000000000..438831aa5726b7491d3feda5f92b3cf22fc0aaec --- /dev/null +++ b/models/school.py @@ -0,0 +1,11 @@ +from sqlalchemy import Column, Integer, String + +from core.db import Base + + + +class School(Base): + id = Column(Integer, primary_key=True) + name = Column(String(length=128), nullable=False) + address = Column(String(length=64)) + __tablename__ = "school" # noqa diff --git a/models/teacher_note.py b/models/teacher_note.py new file mode 100644 index 0000000000000000000000000000000000000000..8a894d3916a0b736ef3409eb1fdaecdda7369129 --- /dev/null +++ b/models/teacher_note.py @@ -0,0 +1,14 @@ +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship + +from core.db import Base + + +class TeacherNote(Base): + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("user.id", ondelete="cascade")) + user = relationship("User", backref="teachernote", foreign_keys=[user_id]) + student_id = Column(Integer, ForeignKey("user.id", ondelete="cascade")) + student = relationship("User", foreign_keys=[student_id]) + message = Column(String(length=512)) + __tablename__ = "teachernote" diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000000000000000000000000000000000000..1cf9ead1c9e276451220cde2b1ba949825bab2a6 --- /dev/null +++ b/models/user.py @@ -0,0 +1,60 @@ +from core.config import settings +from core.db import Base +from sqlalchemy import ( + Boolean, + Column, + Integer, + SmallInteger, + String, + ForeignKey, + DateTime, + Date, +) +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import relationship + + +class User(Base): + id = Column(Integer, primary_key=True, index=True) + profile_image = Column(String(100)) + full_name = Column(String, index=True) + email = Column(String, index=True, nullable=False, unique=True) + + two_fa_secret = Column(String) + + roll = Column(SmallInteger, nullable=True) + + group_id = Column(Integer, ForeignKey("group.id", ondelete="cascade")) + group = relationship("Group", backref="student") + + teacher_group = relationship( + "TeacherGroupCourseAssociation", back_populates="teacher" + ) + teacher_department_id = Column( + ForeignKey("department.id", ondelete="SET NULL"), nullable=True + ) + teacher_department = relationship("Department", backref="teachers") + + dob = Column(Date, nullable=False) + address = Column(String(length=128), nullable=False) + contact_number = Column(String(length=32), index=True, nullable=False) + + hashed_password = Column(String, nullable=False) + is_active = Column(Boolean(), default=False) + user_type = Column( + SmallInteger, + default=settings.UserType.STUDENT.value, + nullable=False, + index=True, + ) + + join_year = Column(SmallInteger) + + @hybrid_property + def is_superuser(self): + if self.user_type == settings.UserType.SUPERADMIN.value: + return True + else: + return False + + __tablename__ = "user" diff --git a/models/user_permission.py b/models/user_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..a6157a6e991834605d443c71c738e998a89dd24f --- /dev/null +++ b/models/user_permission.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, Integer, String + +from core.db import Base +from sqlalchemy import Column, Integer, String + + +class UserPermission(Base): + id = Column(Integer, primary_key=True) + name = Column(String, unique=True, index=True) + __tablename__ = "userpermission" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000000000000000000000000000000000000..b2cc7757aa65297244e89cd7550b04d50efeec70 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2430 @@ +[[package]] +name = "aiofiles" +version = "0.6.0" +description = "File support for asyncio." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "aioredis" +version = "1.3.1" +description = "asyncio (PEP 3156) Redis support" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +async-timeout = "*" +hiredis = "*" + +[[package]] +name = "alembic" +version = "1.7.7" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + +[[package]] +name = "amqp" +version = "5.1.0" +description = "Low-level AMQP client for Python (fork of amqplib)." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +vine = ">=5.0.0" + +[[package]] +name = "anyio" +version = "3.5.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "asgiref" +version = "3.5.0" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "astroid" +version = "2.11.2" +description = "An abstract syntax tree for Python with inference support." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = ">=1.11,<2" + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "autoflake" +version = "1.4" +description = "Removes unused imports and unused variables" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pyflakes = ">=1.1.0" + +[[package]] +name = "autopep8" +version = "1.5.7" +description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pycodestyle = ">=2.7.0" +toml = "*" + +[[package]] +name = "bcrypt" +version = "3.2.0" +description = "Modern password hashing for your software and your servers" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.1" +six = ">=1.4.1" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "billiard" +version = "3.6.4.0" +description = "Python multiprocessing fork with improvements and bugfixes" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "black" +version = "20.8b1" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +appdirs = "*" +click = ">=7.1.2" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.6,<1" +regex = ">=2020.1.8" +toml = ">=0.10.1" +typed-ast = ">=1.4.0" +typing-extensions = ">=3.7.4" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] + +[[package]] +name = "cachetools" +version = "5.0.0" +description = "Extensible memoizing collections and decorators" +category = "main" +optional = false +python-versions = "~=3.7" + +[[package]] +name = "celery" +version = "5.1.2" +description = "Distributed Task Queue." +category = "main" +optional = false +python-versions = ">=3.6," + +[package.dependencies] +billiard = ">=3.6.4.0,<4.0" +click = ">=7.0,<8.0" +click-didyoumean = ">=0.0.3" +click-plugins = ">=1.1.1" +click-repl = ">=0.1.6" +kombu = ">=5.1.0,<6.0" +pytz = ">0.0-dev" +vine = ">=5.0.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=1.3.2)"] +auth = ["cryptography"] +azureblockblob = ["azure-storage-blob (==12.6.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver (<3.21.0)"] +consul = ["python-consul2"] +cosmosdbsql = ["pydocumentdb (==2.3.2)"] +couchbase = ["couchbase (>=3.0.0)"] +couchdb = ["pycouchdb"] +django = ["Django (>=1.11)"] +dynamodb = ["boto3 (>=1.9.178)"] +elasticsearch = ["elasticsearch"] +eventlet = ["eventlet (>=0.26.1)"] +gevent = ["gevent (>=1.0.0)"] +librabbitmq = ["librabbitmq (>=1.5.0)"] +memcache = ["pylibmc"] +mongodb = ["pymongo[srv] (>=3.3.0)"] +msgpack = ["msgpack"] +pymemcache = ["python-memcached"] +pyro = ["pyro4"] +pytest = ["pytest-celery"] +redis = ["redis (>=3.2.0)"] +s3 = ["boto3 (>=1.9.125)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem"] +sqlalchemy = ["sqlalchemy"] +sqs = ["boto3 (>=1.9.125)", "pycurl (==7.43.0.5)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard"] + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.15.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "click-didyoumean" +version = "0.3.0" +description = "Enables git-like *did-you-mean* feature in click" +category = "main" +optional = false +python-versions = ">=3.6.2,<4.0.0" + +[package.dependencies] +click = ">=7" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["pytest (>=3.6)", "pytest-cov", "wheel", "coveralls"] + +[[package]] +name = "click-repl" +version = "0.2.0" +description = "REPL plugin for Click" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = "*" +prompt-toolkit = "*" +six = "*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.3.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "36.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + +[[package]] +name = "cssselect" +version = "1.1.0" +description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "cssutils" +version = "2.4.0" +description = "A CSS Cascading Style Sheets library for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "mock", "lxml", "cssselect", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources"] + +[[package]] +name = "dill" +version = "0.3.4" +description = "serialize all of python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*" + +[package.extras] +graph = ["objgraph (>=1.7.2)"] + +[[package]] +name = "dnspython" +version = "2.2.1" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[package.extras] +dnssec = ["cryptography (>=2.6,<37.0)"] +curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] +doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] +idna = ["idna (>=2.1,<4.0)"] +trio = ["trio (>=0.14,<0.20)"] +wmi = ["wmi (>=1.5.1,<2.0.0)"] + +[[package]] +name = "email-validator" +version = "1.1.3" +description = "A robust email syntax and deliverability validation library for Python 2.x/3.x." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +dnspython = ">=1.15.0" +idna = ">=2.0.0" + +[[package]] +name = "emails" +version = "0.6" +description = "Modern python library for emails." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +chardet = "*" +cssutils = "*" +lxml = "*" +premailer = "*" +python-dateutil = "*" +requests = "*" + +[[package]] +name = "faker" +version = "8.16.0" +description = "Faker is a Python package that generates fake data for you." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +python-dateutil = ">=2.4" +text-unidecode = "1.3" + +[[package]] +name = "fastapi" +version = "0.75.1" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = "0.17.1" + +[package.extras] +all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer (>=0.4.1,<0.5.0)", "pyyaml (>=5.3.1,<6.0.0)"] +test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==22.3.0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "greenlet" +version = "1.1.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx"] + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "h11" +version = "0.13.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "hiredis" +version = "2.0.0" +description = "Python wrapper for hiredis" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "httptools" +version = "0.4.0" +description = "A collection of framework independent HTTP protocol utils." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +test = ["Cython (>=0.29.24,<0.30.0)"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "jedi" +version = "0.18.1" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "jinja2" +version = "3.1.1" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "kombu" +version = "5.2.4" +description = "Messaging library for Python." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +amqp = ">=5.0.9,<6.0.0" +vine = "*" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.0.0)"] +azurestoragequeues = ["azure-storage-queue"] +consul = ["python-consul (>=0.6.0)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +mongodb = ["pymongo (>=3.3.0,<3.12.1)"] +msgpack = ["msgpack"] +pyro = ["pyro4"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=3.4.1,!=4.0.0,!=4.0.1)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy"] +sqs = ["boto3 (>=1.9.12)", "pycurl (>=7.44.1,<7.45.0)", "urllib3 (>=1.26.7)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.7.1" +description = "A fast and thorough lazy object proxy." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "lxml" +version = "4.8.0" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + +[[package]] +name = "mako" +version = "1.2.0" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markupsafe" +version = "2.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy" +version = "0.812" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +typed-ast = ">=1.4.0,<1.5.0" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "passlib" +version = "1.7.4" +description = "comprehensive password hashing framework supporting over 30 schemes" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +build_docs = ["sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)", "cloud-sptheme (>=1.10.1)"] +totp = ["cryptography"] + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.5.1" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "premailer" +version = "3.10.0" +description = "Turns CSS blocks into style attributes" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cachetools = "*" +cssselect = "*" +cssutils = "*" +lxml = "*" +requests = "*" + +[package.extras] +dev = ["tox", "twine", "therapist", "black", "flake8", "wheel"] +test = ["nose", "mock"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.29" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psycopg2-binary" +version = "2.9.3" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.9.0" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pylint" +version = "2.13.5" +description = "python code static checker" +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +astroid = ">=2.11.2,<=2.12.0-dev0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +dill = ">=0.2" +isort = ">=4.2.5,<6" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} + +[package.extras] +testutil = ["gitpython (>3)"] + +[[package]] +name = "pyotp" +version = "2.6.0" +description = "Python One Time Password Library" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyparsing" +version = "3.0.8" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "0.20.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.4.0" + +[[package]] +name = "pytz" +version = "2022.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "raven" +version = "6.10.0" +description = "Raven is a client for Sentry (https://getsentry.com)" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +flask = ["Flask (>=0.8)", "blinker (>=1.1)"] +tests = ["bottle", "celery (>=2.5)", "coverage (<4)", "exam (>=0.5.2)", "flake8 (==3.5.0)", "logbook", "mock", "nose", "pytz", "pytest (>=3.2.0,<3.3.0)", "pytest-timeout (==1.2.1)", "pytest-xdist (==1.18.2)", "pytest-pythonpath (==0.7.2)", "pytest-cov (==2.5.1)", "pytest-flake8 (==1.0.0)", "requests", "tornado (>=4.1,<5.0)", "tox", "webob", "webtest", "wheel", "anyjson", "zconfig", "Flask (>=0.8)", "blinker (>=1.1)", "Flask-Login (>=0.2.0)", "blinker (>=1.1)", "sanic (>=0.7.0)", "aiohttp"] + +[[package]] +name = "regex" +version = "2022.3.15" +description = "Alternative regular expression module, to replace re." +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "requests" +version = "2.27.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} +idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "rope" +version = "0.18.0" +description = "a python refactoring library..." +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +dev = ["pytest"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "sqlalchemy" +version = "1.4.35" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} + +[package.extras] +aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] +aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3,!=0.2.4)"] +mariadb_connector = ["mariadb (>=1.0.1)"] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_pg8000 = ["pg8000 (>=1.16.6)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "sqlalchemy-stubs" +version = "0.4" +description = "SQLAlchemy stubs and mypy plugin" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +mypy = ">=0.790" +typing-extensions = ">=3.7.4" + +[[package]] +name = "starlette" +version = "0.17.1" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4" + +[package.extras] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] + +[[package]] +name = "tenacity" +version = "7.0.0" +description = "Retry code until it succeeds" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +doc = ["reno", "sphinx", "tornado (>=4.5)"] + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typed-ast" +version = "1.4.3" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "typing-extensions" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "uvicorn" +version = "0.17.6" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +asgiref = ">=3.4.0" +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.4.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +PyYAML = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchgod = {version = ">=0.6", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.0", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["websockets (>=10.0)", "httptools (>=0.4.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] + +[[package]] +name = "uvloop" +version = "0.16.0" +description = "Fast implementation of asyncio event loop on top of libuv" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +dev = ["Cython (>=0.29.24,<0.30.0)", "pytest (>=3.6.0)", "Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)"] +test = ["aiohttp", "flake8 (>=3.9.2,<3.10.0)", "psutil", "pycodestyle (>=2.7.0,<2.8.0)", "pyOpenSSL (>=19.0.0,<19.1.0)", "mypy (>=0.800)"] + +[[package]] +name = "vine" +version = "5.0.0" +description = "Promises, promises, promises." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "watchgod" +version = "0.8.2" +description = "Simple, modern file watching and code reload in python." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0.0,<4" + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "websockets" +version = "10.2" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "wrapt" +version = "1.14.0" +description = "Module for decorators, wrappers and monkey patching." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "7f0dfee7fc47a36435c3f7006922942e4af815affb216b3e2a7fb151ca2d4ca6" + +[metadata.files] +aiofiles = [ + {file = "aiofiles-0.6.0-py3-none-any.whl", hash = "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27"}, + {file = "aiofiles-0.6.0.tar.gz", hash = "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"}, +] +aioredis = [ + {file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"}, + {file = "aioredis-1.3.1.tar.gz", hash = "sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a"}, +] +alembic = [ + {file = "alembic-1.7.7-py3-none-any.whl", hash = "sha256:29be0856ec7591c39f4e1cb10f198045d890e6e2274cf8da80cb5e721a09642b"}, + {file = "alembic-1.7.7.tar.gz", hash = "sha256:4961248173ead7ce8a21efb3de378f13b8398e6630fab0eb258dc74a8af24c58"}, +] +amqp = [ + {file = "amqp-5.1.0-py3-none-any.whl", hash = "sha256:a575f4fa659a2290dc369b000cff5fea5c6be05fe3f2d5e511bcf56c7881c3ef"}, + {file = "amqp-5.1.0.tar.gz", hash = "sha256:446b3e8a8ebc2ceafd424ffcaab1c353830d48161256578ed7a65448e601ebed"}, +] +anyio = [ + {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, + {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, +] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +asgiref = [ + {file = "asgiref-3.5.0-py3-none-any.whl", hash = "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"}, + {file = "asgiref-3.5.0.tar.gz", hash = "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0"}, +] +astroid = [ + {file = "astroid-2.11.2-py3-none-any.whl", hash = "sha256:cc8cc0d2d916c42d0a7c476c57550a4557a083081976bf42a73414322a6411d9"}, + {file = "astroid-2.11.2.tar.gz", hash = "sha256:8d0a30fe6481ce919f56690076eafbb2fb649142a89dc874f1ec0e7a011492d0"}, +] +async-timeout = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +autoflake = [ + {file = "autoflake-1.4.tar.gz", hash = "sha256:61a353012cff6ab94ca062823d1fb2f692c4acda51c76ff83a8d77915fba51ea"}, +] +autopep8 = [ + {file = "autopep8-1.5.7-py2.py3-none-any.whl", hash = "sha256:aa213493c30dcdac99537249ee65b24af0b2c29f2e83cd8b3f68760441ed0db9"}, + {file = "autopep8-1.5.7.tar.gz", hash = "sha256:276ced7e9e3cb22e5d7c14748384a5cf5d9002257c0ed50c0e075b68011bb6d0"}, +] +bcrypt = [ + {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, + {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, + {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, + {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, + {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, +] +billiard = [ + {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"}, + {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"}, +] +black = [ + {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, +] +cachetools = [ + {file = "cachetools-5.0.0-py3-none-any.whl", hash = "sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4"}, + {file = "cachetools-5.0.0.tar.gz", hash = "sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6"}, +] +celery = [ + {file = "celery-5.1.2-py3-none-any.whl", hash = "sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42"}, + {file = "celery-5.1.2.tar.gz", hash = "sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0"}, +] +certifi = [ + {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, + {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, +] +cffi = [ + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, +] +chardet = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +click-didyoumean = [ + {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"}, + {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"}, +] +click-plugins = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] +click-repl = [ + {file = "click-repl-0.2.0.tar.gz", hash = "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"}, + {file = "click_repl-0.2.0-py3-none-any.whl", hash = "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, + {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, + {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, + {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, + {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, + {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, + {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, + {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, + {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, + {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, + {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, +] +cryptography = [ + {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6"}, + {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf"}, + {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86"}, + {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2"}, + {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb"}, + {file = "cryptography-36.0.2-cp36-abi3-win32.whl", hash = "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6"}, + {file = "cryptography-36.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29"}, + {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7"}, + {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e"}, + {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51"}, + {file = "cryptography-36.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3"}, + {file = "cryptography-36.0.2.tar.gz", hash = "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9"}, +] +cssselect = [ + {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"}, + {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"}, +] +cssutils = [ + {file = "cssutils-2.4.0-py3-none-any.whl", hash = "sha256:6c7ab239b432c157cd55303993935b92be07272e694d66c75b95eb56928936f6"}, + {file = "cssutils-2.4.0.tar.gz", hash = "sha256:2d97210a83b0a3fe1e4469f5ff9a6420b078572035188b1bab7103c3a36dc89b"}, +] +dill = [ + {file = "dill-0.3.4-py2.py3-none-any.whl", hash = "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f"}, + {file = "dill-0.3.4.zip", hash = "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675"}, +] +dnspython = [ + {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"}, + {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"}, +] +email-validator = [ + {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, + {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, +] +emails = [ + {file = "emails-0.6-py2.py3-none-any.whl", hash = "sha256:72c1e3198075709cc35f67e1b49e2da1a2bc087e9b444073db61a379adfb7f3c"}, + {file = "emails-0.6.tar.gz", hash = "sha256:a4c2d67ea8b8831967a750d8edc6e77040d7693143fe280e6d2a367d9c36ff88"}, +] +faker = [ + {file = "Faker-8.16.0-py3-none-any.whl", hash = "sha256:bb10913b9d3ac2aa37180f816c82040e81f9e0c32cb08445533f293cec8930bf"}, + {file = "Faker-8.16.0.tar.gz", hash = "sha256:d70b375d0af0e4c3abd594003691a1055a96281a414884e623d27bccc7d781da"}, +] +fastapi = [ + {file = "fastapi-0.75.1-py3-none-any.whl", hash = "sha256:f46f8fc81261c2bd956584114da9da98c84e2410c807bc2487532dabf55e7ab8"}, + {file = "fastapi-0.75.1.tar.gz", hash = "sha256:8b62bde916d657803fb60fffe88e2b2c9fb854583784607e4347681cae20ad01"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +greenlet = [ + {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, + {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, + {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, + {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, + {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, + {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, + {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, + {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, + {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, + {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, + {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, + {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, + {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, + {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, + {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, + {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, + {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, + {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, + {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, + {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, + {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, + {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, + {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, + {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, + {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, + {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, + {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, + {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, + {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, +] +gunicorn = [ + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] +h11 = [ + {file = "h11-0.13.0-py3-none-any.whl", hash = "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442"}, + {file = "h11-0.13.0.tar.gz", hash = "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06"}, +] +hiredis = [ + {file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3d55e36715ff06cdc0ab62f9591607c4324297b6b6ce5b58cb9928b3defe30ea"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:5d2a48c80cf5a338d58aae3c16872f4d452345e18350143b3bf7216d33ba7b99"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:240ce6dc19835971f38caf94b5738092cb1e641f8150a9ef9251b7825506cb05"}, + {file = "hiredis-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:5dc7a94bb11096bc4bffd41a3c4f2b958257085c01522aa81140c68b8bf1630a"}, + {file = "hiredis-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:139705ce59d94eef2ceae9fd2ad58710b02aee91e7fa0ccb485665ca0ecbec63"}, + {file = "hiredis-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c39c46d9e44447181cd502a35aad2bb178dbf1b1f86cf4db639d7b9614f837c6"}, + {file = "hiredis-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:adf4dd19d8875ac147bf926c727215a0faf21490b22c053db464e0bf0deb0485"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0f41827028901814c709e744060843c77e78a3aca1e0d6875d2562372fcb405a"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:508999bec4422e646b05c95c598b64bdbef1edf0d2b715450a078ba21b385bcc"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:0d5109337e1db373a892fdcf78eb145ffb6bbd66bb51989ec36117b9f7f9b579"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:04026461eae67fdefa1949b7332e488224eac9e8f2b5c58c98b54d29af22093e"}, + {file = "hiredis-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a00514362df15af041cc06e97aebabf2895e0a7c42c83c21894be12b84402d79"}, + {file = "hiredis-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:09004096e953d7ebd508cded79f6b21e05dff5d7361771f59269425108e703bc"}, + {file = "hiredis-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"}, + {file = "hiredis-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:294a6697dfa41a8cba4c365dd3715abc54d29a86a40ec6405d677ca853307cfb"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:3dddf681284fe16d047d3ad37415b2e9ccdc6c8986c8062dbe51ab9a358b50a5"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dcef843f8de4e2ff5e35e96ec2a4abbdf403bd0f732ead127bd27e51f38ac298"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:87c7c10d186f1743a8fd6a971ab6525d60abd5d5d200f31e073cd5e94d7e7a9d"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7f0055f1809b911ab347a25d786deff5e10e9cf083c3c3fd2dd04e8612e8d9db"}, + {file = "hiredis-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:11d119507bb54e81f375e638225a2c057dda748f2b1deef05c2b1a5d42686048"}, + {file = "hiredis-2.0.0-cp38-cp38-win32.whl", hash = "sha256:7492af15f71f75ee93d2a618ca53fea8be85e7b625e323315169977fae752426"}, + {file = "hiredis-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:65d653df249a2f95673976e4e9dd7ce10de61cfc6e64fa7eeaa6891a9559c581"}, + {file = "hiredis-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8427a5e9062ba66fc2c62fb19a72276cf12c780e8db2b0956ea909c48acff5"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:3f5f7e3a4ab824e3de1e1700f05ad76ee465f5f11f5db61c4b297ec29e692b2e"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:e3447d9e074abf0e3cd85aef8131e01ab93f9f0e86654db7ac8a3f73c63706ce"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:8b42c0dc927b8d7c0eb59f97e6e34408e53bc489f9f90e66e568f329bff3e443"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:b84f29971f0ad4adaee391c6364e6f780d5aae7e9226d41964b26b49376071d0"}, + {file = "hiredis-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:0b39ec237459922c6544d071cdcf92cbb5bc6685a30e7c6d985d8a3e3a75326e"}, + {file = "hiredis-2.0.0-cp39-cp39-win32.whl", hash = "sha256:a7928283143a401e72a4fad43ecc85b35c27ae699cf5d54d39e1e72d97460e1d"}, + {file = "hiredis-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a4ee8000454ad4486fb9f28b0cab7fa1cd796fc36d639882d0b34109b5b3aec9"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1f03d4dadd595f7a69a75709bc81902673fa31964c75f93af74feac2f134cc54"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux1_x86_64.whl", hash = "sha256:04927a4c651a0e9ec11c68e4427d917e44ff101f761cd3b5bc76f86aaa431d27"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:a39efc3ade8c1fb27c097fd112baf09d7fd70b8cb10ef1de4da6efbe066d381d"}, + {file = "hiredis-2.0.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:07bbf9bdcb82239f319b1f09e8ef4bdfaec50ed7d7ea51a56438f39193271163"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:807b3096205c7cec861c8803a6738e33ed86c9aae76cac0e19454245a6bbbc0a"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux1_x86_64.whl", hash = "sha256:1233e303645f468e399ec906b6b48ab7cd8391aae2d08daadbb5cad6ace4bd87"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:cb2126603091902767d96bcb74093bd8b14982f41809f85c9b96e519c7e1dc41"}, + {file = "hiredis-2.0.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0"}, + {file = "hiredis-2.0.0.tar.gz", hash = "sha256:81d6d8e39695f2c37954d1011c0480ef7cf444d4e3ae24bc5e89ee5de360139a"}, +] +httptools = [ + {file = "httptools-0.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcddfe70553be717d9745990dfdb194e22ee0f60eb8f48c0794e7bfeda30d2d5"}, + {file = "httptools-0.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1ee0b459257e222b878a6c09ccf233957d3a4dcb883b0847640af98d2d9aac23"}, + {file = "httptools-0.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceafd5e960b39c7e0d160a1936b68eb87c5e79b3979d66e774f0c77d4d8faaed"}, + {file = "httptools-0.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fdb9f9ed79bc6f46b021b3319184699ba1a22410a82204e6e89c774530069683"}, + {file = "httptools-0.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:abe829275cdd4174b4c4e65ad718715d449e308d59793bf3a931ee1bf7e7b86c"}, + {file = "httptools-0.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7af6bdbd21a2a25d6784f6d67f44f5df33ef39b6159543b9f9064d365c01f919"}, + {file = "httptools-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5d1fe6b6661022fd6cac541f54a4237496b246e6f1c0a6b41998ee08a1135afe"}, + {file = "httptools-0.4.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:48e48530d9b995a84d1d89ae6b3ec4e59ea7d494b150ac3bbc5e2ac4acce92cd"}, + {file = "httptools-0.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a113789e53ac1fa26edf99856a61e4c493868e125ae0dd6354cf518948fbbd5c"}, + {file = "httptools-0.4.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e2eb957787cbb614a0f006bfc5798ff1d90ac7c4dd24854c84edbdc8c02369e"}, + {file = "httptools-0.4.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:7ee9f226acab9085037582c059d66769862706e8e8cd2340470ceb8b3850873d"}, + {file = "httptools-0.4.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:701e66b59dd21a32a274771238025d58db7e2b6ecebbab64ceff51b8e31527ae"}, + {file = "httptools-0.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6a1a7dfc1f9c78a833e2c4904757a0f47ce25d08634dd2a52af394eefe5f9777"}, + {file = "httptools-0.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:903f739c9fb78dab8970b0f3ea51f21955b24b45afa77b22ff0e172fc11ef111"}, + {file = "httptools-0.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54bbd295f031b866b9799dd39cb45deee81aca036c9bff9f58ca06726f6494f1"}, + {file = "httptools-0.4.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3194f6d6443befa8d4db16c1946b2fc428a3ceb8ab32eb6f09a59f86104dc1a0"}, + {file = "httptools-0.4.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cd1295f52971097f757edfbfce827b6dbbfb0f7a74901ee7d4933dff5ad4c9af"}, + {file = "httptools-0.4.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:20a45bcf22452a10fa8d58b7dbdb474381f6946bf5b8933e3662d572bc61bae4"}, + {file = "httptools-0.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d1f27bb0f75bef722d6e22dc609612bfa2f994541621cd2163f8c943b6463dfe"}, + {file = "httptools-0.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7f7bfb74718f52d5ed47d608d507bf66d3bc01d4a8b3e6dd7134daaae129357b"}, + {file = "httptools-0.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a522d12e2ddbc2e91842ffb454a1aeb0d47607972c7d8fc88bd0838d97fb8a2a"}, + {file = "httptools-0.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2db44a0b294d317199e9f80123e72c6b005c55b625b57fae36de68670090fa48"}, + {file = "httptools-0.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c286985b5e194ca0ebb2908d71464b9be8f17cc66d6d3e330e8d5407248f56ad"}, + {file = "httptools-0.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3a4e165ca6204f34856b765d515d558dc84f1352033b8721e8d06c3e44930c3"}, + {file = "httptools-0.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:72aa3fbe636b16d22e04b5a9d24711b043495e0ecfe58080addf23a1a37f3409"}, + {file = "httptools-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9967d9758df505975913304c434cb9ab21e2c609ad859eb921f2f615a038c8de"}, + {file = "httptools-0.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f72b5d24d6730035128b238decdc4c0f2104b7056a7ca55cf047c106842ec890"}, + {file = "httptools-0.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:29bf97a5c532da9c7a04de2c7a9c31d1d54f3abd65a464119b680206bbbb1055"}, + {file = "httptools-0.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98993805f1e3cdb53de4eed02b55dcc953cdf017ba7bbb2fd89226c086a6d855"}, + {file = "httptools-0.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d9b90bf58f3ba04e60321a23a8723a1ff2a9377502535e70495e5ada8e6e6722"}, + {file = "httptools-0.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a99346ebcb801b213c591540837340bdf6fd060a8687518d01c607d338b7424"}, + {file = "httptools-0.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:645373c070080e632480a3d251d892cb795be3d3a15f86975d0f1aca56fd230d"}, + {file = "httptools-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:34d2903dd2a3dd85d33705b6fde40bf91fc44411661283763fd0746723963c83"}, + {file = "httptools-0.4.0.tar.gz", hash = "sha256:2c9a930c378b3d15d6b695fb95ebcff81a7395b4f9775c4f10a076beb0b2c1ff"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +jedi = [ + {file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"}, + {file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"}, +] +jinja2 = [ + {file = "Jinja2-3.1.1-py3-none-any.whl", hash = "sha256:539835f51a74a69f41b848a9645dbdc35b4f20a3b601e2d9a7e22947b15ff119"}, + {file = "Jinja2-3.1.1.tar.gz", hash = "sha256:640bed4bb501cbd17194b3cace1dc2126f5b619cf068a726b98192a0fde74ae9"}, +] +kombu = [ + {file = "kombu-5.2.4-py3-none-any.whl", hash = "sha256:8b213b24293d3417bcf0d2f5537b7f756079e3ea232a8386dcc89a59fd2361a4"}, + {file = "kombu-5.2.4.tar.gz", hash = "sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, + {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, +] +lxml = [ + {file = "lxml-4.8.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b"}, + {file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430"}, + {file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a"}, + {file = "lxml-4.8.0-cp27-cp27m-win32.whl", hash = "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5"}, + {file = "lxml-4.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9"}, + {file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc"}, + {file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170"}, + {file = "lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa"}, + {file = "lxml-4.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1"}, + {file = "lxml-4.8.0-cp310-cp310-win32.whl", hash = "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b"}, + {file = "lxml-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76"}, + {file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6"}, + {file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2"}, + {file = "lxml-4.8.0-cp35-cp35m-win32.whl", hash = "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150"}, + {file = "lxml-4.8.0-cp35-cp35m-win_amd64.whl", hash = "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654"}, + {file = "lxml-4.8.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613"}, + {file = "lxml-4.8.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33"}, + {file = "lxml-4.8.0-cp36-cp36m-win32.whl", hash = "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429"}, + {file = "lxml-4.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63"}, + {file = "lxml-4.8.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85"}, + {file = "lxml-4.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141"}, + {file = "lxml-4.8.0-cp37-cp37m-win32.whl", hash = "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63"}, + {file = "lxml-4.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8"}, + {file = "lxml-4.8.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9"}, + {file = "lxml-4.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68"}, + {file = "lxml-4.8.0-cp38-cp38-win32.whl", hash = "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696"}, + {file = "lxml-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939"}, + {file = "lxml-4.8.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87"}, + {file = "lxml-4.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9"}, + {file = "lxml-4.8.0-cp39-cp39-win32.whl", hash = "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea"}, + {file = "lxml-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93"}, + {file = "lxml-4.8.0.tar.gz", hash = "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23"}, +] +mako = [ + {file = "Mako-1.2.0-py3-none-any.whl", hash = "sha256:23aab11fdbbb0f1051b93793a58323ff937e98e34aece1c4219675122e57e4ba"}, + {file = "Mako-1.2.0.tar.gz", hash = "sha256:9a7c7e922b87db3686210cf49d5d767033a41d4010b284e747682c92bddd8b39"}, +] +markupsafe = [ + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mypy = [ + {file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"}, + {file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"}, + {file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"}, + {file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"}, + {file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"}, + {file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"}, + {file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"}, + {file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"}, + {file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"}, + {file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"}, + {file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"}, + {file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"}, + {file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"}, + {file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"}, + {file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"}, + {file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"}, + {file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"}, + {file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"}, + {file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"}, + {file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"}, + {file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"}, + {file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +parso = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] +passlib = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, + {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +premailer = [ + {file = "premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a"}, + {file = "premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.29-py3-none-any.whl", hash = "sha256:62291dad495e665fca0bda814e342c69952086afb0f4094d0893d357e5c78752"}, + {file = "prompt_toolkit-3.0.29.tar.gz", hash = "sha256:bd640f60e8cecd74f0dc249713d433ace2ddc62b65ee07f96d358e0b152b6ea7"}, +] +psycopg2-binary = [ + {file = "psycopg2-binary-2.9.3.tar.gz", hash = "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-win32.whl", hash = "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029"}, + {file = "psycopg2_binary-2.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-win32.whl", hash = "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b"}, + {file = "psycopg2_binary-2.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-win32.whl", hash = "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba"}, + {file = "psycopg2_binary-2.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-win32.whl", hash = "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce"}, + {file = "psycopg2_binary-2.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-win32.whl", hash = "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d"}, + {file = "psycopg2_binary-2.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +pydantic = [ + {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, + {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, + {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, + {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, + {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, + {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, + {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, + {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, + {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, + {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, + {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, + {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, + {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, + {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, + {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, + {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, + {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, + {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, + {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, + {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, + {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, + {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, + {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pylint = [ + {file = "pylint-2.13.5-py3-none-any.whl", hash = "sha256:c149694cfdeaee1aa2465e6eaab84c87a881a7d55e6e93e09466be7164764d1e"}, + {file = "pylint-2.13.5.tar.gz", hash = "sha256:dab221658368c7a05242e673c275c488670144123f4bd262b2777249c1c0de9b"}, +] +pyotp = [ + {file = "pyotp-2.6.0-py2.py3-none-any.whl", hash = "sha256:9d144de0f8a601d6869abe1409f4a3f75f097c37b50a36a3bf165810a6e23f28"}, + {file = "pyotp-2.6.0.tar.gz", hash = "sha256:d28ddfd40e0c1b6a6b9da961c7d47a10261fb58f378cb00f05ce88b26df9c432"}, +] +pyparsing = [ + {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, + {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +python-dotenv = [ + {file = "python-dotenv-0.20.0.tar.gz", hash = "sha256:b7e3b04a59693c42c36f9ab1cc2acc46fa5df8c78e178fc33a8d4cd05c8d498f"}, + {file = "python_dotenv-0.20.0-py3-none-any.whl", hash = "sha256:d92a187be61fe482e4fd675b6d52200e7be63a12b724abbf931a40ce4fa92938"}, +] +python-multipart = [ + {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, +] +pytz = [ + {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, + {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +raven = [ + {file = "raven-6.10.0-py2.py3-none-any.whl", hash = "sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"}, + {file = "raven-6.10.0.tar.gz", hash = "sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54"}, +] +regex = [ + {file = "regex-2022.3.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:42eb13b93765c6698a5ab3bcd318d8c39bb42e5fa8a7fcf7d8d98923f3babdb1"}, + {file = "regex-2022.3.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9beb03ff6fe509d6455971c2489dceb31687b38781206bcec8e68bdfcf5f1db2"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0a5a1fdc9f148a8827d55b05425801acebeeefc9e86065c7ac8b8cc740a91ff"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb374a2a4dba7c4be0b19dc7b1adc50e6c2c26c3369ac629f50f3c198f3743a4"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c33ce0c665dd325200209340a88438ba7a470bd5f09f7424e520e1a3ff835b52"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04c09b9651fa814eeeb38e029dc1ae83149203e4eeb94e52bb868fadf64852bc"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab5d89cfaf71807da93c131bb7a19c3e19eaefd613d14f3bce4e97de830b15df"}, + {file = "regex-2022.3.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e2630ae470d6a9f8e4967388c1eda4762706f5750ecf387785e0df63a4cc5af"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:df037c01d68d1958dad3463e2881d3638a0d6693483f58ad41001aa53a83fcea"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:940570c1a305bac10e8b2bc934b85a7709c649317dd16520471e85660275083a"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7f63877c87552992894ea1444378b9c3a1d80819880ae226bb30b04789c0828c"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:3e265b388cc80c7c9c01bb4f26c9e536c40b2c05b7231fbb347381a2e1c8bf43"}, + {file = "regex-2022.3.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:058054c7a54428d5c3e3739ac1e363dc9347d15e64833817797dc4f01fb94bb8"}, + {file = "regex-2022.3.15-cp310-cp310-win32.whl", hash = "sha256:76435a92e444e5b8f346aed76801db1c1e5176c4c7e17daba074fbb46cb8d783"}, + {file = "regex-2022.3.15-cp310-cp310-win_amd64.whl", hash = "sha256:174d964bc683b1e8b0970e1325f75e6242786a92a22cedb2a6ec3e4ae25358bd"}, + {file = "regex-2022.3.15-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6e1d8ed9e61f37881c8db383a124829a6e8114a69bd3377a25aecaeb9b3538f8"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b52771f05cff7517f7067fef19ffe545b1f05959e440d42247a17cd9bddae11b"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:673f5a393d603c34477dbad70db30025ccd23996a2d0916e942aac91cc42b31a"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8923e1c5231549fee78ff9b2914fad25f2e3517572bb34bfaa3aea682a758683"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764e66a0e382829f6ad3bbce0987153080a511c19eb3d2f8ead3f766d14433ac"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd00859291658fe1fda48a99559fb34da891c50385b0bfb35b808f98956ef1e7"}, + {file = "regex-2022.3.15-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:aa2ce79f3889720b46e0aaba338148a1069aea55fda2c29e0626b4db20d9fcb7"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:34bb30c095342797608727baf5c8aa122406aa5edfa12107b8e08eb432d4c5d7"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:25ecb1dffc5e409ca42f01a2b2437f93024ff1612c1e7983bad9ee191a5e8828"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:aa5eedfc2461c16a092a2fabc5895f159915f25731740c9152a1b00f4bcf629a"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:7d1a6e403ac8f1d91d8f51c441c3f99367488ed822bda2b40836690d5d0059f5"}, + {file = "regex-2022.3.15-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:3e4d710ff6539026e49f15a3797c6b1053573c2b65210373ef0eec24480b900b"}, + {file = "regex-2022.3.15-cp36-cp36m-win32.whl", hash = "sha256:0100f0ded953b6b17f18207907159ba9be3159649ad2d9b15535a74de70359d3"}, + {file = "regex-2022.3.15-cp36-cp36m-win_amd64.whl", hash = "sha256:f320c070dea3f20c11213e56dbbd7294c05743417cde01392148964b7bc2d31a"}, + {file = "regex-2022.3.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fc8c7958d14e8270171b3d72792b609c057ec0fa17d507729835b5cff6b7f69a"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ca6dcd17f537e9f3793cdde20ac6076af51b2bd8ad5fe69fa54373b17b48d3c"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0214ff6dff1b5a4b4740cfe6e47f2c4c92ba2938fca7abbea1359036305c132f"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a98ae493e4e80b3ded6503ff087a8492db058e9c68de371ac3df78e88360b374"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b1cc70e31aacc152a12b39245974c8fccf313187eead559ee5966d50e1b5817"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4829db3737480a9d5bfb1c0320c4ee13736f555f53a056aacc874f140e98f64"}, + {file = "regex-2022.3.15-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:303b15a3d32bf5fe5a73288c316bac5807587f193ceee4eb6d96ee38663789fa"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:dc7b7c16a519d924c50876fb152af661a20749dcbf653c8759e715c1a7a95b18"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ce3057777a14a9a1399b81eca6a6bfc9612047811234398b84c54aeff6d536ea"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:48081b6bff550fe10bcc20c01cf6c83dbca2ccf74eeacbfac240264775fd7ecf"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dcbb7665a9db9f8d7642171152c45da60e16c4f706191d66a1dc47ec9f820aed"}, + {file = "regex-2022.3.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c155a1a80c5e7a8fa1d9bb1bf3c8a953532b53ab1196092749bafb9d3a7cbb60"}, + {file = "regex-2022.3.15-cp37-cp37m-win32.whl", hash = "sha256:04b5ee2b6d29b4a99d38a6469aa1db65bb79d283186e8460542c517da195a8f6"}, + {file = "regex-2022.3.15-cp37-cp37m-win_amd64.whl", hash = "sha256:797437e6024dc1589163675ae82f303103063a0a580c6fd8d0b9a0a6708da29e"}, + {file = "regex-2022.3.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8afcd1c2297bc989dceaa0379ba15a6df16da69493635e53431d2d0c30356086"}, + {file = "regex-2022.3.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0066a6631c92774391f2ea0f90268f0d82fffe39cb946f0f9c6b382a1c61a5e5"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8248f19a878c72d8c0a785a2cd45d69432e443c9f10ab924c29adda77b324ae"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d1f3ea0d1924feb4cf6afb2699259f658a08ac6f8f3a4a806661c2dfcd66db1"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:794a6bc66c43db8ed06698fc32aaeaac5c4812d9f825e9589e56f311da7becd9"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d1445824944e642ffa54c4f512da17a953699c563a356d8b8cbdad26d3b7598"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f553a1190ae6cd26e553a79f6b6cfba7b8f304da2071052fa33469da075ea625"}, + {file = "regex-2022.3.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:75a5e6ce18982f0713c4bac0704bf3f65eed9b277edd3fb9d2b0ff1815943327"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f16cf7e4e1bf88fecf7f41da4061f181a6170e179d956420f84e700fb8a3fd6b"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dad3991f0678facca1a0831ec1ddece2eb4d1dd0f5150acb9440f73a3b863907"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:491fc754428514750ab21c2d294486223ce7385446f2c2f5df87ddbed32979ae"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:6504c22c173bb74075d7479852356bb7ca80e28c8e548d4d630a104f231e04fb"}, + {file = "regex-2022.3.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01c913cf573d1da0b34c9001a94977273b5ee2fe4cb222a5d5b320f3a9d1a835"}, + {file = "regex-2022.3.15-cp38-cp38-win32.whl", hash = "sha256:029e9e7e0d4d7c3446aa92474cbb07dafb0b2ef1d5ca8365f059998c010600e6"}, + {file = "regex-2022.3.15-cp38-cp38-win_amd64.whl", hash = "sha256:947a8525c0a95ba8dc873191f9017d1b1e3024d4dc757f694e0af3026e34044a"}, + {file = "regex-2022.3.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:591d4fba554f24bfa0421ba040cd199210a24301f923ed4b628e1e15a1001ff4"}, + {file = "regex-2022.3.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9809404528a999cf02a400ee5677c81959bc5cb938fdc696b62eb40214e3632"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f08a7e4d62ea2a45557f561eea87c907222575ca2134180b6974f8ac81e24f06"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a86cac984da35377ca9ac5e2e0589bd11b3aebb61801204bd99c41fac516f0d"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:286908cbe86b1a0240a867aecfe26a439b16a1f585d2de133540549831f8e774"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b7494df3fdcc95a1f76cf134d00b54962dd83189520fd35b8fcd474c0aa616d"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b1ceede92400b3acfebc1425937454aaf2c62cd5261a3fabd560c61e74f6da3"}, + {file = "regex-2022.3.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0317eb6331146c524751354ebef76a7a531853d7207a4d760dfb5f553137a2a4"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c144405220c5ad3f5deab4c77f3e80d52e83804a6b48b6bed3d81a9a0238e4c"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5b2e24f3ae03af3d8e8e6d824c891fea0ca9035c5d06ac194a2700373861a15c"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f2c53f3af011393ab5ed9ab640fa0876757498aac188f782a0c620e33faa2a3d"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:060f9066d2177905203516c62c8ea0066c16c7342971d54204d4e51b13dfbe2e"}, + {file = "regex-2022.3.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:530a3a16e57bd3ea0dff5ec2695c09632c9d6c549f5869d6cf639f5f7153fb9c"}, + {file = "regex-2022.3.15-cp39-cp39-win32.whl", hash = "sha256:78ce90c50d0ec970bd0002462430e00d1ecfd1255218d52d08b3a143fe4bde18"}, + {file = "regex-2022.3.15-cp39-cp39-win_amd64.whl", hash = "sha256:c5adc854764732dbd95a713f2e6c3e914e17f2ccdc331b9ecb777484c31f73b6"}, + {file = "regex-2022.3.15.tar.gz", hash = "sha256:0a7b75cc7bb4cc0334380053e4671c560e31272c9d2d5a6c4b8e9ae2c9bd0f82"}, +] +requests = [ + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] +rope = [ + {file = "rope-0.18.0.tar.gz", hash = "sha256:786b5c38c530d4846aa68a42604f61b4e69a493390e3ca11b88df0fbfdc3ed04"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +sqlalchemy = [ + {file = "SQLAlchemy-1.4.35-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:093b3109c2747d5dc0fa4314b1caf4c7ca336d5c8c831e3cfbec06a7e861e1e6"}, + {file = "SQLAlchemy-1.4.35-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6fb6b9ed1d0be7fa2c90be8ad2442c14cbf84eb0709dd1afeeff1e511550041"}, + {file = "SQLAlchemy-1.4.35-cp27-cp27m-win32.whl", hash = "sha256:d38a49aa75a5759d0d118e26701d70c70a37b896379115f8386e91b0444bfa70"}, + {file = "SQLAlchemy-1.4.35-cp27-cp27m-win_amd64.whl", hash = "sha256:70e571ae9ee0ff36ed37e2b2765445d54981e4d600eccdf6fe3838bc2538d157"}, + {file = "SQLAlchemy-1.4.35-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:48036698f20080462e981b18d77d574631a3d1fc2c33b416c6df299ec1d10b99"}, + {file = "SQLAlchemy-1.4.35-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:4ba2c1f368bcf8551cdaa27eac525022471015633d5bdafbc4297e0511f62f51"}, + {file = "SQLAlchemy-1.4.35-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17316100fcd0b6371ac9211351cb976fd0c2e12a859c1a57965e3ef7f3ed2bc"}, + {file = "SQLAlchemy-1.4.35-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9837133b89ad017e50a02a3b46419869cf4e9aa02743e911b2a9e25fa6b05403"}, + {file = "SQLAlchemy-1.4.35-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4efb70a62cbbbc052c67dc66b5448b0053b509732184af3e7859d05fdf6223c"}, + {file = "SQLAlchemy-1.4.35-cp310-cp310-win32.whl", hash = "sha256:1ff9f84b2098ef1b96255a80981ee10f4b5d49b6cfeeccf9632c2078cd86052e"}, + {file = "SQLAlchemy-1.4.35-cp310-cp310-win_amd64.whl", hash = "sha256:48f0eb5bcc87a9b2a95b345ed18d6400daaa86ca414f6840961ed85c342af8f4"}, + {file = "SQLAlchemy-1.4.35-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:da25e75ba9f3fabc271673b6b413ca234994e6d3453424bea36bb5549c5bbaec"}, + {file = "SQLAlchemy-1.4.35-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aeea6ace30603ca9a8869853bb4a04c7446856d7789e36694cd887967b7621f6"}, + {file = "SQLAlchemy-1.4.35-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5dbdbb39c1b100df4d182c78949158073ca46ba2850c64fe02ffb1eb5b70903"}, + {file = "SQLAlchemy-1.4.35-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfd8e4c64c30a5219032e64404d468c425bdbc13b397da906fc9bee6591fc0dd"}, + {file = "SQLAlchemy-1.4.35-cp36-cp36m-win32.whl", hash = "sha256:9dac1924611698f8fe5b2e58601156c01da2b6c0758ba519003013a78280cf4d"}, + {file = "SQLAlchemy-1.4.35-cp36-cp36m-win_amd64.whl", hash = "sha256:e8b09e2d90267717d850f2e2323919ea32004f55c40e5d53b41267e382446044"}, + {file = "SQLAlchemy-1.4.35-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:63c82c9e8ccc2fb4bfd87c24ffbac320f70b7c93b78f206c1f9c441fa3013a5f"}, + {file = "SQLAlchemy-1.4.35-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:effadcda9a129cc56408dd5b2ea20ee9edcea24bd58e6a1489fa27672d733182"}, + {file = "SQLAlchemy-1.4.35-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2c6c411d8c59afba95abccd2b418f30ade674186660a2d310d364843049fb2c1"}, + {file = "SQLAlchemy-1.4.35-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2489e70bfa2356f2d421106794507daccf6cc8711753c442fc97272437fc606"}, + {file = "SQLAlchemy-1.4.35-cp37-cp37m-win32.whl", hash = "sha256:186cb3bd77abf2ddcf722f755659559bfb157647b3fd3f32ea1c70e8311e8f6b"}, + {file = "SQLAlchemy-1.4.35-cp37-cp37m-win_amd64.whl", hash = "sha256:babd63fb7cb6b0440abb6d16aca2be63342a6eea3dc7b613bb7a9357dc36920f"}, + {file = "SQLAlchemy-1.4.35-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9e1a72197529ea00357640f21d92ffc7024e156ef9ac36edf271c8335facbc1a"}, + {file = "SQLAlchemy-1.4.35-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e255a8dd5572b0c66d6ee53597d36157ad6cf3bc1114f61c54a65189f996ab03"}, + {file = "SQLAlchemy-1.4.35-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9bec63b1e20ef69484f530fb4b4837e050450637ff9acd6dccc7003c5013abf8"}, + {file = "SQLAlchemy-1.4.35-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95411abc0e36d18f54fa5e24d42960ea3f144fb16caaa5a8c2e492b5424cc82c"}, + {file = "SQLAlchemy-1.4.35-cp38-cp38-win32.whl", hash = "sha256:28b17ebbaee6587013be2f78dc4f6e95115e1ec8dd7647c4e7be048da749e48b"}, + {file = "SQLAlchemy-1.4.35-cp38-cp38-win_amd64.whl", hash = "sha256:9e7094cf04e6042c4210a185fa7b9b8b3b789dd6d1de7b4f19452290838e48bd"}, + {file = "SQLAlchemy-1.4.35-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:1b4eac3933c335d7f375639885765722534bb4e52e51cdc01a667eea822af9b6"}, + {file = "SQLAlchemy-1.4.35-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d8edfb09ed2b865485530c13e269833dab62ab2d582fde21026c9039d4d0e62"}, + {file = "SQLAlchemy-1.4.35-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6204d06bfa85f87625e1831ca663f9dba91ac8aec24b8c65d02fb25cbaf4b4d7"}, + {file = "SQLAlchemy-1.4.35-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28aa2ef06c904729620cc735262192e622db9136c26d8587f71f29ec7715628a"}, + {file = "SQLAlchemy-1.4.35-cp39-cp39-win32.whl", hash = "sha256:ecc81336b46e31ae9c9bdfa220082079914e31a476d088d3337ecf531d861228"}, + {file = "SQLAlchemy-1.4.35-cp39-cp39-win_amd64.whl", hash = "sha256:53c7469b86a60fe2babca4f70111357e6e3d5150373bc85eb3b914356983e89a"}, + {file = "SQLAlchemy-1.4.35.tar.gz", hash = "sha256:2ffc813b01dc6473990f5e575f210ca5ac2f5465ace3908b78ffd6d20058aab5"}, +] +sqlalchemy-stubs = [ + {file = "sqlalchemy-stubs-0.4.tar.gz", hash = "sha256:c665d6dd4482ef642f01027fa06c3d5e91befabb219dc71fc2a09e7d7695f7ae"}, + {file = "sqlalchemy_stubs-0.4-py3-none-any.whl", hash = "sha256:5eec7aa110adf9b957b631799a72fef396b23ff99fe296df726645d01e312aa5"}, +] +starlette = [ + {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, + {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, +] +tenacity = [ + {file = "tenacity-7.0.0-py2.py3-none-any.whl", hash = "sha256:a0ce48587271515db7d3a5e700df9ae69cce98c4b57c23a4886da15243603dd8"}, + {file = "tenacity-7.0.0.tar.gz", hash = "sha256:5bd16ef5d3b985647fe28dfa6f695d343aa26479a04e8792b9d3c8f49e361ae1"}, +] +text-unidecode = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typed-ast = [ + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, + {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, + {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, + {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, + {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, + {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, + {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, + {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, + {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, + {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, + {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, + {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, + {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, + {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, + {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, + {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, + {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, + {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, +] +typing-extensions = [ + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, +] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] +uvicorn = [ + {file = "uvicorn-0.17.6-py3-none-any.whl", hash = "sha256:19e2a0e96c9ac5581c01eb1a79a7d2f72bb479691acd2b8921fce48ed5b961a6"}, + {file = "uvicorn-0.17.6.tar.gz", hash = "sha256:5180f9d059611747d841a4a4c4ab675edf54c8489e97f96d0583ee90ac3bfc23"}, +] +uvloop = [ + {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"}, + {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30ba9dcbd0965f5c812b7c2112a1ddf60cf904c1c160f398e7eed3a6b82dcd9c"}, + {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bd53f7f5db562f37cd64a3af5012df8cac2c464c97e732ed556800129505bd64"}, + {file = "uvloop-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772206116b9b57cd625c8a88f2413df2fcfd0b496eb188b82a43bed7af2c2ec9"}, + {file = "uvloop-0.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b572256409f194521a9895aef274cea88731d14732343da3ecdb175228881638"}, + {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:04ff57aa137230d8cc968f03481176041ae789308b4d5079118331ab01112450"}, + {file = "uvloop-0.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a19828c4f15687675ea912cc28bbcb48e9bb907c801873bd1519b96b04fb805"}, + {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e814ac2c6f9daf4c36eb8e85266859f42174a4ff0d71b99405ed559257750382"}, + {file = "uvloop-0.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd8f42ea1ea8f4e84d265769089964ddda95eb2bb38b5cbe26712b0616c3edee"}, + {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:647e481940379eebd314c00440314c81ea547aa636056f554d491e40503c8464"}, + {file = "uvloop-0.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0d26fa5875d43ddbb0d9d79a447d2ace4180d9e3239788208527c4784f7cab"}, + {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ccd57ae8db17d677e9e06192e9c9ec4bd2066b77790f9aa7dede2cc4008ee8f"}, + {file = "uvloop-0.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:089b4834fd299d82d83a25e3335372f12117a7d38525217c2258e9b9f4578897"}, + {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98d117332cc9e5ea8dfdc2b28b0a23f60370d02e1395f88f40d1effd2cb86c4f"}, + {file = "uvloop-0.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e5f2e2ff51aefe6c19ee98af12b4ae61f5be456cd24396953244a30880ad861"}, + {file = "uvloop-0.16.0.tar.gz", hash = "sha256:f74bc20c7b67d1c27c72601c78cf95be99d5c2cdd4514502b4f3eb0933ff1228"}, +] +vine = [ + {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"}, + {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"}, +] +watchgod = [ + {file = "watchgod-0.8.2-py3-none-any.whl", hash = "sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce"}, + {file = "watchgod-0.8.2.tar.gz", hash = "sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +websockets = [ + {file = "websockets-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5396710f86a306cf52f87fd8ea594a0e894ba0cc5a36059eaca3a477dc332aa"}, + {file = "websockets-10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b22bdc795e62e71118b63e14a08bacfa4f262fd2877de7e5b950f5ac16b0348f"}, + {file = "websockets-10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b04270b5613f245ec84bb2c6a482a9d009aefad37c0575f6cda8499125d5d5c"}, + {file = "websockets-10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5c335dc0e7dc271ef36df3f439868b3c790775f345338c2f61a562f1074187b"}, + {file = "websockets-10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a009eb551c46fd79737791c0c833fc0e5b56bcd1c3057498b262d660b92e9cd"}, + {file = "websockets-10.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a10c0c1ee02164246f90053273a42d72a3b2452a7e7486fdae781138cf7fbe2d"}, + {file = "websockets-10.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b38a5c9112e3dbbe45540f7b60c5204f49b3cb501b40950d6ab34cd202ab1d0"}, + {file = "websockets-10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2aa9b91347ecd0412683f28aabe27f6bad502d89bd363b76e0a3508b1596402e"}, + {file = "websockets-10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b7fe45ae43ac814beb8ca09d6995b56800676f2cfa8e23f42839dc69bba34a42"}, + {file = "websockets-10.2-cp310-cp310-win32.whl", hash = "sha256:cef40a1b183dcf39d23b392e9dd1d9b07ab9c46aadf294fff1350fb79146e72b"}, + {file = "websockets-10.2-cp310-cp310-win_amd64.whl", hash = "sha256:c21a67ab9a94bd53e10bba21912556027fea944648a09e6508415ad14e37c325"}, + {file = "websockets-10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb316b87cbe3c0791c2ad92a5a36bf6adc87c457654335810b25048c1daa6fd5"}, + {file = "websockets-10.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f14bd10e170abc01682a9f8b28b16e6f20acf6175945ef38db6ffe31b0c72c3f"}, + {file = "websockets-10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fa35c5d1830d0fb7b810324e9eeab9aa92e8f273f11fdbdc0741dcded6d72b9f"}, + {file = "websockets-10.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:71a4491cfe7a9f18ee57d41163cb6a8a3fa591e0f0564ca8b0ed86b2a30cced4"}, + {file = "websockets-10.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6193bbc1ee63aadeb9a4d81de0e19477401d150d506aee772d8380943f118186"}, + {file = "websockets-10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8beac786a388bb99a66c3be4ab0fb38273c0e3bc17f612a4e0a47c4fc8b9c045"}, + {file = "websockets-10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c67d9cacb3f6537ca21e9b224d4fd08481538e43bcac08b3d93181b0816def39"}, + {file = "websockets-10.2-cp37-cp37m-win32.whl", hash = "sha256:a03a25d95cc7400bd4d61a63460b5d85a7761c12075ee2f51de1ffe73aa593d3"}, + {file = "websockets-10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f8296b8408ec6853b26771599990721a26403e62b9de7e50ac0a056772ac0b5e"}, + {file = "websockets-10.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7bb9d8a6beca478c7e9bdde0159bd810cc1006ad6a7cb460533bae39da692ca2"}, + {file = "websockets-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:05f6e9757017270e7a92a2975e2ae88a9a582ffc4629086fd6039aa80e99cd86"}, + {file = "websockets-10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1c9031e90ebfc486e9cdad532b94004ade3aa39a31d3c46c105bb0b579cd2490"}, + {file = "websockets-10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82bc33db6d8309dc27a3bee11f7da2288ad925fcbabc2a4bb78f7e9c56249baf"}, + {file = "websockets-10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:24b879ba7db12bb525d4e58089fcbe6a3df3ce4666523183654170e86d372cbe"}, + {file = "websockets-10.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf931c33db9c87c53d009856045dd524e4a378445693382a920fa1e0eb77c36c"}, + {file = "websockets-10.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:669e54228a4d9457abafed27cbf0e2b9f401445c4dfefc12bf8e4db9751703b8"}, + {file = "websockets-10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bffc65442dd35c473ca9790a3fa3ba06396102a950794f536783f4b8060af8dd"}, + {file = "websockets-10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d4d110a84b63c5cfdd22485acc97b8b919aefeecd6300c0c9d551e055b9a88ea"}, + {file = "websockets-10.2-cp38-cp38-win32.whl", hash = "sha256:117383d0a17a0dda349f7a8790763dde75c1508ff8e4d6e8328b898b7df48397"}, + {file = "websockets-10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b66421f9f13d4df60cd48ab977ed2c2b6c9147ae1a33caf5a9f46294422fda1"}, + {file = "websockets-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac081aa0307f263d63c5ff0727935c736c8dad51ddf2dc9f5d0c4759842aefaa"}, + {file = "websockets-10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b4059e2ccbe6587b6dc9a01db5fc49ead9a884faa4076eea96c5ec62cb32f42a"}, + {file = "websockets-10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9ca2ca05a4c29179f06cf6727b45dba5d228da62623ec9df4184413d8aae6cb9"}, + {file = "websockets-10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97950c7c844ec6f8d292440953ae18b99e3a6a09885e09d20d5e7ecd9b914cf8"}, + {file = "websockets-10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:98f57b3120f8331cd7440dbe0e776474f5e3632fdaa474af1f6b754955a47d71"}, + {file = "websockets-10.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a72b92f96e5e540d5dda99ee3346e199ade8df63152fa3c737260da1730c411f"}, + {file = "websockets-10.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:038afef2a05893578d10dadbdbb5f112bd115c46347e1efe99f6a356ff062138"}, + {file = "websockets-10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f09f46b1ff6d09b01c7816c50bd1903cf7d02ebbdb63726132717c2fcda835d5"}, + {file = "websockets-10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2349fa81b6b959484bb2bda556ccb9eb70ba68987646a0f8a537a1a18319fb03"}, + {file = "websockets-10.2-cp39-cp39-win32.whl", hash = "sha256:bef03a51f9657fb03d8da6ccd233fe96e04101a852f0ffd35f5b725b28221ff3"}, + {file = "websockets-10.2-cp39-cp39-win_amd64.whl", hash = "sha256:1c1f3b18c8162e3b09761d0c6a0305fd642934202541cc511ef972cb9463261e"}, + {file = "websockets-10.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a38a0175ae82e4a8c4bac29fc01b9ee26d7d5a614e5ee11e7813c68a7d938ce"}, + {file = "websockets-10.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e56606842bb24e16e36ae7eb308d866b4249cf0be8f63b212f287eeb76b124"}, + {file = "websockets-10.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0f73cb2526d6da268e86977b2c4b58f2195994e53070fe567d5487c6436047e6"}, + {file = "websockets-10.2-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cd02f36d37e503aca88ab23cc0a1a0e92a263d37acf6331521eb38040dcf77b"}, + {file = "websockets-10.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:56d48eebe9e39ce0d68701bce3b21df923aa05dcc00f9fd8300de1df31a7c07c"}, + {file = "websockets-10.2.tar.gz", hash = "sha256:8351c3c86b08156337b0e4ece0e3c5ec3e01fcd14e8950996832a23c99416098"}, +] +wrapt = [ + {file = "wrapt-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:5a9a1889cc01ed2ed5f34574c90745fab1dd06ec2eee663e8ebeefe363e8efd7"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9a3ff5fb015f6feb78340143584d9f8a0b91b6293d6b5cf4295b3e95d179b88c"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4b847029e2d5e11fd536c9ac3136ddc3f54bc9488a75ef7d040a3900406a91eb"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:9a5a544861b21e0e7575b6023adebe7a8c6321127bb1d238eb40d99803a0e8bd"}, + {file = "wrapt-1.14.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:88236b90dda77f0394f878324cfbae05ae6fde8a84d548cfe73a75278d760291"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f0408e2dbad9e82b4c960274214af533f856a199c9274bd4aff55d4634dedc33"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:9d8c68c4145041b4eeae96239802cfdfd9ef927754a5be3f50505f09f309d8c6"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:22626dca56fd7f55a0733e604f1027277eb0f4f3d95ff28f15d27ac25a45f71b"}, + {file = "wrapt-1.14.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:65bf3eb34721bf18b5a021a1ad7aa05947a1767d1aa272b725728014475ea7d5"}, + {file = "wrapt-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09d16ae7a13cff43660155383a2372b4aa09109c7127aa3f24c3cf99b891c330"}, + {file = "wrapt-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:debaf04f813ada978d7d16c7dfa16f3c9c2ec9adf4656efdc4defdf841fc2f0c"}, + {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748df39ed634851350efa87690c2237a678ed794fe9ede3f0d79f071ee042561"}, + {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1807054aa7b61ad8d8103b3b30c9764de2e9d0c0978e9d3fc337e4e74bf25faa"}, + {file = "wrapt-1.14.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:763a73ab377390e2af26042f685a26787c402390f682443727b847e9496e4a2a"}, + {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8529b07b49b2d89d6917cfa157d3ea1dfb4d319d51e23030664a827fe5fd2131"}, + {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:68aeefac31c1f73949662ba8affaf9950b9938b712fb9d428fa2a07e40ee57f8"}, + {file = "wrapt-1.14.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59d7d92cee84a547d91267f0fea381c363121d70fe90b12cd88241bd9b0e1763"}, + {file = "wrapt-1.14.0-cp310-cp310-win32.whl", hash = "sha256:3a88254881e8a8c4784ecc9cb2249ff757fd94b911d5df9a5984961b96113fff"}, + {file = "wrapt-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a242871b3d8eecc56d350e5e03ea1854de47b17f040446da0e47dc3e0b9ad4d"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a65bffd24409454b889af33b6c49d0d9bcd1a219b972fba975ac935f17bdf627"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9d9fcd06c952efa4b6b95f3d788a819b7f33d11bea377be6b8980c95e7d10775"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:db6a0ddc1282ceb9032e41853e659c9b638789be38e5b8ad7498caac00231c23"}, + {file = "wrapt-1.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:14e7e2c5f5fca67e9a6d5f753d21f138398cad2b1159913ec9e9a67745f09ba3"}, + {file = "wrapt-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:6d9810d4f697d58fd66039ab959e6d37e63ab377008ef1d63904df25956c7db0"}, + {file = "wrapt-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:d808a5a5411982a09fef6b49aac62986274ab050e9d3e9817ad65b2791ed1425"}, + {file = "wrapt-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b77159d9862374da213f741af0c361720200ab7ad21b9f12556e0eb95912cd48"}, + {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36a76a7527df8583112b24adc01748cd51a2d14e905b337a6fefa8b96fc708fb"}, + {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0057b5435a65b933cbf5d859cd4956624df37b8bf0917c71756e4b3d9958b9e"}, + {file = "wrapt-1.14.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0a4ca02752ced5f37498827e49c414d694ad7cf451ee850e3ff160f2bee9d3"}, + {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8c6be72eac3c14baa473620e04f74186c5d8f45d80f8f2b4eda6e1d18af808e8"}, + {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:21b1106bff6ece8cb203ef45b4f5778d7226c941c83aaaa1e1f0f4f32cc148cd"}, + {file = "wrapt-1.14.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:493da1f8b1bb8a623c16552fb4a1e164c0200447eb83d3f68b44315ead3f9036"}, + {file = "wrapt-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:89ba3d548ee1e6291a20f3c7380c92f71e358ce8b9e48161401e087e0bc740f8"}, + {file = "wrapt-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:729d5e96566f44fccac6c4447ec2332636b4fe273f03da128fff8d5559782b06"}, + {file = "wrapt-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:891c353e95bb11abb548ca95c8b98050f3620a7378332eb90d6acdef35b401d4"}, + {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23f96134a3aa24cc50614920cc087e22f87439053d886e474638c68c8d15dc80"}, + {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6807bcee549a8cb2f38f73f469703a1d8d5d990815c3004f21ddb68a567385ce"}, + {file = "wrapt-1.14.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6915682f9a9bc4cf2908e83caf5895a685da1fbd20b6d485dafb8e218a338279"}, + {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f2f3bc7cd9c9fcd39143f11342eb5963317bd54ecc98e3650ca22704b69d9653"}, + {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3a71dbd792cc7a3d772ef8cd08d3048593f13d6f40a11f3427c000cf0a5b36a0"}, + {file = "wrapt-1.14.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5a0898a640559dec00f3614ffb11d97a2666ee9a2a6bad1259c9facd01a1d4d9"}, + {file = "wrapt-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:167e4793dc987f77fd476862d32fa404d42b71f6a85d3b38cbce711dba5e6b68"}, + {file = "wrapt-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d066ffc5ed0be00cd0352c95800a519cf9e4b5dd34a028d301bdc7177c72daf3"}, + {file = "wrapt-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d9bdfa74d369256e4218000a629978590fd7cb6cf6893251dad13d051090436d"}, + {file = "wrapt-1.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2498762814dd7dd2a1d0248eda2afbc3dd9c11537bc8200a4b21789b6df6cd38"}, + {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f24ca7953f2643d59a9c87d6e272d8adddd4a53bb62b9208f36db408d7aafc7"}, + {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b835b86bd5a1bdbe257d610eecab07bf685b1af2a7563093e0e69180c1d4af1"}, + {file = "wrapt-1.14.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b21650fa6907e523869e0396c5bd591cc326e5c1dd594dcdccac089561cacfb8"}, + {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:354d9fc6b1e44750e2a67b4b108841f5f5ea08853453ecbf44c81fdc2e0d50bd"}, + {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1f83e9c21cd5275991076b2ba1cd35418af3504667affb4745b48937e214bafe"}, + {file = "wrapt-1.14.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:61e1a064906ccba038aa3c4a5a82f6199749efbbb3cef0804ae5c37f550eded0"}, + {file = "wrapt-1.14.0-cp38-cp38-win32.whl", hash = "sha256:28c659878f684365d53cf59dc9a1929ea2eecd7ac65da762be8b1ba193f7e84f"}, + {file = "wrapt-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:b0ed6ad6c9640671689c2dbe6244680fe8b897c08fd1fab2228429b66c518e5e"}, + {file = "wrapt-1.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b3f7e671fb19734c872566e57ce7fc235fa953d7c181bb4ef138e17d607dc8a1"}, + {file = "wrapt-1.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87fa943e8bbe40c8c1ba4086971a6fefbf75e9991217c55ed1bcb2f1985bd3d4"}, + {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4775a574e9d84e0212f5b18886cace049a42e13e12009bb0491562a48bb2b758"}, + {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d57677238a0c5411c76097b8b93bdebb02eb845814c90f0b01727527a179e4d"}, + {file = "wrapt-1.14.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00108411e0f34c52ce16f81f1d308a571df7784932cc7491d1e94be2ee93374b"}, + {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d332eecf307fca852d02b63f35a7872de32d5ba8b4ec32da82f45df986b39ff6"}, + {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:01f799def9b96a8ec1ef6b9c1bbaf2bbc859b87545efbecc4a78faea13d0e3a0"}, + {file = "wrapt-1.14.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47045ed35481e857918ae78b54891fac0c1d197f22c95778e66302668309336c"}, + {file = "wrapt-1.14.0-cp39-cp39-win32.whl", hash = "sha256:2eca15d6b947cfff51ed76b2d60fd172c6ecd418ddab1c5126032d27f74bc350"}, + {file = "wrapt-1.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:bb36fbb48b22985d13a6b496ea5fb9bb2a076fea943831643836c9f6febbcfdc"}, + {file = "wrapt-1.14.0.tar.gz", hash = "sha256:8323a43bd9c91f62bb7d4be74cc9ff10090e7ef820e27bfe8815c57e68261311"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..075b8cc3b92fd26a2f9b046539e2b2366ef78a78 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,54 @@ +[tool.poetry] +name = "backend" +version = "0.1.0" +description = "" +authors = ["Daze ", "Yugesh "] + +[tool.poetry.dependencies] +python = "^3.9" +fastapi = "^0.75.1" +passlib = {version = "^1.7.4", extras = ["bcrypt"]} +alembic = "^1.5.7" +python-multipart = "^0.0.5" +email-validator = "^1.1.2" +requests = "^2.25.1" +celery = "^5.0.5" +tenacity = "^7.0.0" +pydantic = "^1.8.1" +emails = "^0.6" +raven = "^6.10.0" +gunicorn = "^20.1.0" +psycopg2-binary = "^2.8.6" +SQLAlchemy = "^1.4.2" +pytest = "^6.2.2" +uvicorn = {version = "^0.17.6", extras = ["standard"]} +Faker = "^8.1.0" +Mako = "^1.1.4" +click = "^7.1.2" +aioredis = "^1.3.1" +aiofiles = "^0.6.0" +cryptography = "^36.0.2" +PyYAML = "^6.0" +Jinja2 = "^3.0.3" +pyotp = "^2.6.0" + +[tool.poetry.dev-dependencies] +mypy = "^0.812" +black = "^20.8b1" +isort = "^5.8.0" +autoflake = "^1.4" +flake8 = "^3.9.0" +pytest = "^6.2.2" +sqlalchemy-stubs = "^0.4" +pytest-cov = "^2.11.1" +pylint = "^2.7.2" +rope = "^0.18.0" +jedi = "^0.18.0" +autopep8 = "^1.5.6" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.pylint.'MESSAGES CONTROL'] +extension-pkg-whitelist = "pydantic" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..6f2b7cebcb0174cfe87ee0e7b7ccffdf9c788f87 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,76 @@ +aiofiles==0.6.0 ; python_version >= "3.9" and python_version < "4.0" +aioredis==1.3.1 ; python_version >= "3.9" and python_version < "4.0" +alembic==1.7.7 ; python_version >= "3.9" and python_version < "4.0" +amqp==5.1.0 ; python_version >= "3.9" and python_version < "4.0" +anyio==3.5.0 ; python_version >= "3.9" and python_version < "4.0" +asgiref==3.5.0 ; python_version >= "3.9" and python_version < "4.0" +async-timeout==4.0.2 ; python_version >= "3.9" and python_version < "4.0" +atomicwrites==1.4.0 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" +attrs==21.4.0 ; python_version >= "3.9" and python_version < "4.0" +bcrypt==3.2.0 ; python_version >= "3.9" and python_version < "4.0" +billiard==3.6.4.0 ; python_version >= "3.9" and python_version < "4.0" +cachetools==5.0.0 ; python_version >= "3.9" and python_version < "4.0" +celery==5.1.2 ; python_version >= "3.9" and python_version < "4.0" +certifi==2021.10.8 ; python_version >= "3.9" and python_version < "4.0" +cffi==1.15.0 ; python_version >= "3.9" and python_version < "4.0" +chardet==4.0.0 ; python_version >= "3.9" and python_version < "4.0" +charset-normalizer==2.0.12 ; python_version >= "3.9" and python_version < "4.0" +click-didyoumean==0.3.0 ; python_version >= "3.9" and python_version < "4.0" +click-plugins==1.1.1 ; python_version >= "3.9" and python_version < "4.0" +click-repl==0.2.0 ; python_version >= "3.9" and python_version < "4.0" +click==7.1.2 ; python_version >= "3.9" and python_version < "4.0" +colorama==0.4.4 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" +cryptography==36.0.2 ; python_version >= "3.9" and python_version < "4.0" +cssselect==1.1.0 ; python_version >= "3.9" and python_version < "4.0" +cssutils==2.4.0 ; python_version >= "3.9" and python_version < "4.0" +dnspython==2.2.1 ; python_version >= "3.9" and python_version < "4.0" +email-validator==1.1.3 ; python_version >= "3.9" and python_version < "4.0" +emails==0.6 ; python_version >= "3.9" and python_version < "4.0" +faker==8.16.0 ; python_version >= "3.9" and python_version < "4.0" +fastapi==0.75.1 ; python_version >= "3.9" and python_version < "4.0" +greenlet==1.1.2 ; python_version >= "3.9" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") and python_version < "4.0" +gunicorn==20.1.0 ; python_version >= "3.9" and python_version < "4.0" +h11==0.13.0 ; python_version >= "3.9" and python_version < "4.0" +redis==2.0.0 ; python_version >= "3.9" and python_version < "4.0" +httptools==0.4.0 ; python_version >= "3.9" and python_version < "4.0" +idna==3.3 ; python_version >= "3.9" and python_version < "4.0" +iniconfig==1.1.1 ; python_version >= "3.9" and python_version < "4.0" +jinja2==3.1.1 ; python_version >= "3.9" and python_version < "4.0" +kombu==5.2.4 ; python_version >= "3.9" and python_version < "4.0" +lxml==4.8.0 ; python_version >= "3.9" and python_version < "4.0" +mako==1.2.0 ; python_version >= "3.9" and python_version < "4.0" +markupsafe==2.1.1 ; python_version >= "3.9" and python_version < "4.0" +packaging==21.3 ; python_version >= "3.9" and python_version < "4.0" +passlib[bcrypt]==1.7.4 ; python_version >= "3.9" and python_version < "4.0" +pluggy==1.0.0 ; python_version >= "3.9" and python_version < "4.0" +premailer==3.10.0 ; python_version >= "3.9" and python_version < "4.0" +prompt-toolkit==3.0.29 ; python_version >= "3.9" and python_version < "4.0" +psycopg2-binary==2.9.3 ; python_version >= "3.9" and python_version < "4.0" +py==1.11.0 ; python_version >= "3.9" and python_version < "4.0" +pycparser==2.21 ; python_version >= "3.9" and python_version < "4.0" +pydantic==1.9.0 ; python_version >= "3.9" and python_version < "4.0" +pyotp==2.6.0 ; python_version >= "3.9" and python_version < "4.0" +pyparsing==3.0.8 ; python_version >= "3.9" and python_version < "4.0" +pytest==6.2.5 ; python_version >= "3.9" and python_version < "4.0" +python-dateutil==2.8.2 ; python_version >= "3.9" and python_version < "4.0" +python-dotenv==0.20.0 ; python_version >= "3.9" and python_version < "4.0" +python-multipart==0.0.5 ; python_version >= "3.9" and python_version < "4.0" +pytz==2022.1 ; python_version >= "3.9" and python_version < "4.0" +pyyaml==6.0 ; python_version >= "3.9" and python_version < "4.0" +raven==6.10.0 ; python_version >= "3.9" and python_version < "4.0" +requests==2.27.1 ; python_version >= "3.9" and python_version < "4.0" +six==1.16.0 ; python_version >= "3.9" and python_version < "4.0" +sniffio==1.2.0 ; python_version >= "3.9" and python_version < "4.0" +sqlalchemy==1.4.35 ; python_version >= "3.9" and python_version < "4.0" +starlette==0.17.1 ; python_version >= "3.9" and python_version < "4.0" +tenacity==7.0.0 ; python_version >= "3.9" and python_version < "4.0" +text-unidecode==1.3 ; python_version >= "3.9" and python_version < "4.0" +toml==0.10.2 ; python_version >= "3.9" and python_version < "4.0" +typing-extensions==4.1.1 ; python_version >= "3.9" and python_version < "4.0" +urllib3==1.26.9 ; python_version >= "3.9" and python_version < "4" +uvicorn[standard]==0.17.6 ; python_version >= "3.9" and python_version < "4.0" +uvloop==0.16.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.9" and python_version < "4.0" +vine==5.0.0 ; python_version >= "3.9" and python_version < "4.0" +watchgod==0.8.2 ; python_version >= "3.9" and python_version < "4.0" +wcwidth==0.2.5 ; python_version >= "3.9" and python_version < "4.0" +websockets==10.2 ; python_version >= "3.9" and python_version < "4.0" diff --git a/schemas/__init__.py b/schemas/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..67cd23df9b798750033e4eb20974c6161be964be --- /dev/null +++ b/schemas/__init__.py @@ -0,0 +1,81 @@ +from .class_session import ( + ClassSession, + ClassSessionUpdate, + ClassSessionCreate, + ClassSessionInDB, + AttendanceUpdate, +) +from .course import Course, CourseCreate, CourseUpdate, CourseInDB, CourseMin +from .department import Department, DepartmentCreate, DepartmentUpdate, DepartmentInDB +from .group import Group, GroupCreate, GroupInDB, GroupUpdate, GroupReturn +from .msg import Msg +from .personal_note import ( + PersonalNote, + PersonalNoteUpdate, + PersonalNoteCreate, + PersonalNoteInDB, +) +from .program import Program, ProgramUpdate, ProgramCreate, ProgramInDB +from .school import School, SchoolCreate, SchoolUpdate, SchoolInDB +from .teacher_note import ( + TeacherNote, + TeacherNoteUpdate, + TeacherNoteCreate, + TeacherNoteInDB, +) + +from .user_permission import ( + UserPermission, + UserPermissionCreate, + UserPermissionUpdate, + UserPermissionInDB, +) +from .token import Token, TokenPayload +from .two_fa import Two_FA_Confirm +from .user import ( + User, + UserCreate, + UserInDB, + UserUpdate, + UserReturnMin, + UserSignUp, + TeacherShort, + Name, +) + +from .quiz import ( + Quiz, + QuizCreate, + QuizUpdate, + QuizInDB, + QuizQuestion, + QuizQuestionCreate, + QuizQuestionUpdate, + QuizQuestionInDB, + QuizQuestionwoutAnswer, +) + +from .quiz_answer import ( + QuizAnswer, + QuizAnswerCreate, + QuizAnswerUpdate, + QuizAnswerInDB, + QuizAnsweronlySelected, + QuizAnswerwithName, +) + +from .file import File, FileCreate, FileUpdate, FileInDB +from .auth import LoginData + +from .assignment import Assignment, AssignmentCreate, AssignmentUpdate, AssignmentInDB + + +from .assignment_upload import ( + AssignmentUpload, + AssignmentUploadBase, + AssignmentUploadCreate, + AssignmentUploadInDB, + AssignmentUploadInDBBase, + AssignmentUploadUpdate, + AssignmentUploadwithName, +) diff --git a/schemas/__pycache__/__init__.cpython-310.pyc b/schemas/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c119195d3019867a81b690a51af9b4c33c895ee2 Binary files /dev/null and b/schemas/__pycache__/__init__.cpython-310.pyc differ diff --git a/schemas/__pycache__/assignment.cpython-310.pyc b/schemas/__pycache__/assignment.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4996e3a1ebd800eae9de5117bead2e05b6b0fe3e Binary files /dev/null and b/schemas/__pycache__/assignment.cpython-310.pyc differ diff --git a/schemas/__pycache__/assignment_upload.cpython-310.pyc b/schemas/__pycache__/assignment_upload.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6fed3f23e26e669384e402d266d753a4c8ba368 Binary files /dev/null and b/schemas/__pycache__/assignment_upload.cpython-310.pyc differ diff --git a/schemas/__pycache__/auth.cpython-310.pyc b/schemas/__pycache__/auth.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b884189121c3085d26e6236dab48a7ae1bbedac9 Binary files /dev/null and b/schemas/__pycache__/auth.cpython-310.pyc differ diff --git a/schemas/__pycache__/class_session.cpython-310.pyc b/schemas/__pycache__/class_session.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb70fc8676890f57d46c07070f3224e90729d64e Binary files /dev/null and b/schemas/__pycache__/class_session.cpython-310.pyc differ diff --git a/schemas/__pycache__/course.cpython-310.pyc b/schemas/__pycache__/course.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8d7da87c46ffd13e54b2bf6eb10668000902ea7 Binary files /dev/null and b/schemas/__pycache__/course.cpython-310.pyc differ diff --git a/schemas/__pycache__/department.cpython-310.pyc b/schemas/__pycache__/department.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b10ef5079834a5d5a0fec15a6cfb5f47386315b8 Binary files /dev/null and b/schemas/__pycache__/department.cpython-310.pyc differ diff --git a/schemas/__pycache__/file.cpython-310.pyc b/schemas/__pycache__/file.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..47f2f35386302ed83c9de6787da4177bccec8f86 Binary files /dev/null and b/schemas/__pycache__/file.cpython-310.pyc differ diff --git a/schemas/__pycache__/group.cpython-310.pyc b/schemas/__pycache__/group.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec599571456761eb9e2886a22dfc0b27fc31134e Binary files /dev/null and b/schemas/__pycache__/group.cpython-310.pyc differ diff --git a/schemas/__pycache__/msg.cpython-310.pyc b/schemas/__pycache__/msg.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e819b01fd685f2d51869758538fcaf7d1601cc7a Binary files /dev/null and b/schemas/__pycache__/msg.cpython-310.pyc differ diff --git a/schemas/__pycache__/personal_note.cpython-310.pyc b/schemas/__pycache__/personal_note.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0b02387b52bf806b4c91b3dea3528874720df5c3 Binary files /dev/null and b/schemas/__pycache__/personal_note.cpython-310.pyc differ diff --git a/schemas/__pycache__/program.cpython-310.pyc b/schemas/__pycache__/program.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a09f6aee4add856b61130df1614e0f018bae7650 Binary files /dev/null and b/schemas/__pycache__/program.cpython-310.pyc differ diff --git a/schemas/__pycache__/quiz.cpython-310.pyc b/schemas/__pycache__/quiz.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a77231c71bb2fe5b62b5492a0c64e52317efcd78 Binary files /dev/null and b/schemas/__pycache__/quiz.cpython-310.pyc differ diff --git a/schemas/__pycache__/quiz_answer.cpython-310.pyc b/schemas/__pycache__/quiz_answer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..070fcb6090933176421abff8dc4abb7550dc7c83 Binary files /dev/null and b/schemas/__pycache__/quiz_answer.cpython-310.pyc differ diff --git a/schemas/__pycache__/school.cpython-310.pyc b/schemas/__pycache__/school.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbdadc9a93c7fa3acfdaa1daea9c28155fd1858c Binary files /dev/null and b/schemas/__pycache__/school.cpython-310.pyc differ diff --git a/schemas/__pycache__/teacher_note.cpython-310.pyc b/schemas/__pycache__/teacher_note.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..678a721b9e2e21bf83d4ab4dab067ddb3e661b03 Binary files /dev/null and b/schemas/__pycache__/teacher_note.cpython-310.pyc differ diff --git a/schemas/__pycache__/token.cpython-310.pyc b/schemas/__pycache__/token.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb16f6258ec50849bfe13470b7656af5c4f98f84 Binary files /dev/null and b/schemas/__pycache__/token.cpython-310.pyc differ diff --git a/schemas/__pycache__/two_fa.cpython-310.pyc b/schemas/__pycache__/two_fa.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..da07ab96944a614dc369ee6faf12b7fd0897a384 Binary files /dev/null and b/schemas/__pycache__/two_fa.cpython-310.pyc differ diff --git a/schemas/__pycache__/user.cpython-310.pyc b/schemas/__pycache__/user.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..910c41925b72baabbd7a2d42c85d381fa2b7d18a Binary files /dev/null and b/schemas/__pycache__/user.cpython-310.pyc differ diff --git a/schemas/__pycache__/user_permission.cpython-310.pyc b/schemas/__pycache__/user_permission.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f9cfe9ab860dbe248c4c0c0a903e6d89f1428fb Binary files /dev/null and b/schemas/__pycache__/user_permission.cpython-310.pyc differ diff --git a/schemas/assignment.py b/schemas/assignment.py new file mode 100644 index 0000000000000000000000000000000000000000..fb33952e2eb9772122efdf706ea2a8be1410bceb --- /dev/null +++ b/schemas/assignment.py @@ -0,0 +1,59 @@ +from tokenize import group +from typing import Optional, List, Any # noqa + +from datetime import datetime +from pydantic import BaseModel +from schemas import TeacherShort, CourseMin, GroupReturn + + +class AssignmentBase(BaseModel): + due_date: datetime + marks: int = None + title: str + contents: str = None + files: List[Any] = None + instructor: List[int] + group: List[int] + course_id: int + + +class AssignmentCreate(AssignmentBase): + instructor: Optional[List[int]] + + +class AssignmentUpdate(AssignmentBase): + due_date: datetime = None + marks: int = None + title: str = None + contents: str = None + files: List[Any] = None + instructor: List[int] = None + group: List[int] = None + course_id: int = None + + +class AssignmentInDBBase(AssignmentBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class AssignmentInDB(AssignmentInDBBase): + pass + + +class Assignment(BaseModel): + id: Optional[int] + due_date: datetime = None + marks: int = None + title: str + contents: str = None + files: List[Any] = None + instructor: List[TeacherShort] + group: List[GroupReturn] + course: CourseMin + exists: Optional[bool] + + class Config: + orm_mode = True diff --git a/schemas/assignment_upload.py b/schemas/assignment_upload.py new file mode 100644 index 0000000000000000000000000000000000000000..865c49ded45b47db75e2afe71922a545df5fee42 --- /dev/null +++ b/schemas/assignment_upload.py @@ -0,0 +1,54 @@ +from tokenize import group +from typing import Optional, List, Any # noqa + +from datetime import datetime +from pydantic import BaseModel +from schemas import TeacherShort, CourseMin, GroupReturn, Name + + +class AssignmentUploadBase(BaseModel): + submission_date: datetime + marks_obtained: int = None + assignment_id: int + files: List[Any] = None + student_id: int + + +class AssignmentUploadCreate(AssignmentUploadBase): + pass + + +class AssignmentUploadUpdate(AssignmentUploadBase): + submission_date: Optional[datetime] + marks_obtained: Optional[int] + assignment_id: Optional[int] + files: List[Any] = None + student_id: Optional[int] + + +class AssignmentUploadInDBBase(AssignmentUploadBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class AssignmentUploadInDB(AssignmentUploadInDBBase): + pass + + +class AssignmentUpload(AssignmentUploadInDBBase): + pass + + +class AssignmentUploadwithName(BaseModel): + id: Optional[int] + submission_date: datetime + marks_obtained: int = None + assignment_id: int + files: List[Any] = None + student: Name + student_id: int + + class Config: + orm_mode = True diff --git a/schemas/auth.py b/schemas/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..475a604a1330c4d16cd9ef317a27d1aefb210921 --- /dev/null +++ b/schemas/auth.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel + + +class LoginData(BaseModel): + username: str + password: str + remember_me: bool + + +class ActiveSession(BaseModel): + ua: str + ip: str + diff --git a/schemas/class_session.py b/schemas/class_session.py new file mode 100644 index 0000000000000000000000000000000000000000..5c6dd642259b1573076eba083a6fa78672b42801 --- /dev/null +++ b/schemas/class_session.py @@ -0,0 +1,89 @@ +from datetime import datetime +from typing import Optional, List, Union + +from pydantic import BaseModel +from typing import Any +from schemas.user import UserReturnMin, TeacherShort +from schemas.course import Course +from schemas.file import FileClassSessionReturn + + +# shared properties +class ClassSessionBase(BaseModel): + start_time: datetime + end_time: datetime + instructor: List[int] + course_id: int + group_id: int + description: str + + +# properties to recieve via +class ClassSessionCreate(ClassSessionBase): + pass + + +# properties to recive via API on Update +class ClassSessionUpdate(ClassSessionBase): + start_time: datetime = None + end_time: datetime = None + instructor: List[int] = None + course_id: int = None + description: str = None + group_id: int = None + + +class AttendanceUpdate(BaseModel): + attendant: List[int] + + +# properties to return via the api +class ClassSessionInDBBase(ClassSessionBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class ClassSession(ClassSessionInDBBase): + instructor: List[TeacherShort] + + +class ClassSessionReturn(BaseModel): + id: int + start_time: datetime + end_time: datetime + instructor: List[TeacherShort] + course: Course + group_id: int + description: str + files: List[FileClassSessionReturn] = None + + class Config: + orm_mode = True + + +class AttendantOfClassSession(BaseModel): + id: str + full_name: str + roll: Optional[int] + + class Config: + orm_mode = True + + +class ClassSessionTeacherReturn(ClassSessionReturn): + attendant: List[AttendantOfClassSession] + + +class ClassSessionInDB(ClassSessionInDBBase): + pass + + +class ParticipantOfClassSession(BaseModel): + id: int + full_name: str + profile_image: Union[str, None] + + class Config: + orm_mode = True diff --git a/schemas/course.py b/schemas/course.py new file mode 100644 index 0000000000000000000000000000000000000000..b09328994b7033a5b42b8e70de54be6cbc3715b7 --- /dev/null +++ b/schemas/course.py @@ -0,0 +1,48 @@ +from typing import Optional + +from pydantic import BaseModel + + +# shared properties +class CourseBase(BaseModel): + course_code: str + course_name: str + course_credit: int + department_id: int + + +# properties to recieve via API on creation +class CourseCreate(CourseBase): + pass + + +# properties to recieve via API on update +class CourseUpdate(CourseBase): + course_code: Optional[str] + course_name: Optional[str] + course_credit: Optional[int] + department_id: Optional[int] + + +class CourseInDBBase(CourseBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class CourseInDB(CourseInDBBase): + pass + + +class Course(CourseInDBBase): + pass + + +class CourseMin(BaseModel): + id: Optional[int] + course_code: str + course_name: str + + class Config: + orm_mode = True \ No newline at end of file diff --git a/schemas/department.py b/schemas/department.py new file mode 100644 index 0000000000000000000000000000000000000000..472e5ae64b89fce3b66800cfcb11a744c6b074d6 --- /dev/null +++ b/schemas/department.py @@ -0,0 +1,31 @@ +from typing import Optional # noqa + +from pydantic import BaseModel + + +class DepartmentBase(BaseModel): + name: str + school_id: Optional[int] + + +class DepartmentCreate(DepartmentBase): + pass + + +class DepartmentUpdate(DepartmentBase): + pass + + +class DepartmentInDBBase(DepartmentBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class DepartmentInDB(DepartmentInDBBase): + pass + + +class Department(DepartmentInDBBase): + pass diff --git a/schemas/file.py b/schemas/file.py new file mode 100644 index 0000000000000000000000000000000000000000..9ee8b50d6a3aecbf2804bcdc66668e51506dd444 --- /dev/null +++ b/schemas/file.py @@ -0,0 +1,47 @@ +from typing import Optional # noqa + +from pydantic import BaseModel +from datetime import datetime + + +class FileBase(BaseModel): + name:str + path: str + file_type: str + uploaded_datetime: datetime = None + description: str = None + class_session_id: int + + +class FileCreate(FileBase): + pass + + +class FileUpdate(FileBase): + pass + + +class FileInDBBase(FileBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class FileInDB(FileInDBBase): + pass + + +class File(FileInDBBase): + pass + + +class FileClassSessionReturn(BaseModel): + name: str + path: str + file_type: str + uploaded_datetime: datetime = None + description: str = None + + class Config: + orm_mode = True diff --git a/schemas/group.py b/schemas/group.py new file mode 100644 index 0000000000000000000000000000000000000000..a2a88bd299dab6be02c2c944268e9d34d8f91b5e --- /dev/null +++ b/schemas/group.py @@ -0,0 +1,83 @@ +from schemas.program import Program +from typing import Optional, List, Union # noqa + +from pydantic import BaseModel +from .course import Course + + +class GroupBase(BaseModel): + program_id: int + sem: int + course: List[int] + + +class GroupCreate(BaseModel): + program_id: int + sem: int + course: Optional[List[int]] + + +class GroupUpdate(GroupBase): + pass + + +class GroupInDBBase(GroupBase): + id: Optional[int] + course: List[Course] + + class Config: + orm_mode = True + + +class GroupInDB(GroupInDBBase): + pass + + +class Group(GroupInDBBase): + pass + + +class StudentOfGroup(BaseModel): + id: int + full_name: str + profile_image: Union[str, None] + + class Config: + orm_mode = True + + +class GroupStudentReturn(BaseModel): + id: Optional[int] + student: List[StudentOfGroup] + + class Config: + orm_mode = True + + +class GroupSignInReturn(BaseModel): + id: Optional[int] + sem: int + program: Program + course: List[Course] + + class Config: + orm_mode = True + + +class GroupReturn(BaseModel): + id: Optional[int] + sem: int + program: Program + + class Config: + orm_mode = True + + +class GroupWithProgram(BaseModel): + id: Optional[int] + sem: int + program: Program + course: List[Course] + + class Config: + orm_mode = True diff --git a/schemas/msg.py b/schemas/msg.py new file mode 100644 index 0000000000000000000000000000000000000000..945e0c627bc183f8a0ee0aa736c0a1d8d54cd895 --- /dev/null +++ b/schemas/msg.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class Msg(BaseModel): + msg: str diff --git a/schemas/personal_note.py b/schemas/personal_note.py new file mode 100644 index 0000000000000000000000000000000000000000..598b1c73f2711f85ed17aa653ad47f5d927939b1 --- /dev/null +++ b/schemas/personal_note.py @@ -0,0 +1,47 @@ +from datetime import date, datetime +from typing import Optional, List +from datetime import datetime + + +from pydantic import BaseModel + + +# shared properties +class PersonalNoteBase(BaseModel): + user_id: int + tags: List[str] = None + title: str + content: str + last_updated_time: datetime + + +# properties to recieve via +class PersonalNoteCreate(BaseModel): + user_id: int + tags: List[str] = None + title: str + content: str + + +# properties to recive via API on Update +class PersonalNoteUpdate(BaseModel): + user_id: int = None + tags: List[str] = None + title: str = None + content: str = None + + +# properties to return via the api +class PersonalNoteInDBBase(PersonalNoteBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class PersonalNoteInDB(PersonalNoteInDBBase): + pass + + +class PersonalNote(PersonalNoteInDBBase): + pass diff --git a/schemas/program.py b/schemas/program.py new file mode 100644 index 0000000000000000000000000000000000000000..a53b86134f43a84530e5c47cfce49b4f907339b5 --- /dev/null +++ b/schemas/program.py @@ -0,0 +1,53 @@ +from typing import Optional, List + +from pydantic import BaseModel + + +# properties shared +class ProgramBase(BaseModel): + name: str + department_id: int + + +# properties to recieve via +class ProgramCreate(ProgramBase): + max_sems: int + + +# properties to recive via API on Update +class ProgramUpdate(ProgramBase): + name: Optional[str] + department_id: Optional[int] + + +# properties to return via the api +class ProgramInDBBase(ProgramBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class ProgramInDB(ProgramInDBBase): + pass + + +class Program(ProgramInDBBase): + pass + + +class GroupOfProgram(BaseModel): + id: int + sem: int + + class Config: + orm_mode = True + + +class ProgramGroupReturn(BaseModel): + name: str + department_id: int + groups: List[GroupOfProgram] + + class Config: + orm_mode = True diff --git a/schemas/quiz.py b/schemas/quiz.py new file mode 100644 index 0000000000000000000000000000000000000000..93f357cb1193351412a59b60b388c8d2b9913213 --- /dev/null +++ b/schemas/quiz.py @@ -0,0 +1,119 @@ +from datetime import datetime +from typing import Optional, List, Dict # noqa + +from pydantic import BaseModel, Json +from schemas import GroupReturn, CourseMin, TeacherShort + + +class QuizBase(BaseModel): + end_time: datetime + start_time: datetime + title: str + description: str + is_randomized: bool + display_individual: bool + instructor: List[int] + group: List[int] + course_id: int + total_marks: int = None + + +class QuizCreate(QuizBase): + pass + + +class QuizUpdate(QuizBase): + end_time: datetime = None + start_time: datetime = None + title: str = None + description: str = None + is_randomized: bool = None + display_individual: bool = None + instructor: List[int] = None + group: List[int] = None + course_id: int = None + total_marks: int = None + + +class QuizInDBBase(QuizBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class QuizInDB(QuizInDBBase): + pass + + +class Quiz(BaseModel): + id: Optional[int] + course: CourseMin + end_time: datetime + start_time: datetime + title: str + description: str + is_randomized: bool + display_individual: bool + total_marks: int + group: List[GroupReturn] + instructor: List[TeacherShort] + + class Config: + orm_mode = True + + +# XXX +# XXX +# Quiz Question schema + + +class QuizQuestionBase(BaseModel): + question_text: str = None + question_image: List[str] = None + options: Json + answer: List[int] = None + quiz_id: int + marks: int = None + + +class QuizQuestionCreate(QuizQuestionBase): + pass + + +class QuizQuestionUpdate(QuizQuestionBase): + question_text: str = None + question_image: List[str] = None + options: Json = None + answer: List[int] = None + marks: int = None + quiz_id: int + + +class QuizQuestionInDBBase(QuizQuestionBase): + id: Optional[int] + options: List[Dict[str, str]] + + class Config: + orm_mode = True + + +class QuizQuestionInDB(QuizQuestionInDBBase): + pass + + +class QuizQuestion(QuizQuestionInDBBase): + pass + + +class QuizQuestionwoutAnswer(BaseModel): + id: Optional[int] + question_text: str = None + question_image: List[str] = None + options: List[Dict[str, str]] + quiz_id: int + multiple: bool = False + marks: int + + class Config: + orm_mode = True diff --git a/schemas/quiz_answer.py b/schemas/quiz_answer.py new file mode 100644 index 0000000000000000000000000000000000000000..c5ce8df80d634abd056ed2c4cff4aa3a977e47f6 --- /dev/null +++ b/schemas/quiz_answer.py @@ -0,0 +1,59 @@ +from typing import Optional, List, Dict, Any # noqa + +from pydantic import BaseModel + +from schemas import Name + + +class QuizAnswerBase(BaseModel): + marks_obtained: int = None + options_selected: Dict[int, Any] + quiz_id: int + student_id: int + + +class QuizAnswerCreate(QuizAnswerBase): + pass + + +class QuizAnswerUpdate(QuizAnswerBase): + marks_obtained: int = None + options_selected: Dict[int, Any] = None + quiz_id: int = None + student_id: int = None + + +class QuizAnswerInDBBase(QuizAnswerBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class QuizAnswerInDB(QuizAnswerInDBBase): + pass + + +class QuizAnswer(QuizAnswerInDBBase): + pass + + +class QuizAnsweronlySelected(BaseModel): + id: Optional[int] + options_selected: Dict[int, Any] + quiz_id: int + student_id: int + + class Config: + orm_mode = True + + +class QuizAnswerwithName(BaseModel): + id: Optional[int] + marks_obtained: int + options_selected: Dict[int, Any] + quiz_id: int + student: Name + + class Config: + orm_mode = True diff --git a/schemas/school.py b/schemas/school.py new file mode 100644 index 0000000000000000000000000000000000000000..048ef5d5e2d1a3ca13daf77832841de3213752a9 --- /dev/null +++ b/schemas/school.py @@ -0,0 +1,31 @@ +from typing import Optional # noqa + +from pydantic import BaseModel + + +class SchoolBase(BaseModel): + name: str + address: str + + +class SchoolCreate(SchoolBase): + pass + + +class SchoolUpdate(SchoolBase): + pass + + +class SchoolInDBBase(SchoolBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class SchoolInDB(SchoolInDBBase): + pass + + +class School(SchoolInDBBase): + pass diff --git a/schemas/teacher_note.py b/schemas/teacher_note.py new file mode 100644 index 0000000000000000000000000000000000000000..b0e529bff3dcf0d3e78593799d753b58a0c87c2a --- /dev/null +++ b/schemas/teacher_note.py @@ -0,0 +1,37 @@ +from typing import Optional + +from pydantic import BaseModel + + +# shared properties +class TeacherNoteBase(BaseModel): + user_id: int + student_id: int + message: str + + +# properties to recieve via +class TeacherNoteCreate(TeacherNoteBase): + student_id: int + message: str + + +# properties to recive via API on Update +class TeacherNoteUpdate(TeacherNoteBase): + pass + + +# properties to return via the api +class TeacherNoteInDBBase(TeacherNoteBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class TeacherNoteInDB(TeacherNoteInDBBase): + pass + + +class TeacherNote(TeacherNoteInDBBase): + pass diff --git a/schemas/token.py b/schemas/token.py new file mode 100644 index 0000000000000000000000000000000000000000..27a13680121384f30d7e4a0d0e026f5257f3fbb7 --- /dev/null +++ b/schemas/token.py @@ -0,0 +1,15 @@ +from typing import Optional + +from pydantic import BaseModel + + +class Token(BaseModel): + session: str + + +class TokenPayload(BaseModel): + sub: Optional[int] = None + + +class SessionToken(BaseModel): + session_token: str diff --git a/schemas/two_fa.py b/schemas/two_fa.py new file mode 100644 index 0000000000000000000000000000000000000000..456d3a14d9df51d6661749600a3954a02cddc565 --- /dev/null +++ b/schemas/two_fa.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class Two_FA_Confirm(BaseModel): + totp: int diff --git a/schemas/user.py b/schemas/user.py new file mode 100644 index 0000000000000000000000000000000000000000..6298579fc8cbebb0a33b2bb10a0a347e333cd796 --- /dev/null +++ b/schemas/user.py @@ -0,0 +1,160 @@ +from typing import Any, Optional, List +from datetime import date +from schemas.program import ProgramInDB +from schemas.group import GroupSignInReturn, Group +from schemas.course import CourseInDB +from core.config import settings + +from pydantic import BaseModel, EmailStr + + +# Shared properties +class UserBase(BaseModel): + email: Optional[EmailStr] = None + full_name: Optional[str] = None + user_type: int + address: str = None + group_id: int = None + roll: int = None + teacher_department_id: int = None + contact_number: str = None + dob: date = None + teacher_group: List[List[int]] = None + join_year: Optional[int] = None + + +class UserSignUp(BaseModel): + email: Optional[EmailStr] = None + full_name: Optional[str] = None + address: str = None + group_id: int = None + contact_number: str = None + dob: date = None + join_year: Optional[int] = None + password: str + + +# Properties to receive via API on creation +class UserCreate(UserBase): + email: EmailStr + user_type: int = settings.UserType.STUDENT.value + password: str + + +class AdminUserCreate(BaseModel): + email: EmailStr = None + full_name: str = None + address: Optional[str] = None + group_id: Optional[int] = None + roll: Optional[int] = None + contact_number: str = None + dob: date = None + join_year: Optional[int] = None + + +class VerifyUser(BaseModel): + is_active: bool + + +class UserReturnMin(BaseModel): + id: int + + class Config: + orm_mode = True + + +class TeacherShort(BaseModel): + id: int + full_name: str + profile_image: Optional[str] + + class Config: + orm_mode = True + + +# Properties to receive via API on update +class UserUpdate(BaseModel): + full_name: Optional[str] + address: Optional[str] + group_id: Optional[int] + dob: Optional[date] + contact_number: Optional[str] + profile_image: Optional[str] + + +class PasswordUpdate(BaseModel): + password: str + + +class UserInDBBase(UserBase): + id: Optional[int] = None + teacher_group: Optional[List[Group]] + profile_image: Optional[str] = None + + class Config: + orm_mode = True + + +# Additional properties to return via API +class User(UserInDBBase): + pass + + +class GroupOfTeacherGroupOfUser(BaseModel): + id: int + sem: int + program: ProgramInDB + + class Config: + orm_mode = True + + +class TeacherGroupOfUser(BaseModel): + group_id: int + group: GroupOfTeacherGroupOfUser + course: CourseInDB + + class Config: + orm_mode = True + + +class UserReturn(BaseModel): + id: int = None + email: Optional[EmailStr] = None + profile_image: Optional[str] = None + full_name: Optional[str] = None + address: str = None + roll: int = None + group: GroupSignInReturn = None + teacher_group: List[TeacherGroupOfUser] = None + contact_number: str = None + dob: date = None + user_type: int = None + join_year: Optional[int] = None + is_active: bool = None + + class Config: + orm_mode = True + + +class UserLoginReturn(BaseModel): + user: Optional[UserReturn] + msg: str + two_fa_required: Optional[bool] + + class Config: + orm_mode = True + + +# Additional properties stored in DB +class UserInDB(UserInDBBase): + hashed_password: str + + +class Name(BaseModel): + id: int + full_name: str + profile_image: Optional[str] + + class Config: + orm_mode = True diff --git a/schemas/user_permission.py b/schemas/user_permission.py new file mode 100644 index 0000000000000000000000000000000000000000..2d61ddbb89b3e3fd398ef4f5ce5484f21373320b --- /dev/null +++ b/schemas/user_permission.py @@ -0,0 +1,34 @@ +from typing import Optional + +from pydantic import BaseModel + + +# shared properties +class UserPermissionBase(BaseModel): + name: str + + +# properties to recieve via +class UserPermissionCreate(UserPermissionBase): + pass + + +# properties to recive via API on Update +class UserPermissionUpdate(UserPermissionBase): + pass + + +# properties to return via the api +class UserPermissionInDBBase(UserPermissionBase): + id: Optional[int] + + class Config: + orm_mode = True + + +class UserPermissionInDB(UserPermissionInDBBase): + pass + + +class UserPermission(UserPermissionInDBBase): + pass diff --git a/templates/crud.pyt b/templates/crud.pyt new file mode 100644 index 0000000000000000000000000000000000000000..0be90835f7599d3bb7c2c4fdf154f95a955e861e --- /dev/null +++ b/templates/crud.pyt @@ -0,0 +1,10 @@ +from cruds.base import CRUDBase +from schemas.${snake_case_name} import ${PascalCaseName}Create, ${PascalCaseName}Update +from models.${snake_case_name} import ${PascalCaseName} + + +class CRUD${PascalCaseName}(CRUDBase[${PascalCaseName}, ${PascalCaseName}Create, ${PascalCaseName}Update]): + pass + + +crud_${snake_case_name} = CRUD${PascalCaseName}(${PascalCaseName}) diff --git a/templates/email-templates/reset-password.html b/templates/email-templates/reset-password.html new file mode 100644 index 0000000000000000000000000000000000000000..f17deadb0ab985967e172e1fbe2f5c06cd302992 --- /dev/null +++ b/templates/email-templates/reset-password.html @@ -0,0 +1,117 @@ + + + + + + + Reset Password + + + +
+ +
+
+
+

+ Hi {{name}}, did you want to reset your password? +

+
+

+ Someone (hopefully you) has asked us to reset the password + for your Sikshyalaya account. Please click the button below + to do so. If you didn't request this password reset, you can + go ahead and ignore this email!

+ Still have questions? Please contact the School Admin. +

+
+
+
+ +

+ or click this link: {{link}} +

+
+ +
+ + diff --git a/templates/email-templates/test_email.html b/templates/email-templates/test_email.html new file mode 100644 index 0000000000000000000000000000000000000000..5a3332f0cf591b2dfba75119704773a0eb4ebe98 --- /dev/null +++ b/templates/email-templates/test_email.html @@ -0,0 +1,131 @@ + + + + + + + Reset Password + + + + +
+ +
+
+
+

+ Hi {{name}}, did you want to reset your password? +

+
+

+ Someone (hopefully you) has asked us to reset the password + for your Sikshyalaya account. Please click the button below + to do so. If you didn't request this password reset, you can + go ahead and ignore this email!

+ Still have questions? Please contact the School Admin. +

+
+
+
+ +

+ or click this link: {{link}} +

+
+ +
+ + diff --git a/templates/email-templates/verify-account.html b/templates/email-templates/verify-account.html new file mode 100644 index 0000000000000000000000000000000000000000..ffc29f225c8f8d7ebce05e0973d11d1840a0288d --- /dev/null +++ b/templates/email-templates/verify-account.html @@ -0,0 +1,125 @@ + + + + + + + + Verify Account + + + + +
+ +
+
+
+

+ Hi {{name}}, +


+

You have created account on Sikshyalaya. Please + click the button below to verify your ownership of this email. + Your account cannot be verified until you do so. +

+
+
+
+ +

or click this link: {{link}}

+
+ +
+ + + + diff --git a/templates/endpoint.pyt b/templates/endpoint.pyt new file mode 100644 index 0000000000000000000000000000000000000000..141bfb1cd3e746a360edfbcd51b5706103dad271 --- /dev/null +++ b/templates/endpoint.pyt @@ -0,0 +1,41 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from utils import deps +from cruds import crud_${snake_case_name} +from schemas import ${PascalCaseName}, ${PascalCaseName}Update, ${PascalCaseName}Create + +router = APIRouter() + + +@router.get("/${snake_case_name}", response_model=List[${PascalCaseName}]) +async def get_${snake_case_name}( + db: Session = Depends(deps.get_db), skip: int = 0, limit: int = 100 +) -> Any: + ${snake_case_name} = crud_${snake_case_name}.get_multi(db, skip=skip, limit=limit) + return ${snake_case_name} + + +@router.post("/${snake_case_name}", response_model=${PascalCaseName}) +async def create_${snake_case_name}( + db: Session = Depends(deps.get_db), *, obj_in: ${PascalCaseName}Create +) -> Any: + ${snake_case_name} = crud_${snake_case_name}.create(db, obj_in=obj_in) + return ${snake_case_name} + + +@router.get("/${snake_case_name}/{id}", response_model=${PascalCaseName}) +async def get_specific_${snake_case_name}(db: Session = Depends(deps.get_db), *, id: int) -> Any: + ${snake_case_name} = crud_${snake_case_name}.get(db, id) + return ${snake_case_name} + + +@router.put("/${snake_case_name}/{id}", response_model=${PascalCaseName}) +async def update_${snake_case_name}( + db: Session = Depends(deps.get_db), *, id: int, obj_in: ${PascalCaseName}Update +) -> Any: + ${snake_case_name} = crud_${snake_case_name}.get(db, id) + ${snake_case_name} = crud_${snake_case_name}.update(db, db_obj=${snake_case_name}, obj_in=obj_in) + return ${snake_case_name} diff --git a/templates/model.pyt b/templates/model.pyt new file mode 100644 index 0000000000000000000000000000000000000000..c2ccd7bb6004ec0c173cf3ffdd692534a6c9b406 --- /dev/null +++ b/templates/model.pyt @@ -0,0 +1,12 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship + +from core.db import Base + + + +class ${PascalCaseName}(Base): + id = Column(Integer, primary_key=True) + __tablename__ = "${snake_case_name}" # noqa diff --git a/templates/schema.pyt b/templates/schema.pyt new file mode 100644 index 0000000000000000000000000000000000000000..7e4943a903f002a219881dc67da6d49ac6bcce8c --- /dev/null +++ b/templates/schema.pyt @@ -0,0 +1,31 @@ +from typing import Optional # noqa + +from pydantic import BaseModel + + +class ${PascalCaseName}Base(BaseModel): + name: str + address: str + + +class ${PascalCaseName}Create(${PascalCaseName}Base): + pass + + +class ${PascalCaseName}Update(${PascalCaseName}Base): + pass + + +class ${PascalCaseName}InDBBase(${PascalCaseName}Base): + id: Optional[int] + + class Config: + orm_mode = True + + +class ${PascalCaseName}InDB(${PascalCaseName}InDBBase): + pass + + +class ${PascalCaseName}(${PascalCaseName}InDBBase): + pass diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..16d3c4dbbfec555a7690fe6a0f9f812de215fab2 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +.cache diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/api/api_v1/__init__.py b/tests/api/api_v1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/api/api_v1/test_auth.py b/tests/api/api_v1/test_auth.py new file mode 100644 index 0000000000000000000000000000000000000000..d5c9c2d011679052248f71cc9a56255fd30c5a53 --- /dev/null +++ b/tests/api/api_v1/test_auth.py @@ -0,0 +1,266 @@ +import json +import time +import base64 + +import pytest +import requests + +from core.config import settings +from fastapi.testclient import TestClient +import email +from cruds import crud_user +import re +import os + + +MAILHOG_URL = f"http://{settings.SMTP_HOST}:8025/api" + + +headers = { + "accept": "application/json", + "Content-Type": "application/json", +} + + +def test_signup(client: TestClient) -> None: + mailhog_history_clear = requests.delete(f"{MAILHOG_URL}/v1/messages") + assert mailhog_history_clear.status_code == 200, "Mailhog email delete failed" + + data = { + "email": "test_student@test.local", + "full_name": "Test User", + "address": "Testland", + "group_id": 1, + "contact_number": "9841111111", + "dob": "2000-01-01", + "join_year": 2015, + "password": "testold", + } + + response = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/signup/", + headers=headers, + json=data, + ) + + assert response.status_code == 200 + + +def test_verification_email(client: TestClient) -> None: + mailhog_email_req = requests.get(f"{MAILHOG_URL}/v2/messages") + + assert mailhog_email_req.status_code == 200, "Mailhog email request failed" + + emails = mailhog_email_req.json() + assert emails.get("total") == 1, f'Received {emails.get("total")} emails! Expected 1!' + + verification_email = emails.get("items")[0] + + content = email.message_from_string(verification_email["Raw"]["Data"]) + decoded_email_content = None + for part in content.walk(): + payload = part.get_payload(decode=True) + if payload: + decoded_email_content = payload.decode() + + link = re.findall( + settings.FRONTEND_URL_BASE+"/verify\?token=.{40}", decoded_email_content + )[0] + token = link[-40:] + + assert token, "Token of length 40 not found in the email" + + params = {"token": token} + verify_req = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/verify/", + params=params, + ) + + assert verify_req.status_code == 200 + + +def test_web_session_authentication( + client: TestClient, username=None, password=None, remember_me=None +) -> None: + data = { + "username": username or "test_student@test.local", + "password": password or "testold", + "remember_me": remember_me or True, + } + + req = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/web/", + json=data, + headers=headers, + ) + + assert req.status_code == 200 + assert req.cookies.get("session"), "Cookie not returned!" + + +def test_web_session_authentication_fail(client: TestClient) -> None: + data = { + "username": "this.user.doesnt.exist@unknown.com", + "password": "test123", + "remember_me": True, + } + + req = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/web/", + json=data, + headers=headers, + ) + + assert req.status_code == 401 + assert not req.cookies.get("session"), "Cookie returned on login fail!" + + +def test_send_reset_email(client: TestClient) -> None: + data = { + "email": "test_student@test.local", + } + + response = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/password-recovery/", + headers=headers, + params=data, + ) + + assert response.status_code == 200 + + +def test_reset_password(client: TestClient) -> None: + + mailhog_email_req = requests.get(f"{MAILHOG_URL}/v2/messages") + assert mailhog_email_req.status_code == 200, "Mailhog email request failed" + + emails = mailhog_email_req.json() + + reset_email = emails.get("items")[0] + + content = email.message_from_string(reset_email["Raw"]["Data"]) + decoded_email_content = None + for part in content.walk(): + payload = part.get_payload(decode=True) + if payload: + decoded_email_content = payload.decode() + + link = re.findall( + settings.FRONTEND_URL_BASE+"/reset\?token=.{40}", decoded_email_content + )[0] + token = link[-40:] + + assert token, "Token of length 40 not found in the email" + + data = {"token": token, "new_password": "test"} + + reset_response = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/reset-password/", + headers=headers, + json=data, + ) + + assert reset_response.status_code == 200 + + +def test_login_with_old_password(client: TestClient): + data = { + "username": "test_student@test.local", + "password": "testold", + "remember_me": True, + } + + req = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/web/", + json=data, + headers=headers, + ) + + assert req.status_code != 200, "Logged in with old password after reset" + assert not req.cookies.get("session"), "Cookie returned with old password" + + +def test_login_with_new_password(client: TestClient): + data = { + "username": "test_student@test.local", + "password": "test", + "remember_me": True, + } + + req = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/web/", + json=data, + headers=headers, + ) + + assert req.status_code == 200, "Couldn't login with new password" + assert req.cookies.get("session"), "Cookie not returned with new password" + + +def test_change_password(client: TestClient): + wrong_data = { + "current_password": "testwrong", + "new_password": "newtest", + } + + req = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/change-password/", + json=wrong_data, + headers=headers, + ) + + assert req.status_code != 200 + + data = { + "current_password": "test", + "new_password": "newtest", + } + + req = client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/change-password/", + json=data, + headers=headers, + ) + + assert req.status_code == 200 + + test_web_session_authentication( + client=client, + username="test_student@test.local", + password="newtest", + remember_me=True, + ) + + +def test_logout(client: TestClient) -> None: + response = client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/logout/", + headers=headers, + ) + + assert response.status_code == 200 + + +def test_delete_user(db, super_user_client: TestClient): + data = { + "username": "test_superadmin@test.local", + "password": "test", + "remember_me": False, + } + + req = super_user_client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/web/", + json=data, + headers=headers, + ) + + assert req.status_code == 200, "Superadmin login failed" + assert req.cookies.get("session"), "Superadmin cookie not returned!" + + user = crud_user.get_by_email(db, email="test_student@test.local") + + delete_req = super_user_client.delete( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/users/{user.id}/", + ) + + assert delete_req.status_code == 200 diff --git a/tests/api/api_v1/test_course.py b/tests/api/api_v1/test_course.py new file mode 100644 index 0000000000000000000000000000000000000000..f71ed9d8d2ce7363c4d0217e84a49a3d6d796bd1 --- /dev/null +++ b/tests/api/api_v1/test_course.py @@ -0,0 +1,153 @@ +from core.config import settings +from api.endpoints.course import create_course +from tests.api.api_v1 import test_department +from fastapi.encoders import jsonable_encoder +import models + +from . import test_department + + +headers = { + "accept": "application/json", + "Content-Type": "application/json", +} + + +def test_post_course(super_user_client, db): + test_department.test_post_department(super_user_client, db) + department_id = ( + db.query(models.Department) + .filter(models.Department.name == "Test Department") + .first() + .id + ) + + data = { + "course_code": "TEST101", + "course_name": "Test Course", + "course_credit": 3, + "department_id": department_id, + } + + post_req = super_user_client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/", + json=data, + ) + assert post_req.status_code == 200, "Course post request failed" + + +def test_get_course(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/", + ) + assert get_req.status_code == 200 + + courses = get_req.json() + print(courses) + created_course = [ + course for course in courses if (course.get("course_name") == "Test Course") + ] + assert len(created_course) == 1 + + +def test_get_specific_course(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/", + ) + assert get_req.status_code == 200 + + courses = get_req.json() + created_course = [ + course for course in courses if (course.get("course_name") == "Test Course") + ] + + specific_get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/{created_course[0]['id']}/", + ) + + assert specific_get_req.status_code == 200 + + created_course_specific = specific_get_req.json() + assert created_course_specific + assert created_course_specific.get("course_name") == "Test Course" + + +def test_put_specific_course(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/", + ) + assert get_req.status_code == 200 + + courses = get_req.json() + created_course = [ + course for course in courses if (course.get("course_name") == "Test Course") + ] + + data = { + "course_name": "Updated Course", + "course_code": "TEST102", + "course_credit": 2, + } + + put_req = super_user_client.put( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/{created_course[0]['id']}/", + json=data, + ) + + assert put_req.status_code == 200 + + specific_get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/{created_course[0]['id']}/", + ) + + assert specific_get_req.status_code == 200 + + created_course_specific = specific_get_req.json() + assert created_course_specific + assert created_course_specific.get("course_name") == "Updated Course" + assert created_course_specific.get("course_code") == "TEST102" + assert created_course_specific.get("course_credit") == 2 + + +def test_delete_course(super_user_client, course_id=None): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/", + ) + assert get_req.status_code == 200 + + courses = get_req.json() + if not course_id: + created_course = [ + course + for course in courses + if (course.get("course_name") == "Updated Course") + ] + else: + created_course = [ + course for course in courses if (course.get("id") == course_id) + ] + + delete_req = super_user_client.delete( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/{created_course[0]['id']}/", + ) + + assert delete_req.status_code == 200 + + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/course/", + ) + assert get_req.status_code == 200 + + courses = get_req.json() + filtered_courses = [ + course + for course in courses + if ( + course.get("name") == "Updated Course" + or course.get("course_name") == "Test Course" + ) + ] + assert not filtered_courses + test_department.test_delete_department( + super_user_client, department_id=created_course[0]["department_id"] + ) diff --git a/tests/api/api_v1/test_department.py b/tests/api/api_v1/test_department.py new file mode 100644 index 0000000000000000000000000000000000000000..542d49d26b7b83f88f0725dfb346fdd3de03ac16 --- /dev/null +++ b/tests/api/api_v1/test_department.py @@ -0,0 +1,145 @@ +from fastapi.encoders import jsonable_encoder +import models + +from core.config import settings +from . import test_school + + +headers = { + "accept": "application/json", + "Content-Type": "application/json", +} + + +def test_post_department(super_user_client, db): + test_school.test_post_school(super_user_client) + school_id = ( + db.query(models.School).filter(models.School.name == "Test School").first().id + ) + + data = {"name": "Test Department", "school_id": school_id} + post_req = super_user_client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/", + json=data, + ) + assert post_req.status_code == 200, "Department post request failed" + + +def test_get_department(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/", + ) + assert get_req.status_code == 200 + + departments = get_req.json() + created_department = [ + department + for department in departments + if (department.get("name") == "Test Department") + ] + assert len(created_department) == 1 + + +def test_get_specific_department(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/", + ) + assert get_req.status_code == 200 + + departments = get_req.json() + created_department = [ + department + for department in departments + if (department.get("name") == "Test Department") + ] + + specific_get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/{created_department[0]['id']}/", + ) + + assert specific_get_req.status_code == 200 + + created_department_specific = specific_get_req.json() + assert created_department_specific + assert created_department_specific.get("name") == "Test Department" + + +def test_put_specific_department(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/", + ) + assert get_req.status_code == 200 + + departments = get_req.json() + created_department = [ + department + for department in departments + if (department.get("name") == "Test Department") + ] + + data = { + "name": "Updated Department", + } + + put_req = super_user_client.put( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/{created_department[0]['id']}/", + json=data, + ) + + assert put_req.status_code == 200 + + specific_get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/{created_department[0]['id']}/", + ) + + assert specific_get_req.status_code == 200 + + created_department_specific = specific_get_req.json() + assert created_department_specific + assert created_department_specific.get("name") == "Updated Department" + + +def test_delete_department(super_user_client, department_id=None): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/", + ) + assert get_req.status_code == 200 + + departments = get_req.json() + if not department_id: + created_department = [ + department + for department in departments + if (department.get("name") == "Updated Department") + ] + else: + created_department = [ + department + for department in departments + if (department.get("id") == department_id) + ] + + delete_req = super_user_client.delete( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/{created_department[0]['id']}/", + ) + + assert delete_req.status_code == 200 + + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/department/", + ) + assert get_req.status_code == 200 + + departments = get_req.json() + filtered_department = [ + department + for department in departments + if ( + department.get("name") == "Updated School" + or department.get("name") == "Test Department" + ) + ] + assert not filtered_department + test_school.test_delete_school( + super_user_client, school_id=created_department[0]["school_id"] + ) diff --git a/tests/api/api_v1/test_program.py b/tests/api/api_v1/test_program.py new file mode 100644 index 0000000000000000000000000000000000000000..54faf48a7af632ffcd51bc1eb955d314854b8376 --- /dev/null +++ b/tests/api/api_v1/test_program.py @@ -0,0 +1,141 @@ +from fastapi.encoders import jsonable_encoder +import models + +from core.config import settings +from . import test_department + + +headers = { + "accept": "application/json", + "Content-Type": "application/json", +} + + +def test_post_program(super_user_client, db): + test_department.test_post_department(super_user_client, db) + department_id = ( + db.query(models.Department) + .filter(models.Department.name == "Test Department") + .first() + .id + ) + + data = {"name": "Test Program", "department_id": department_id, "max_sems": 8} + post_req = super_user_client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/", + json=data, + ) + assert post_req.status_code == 200, "Program post request failed" + + +def test_get_program(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/", + ) + assert get_req.status_code == 200 + + programs = get_req.json() + created_program = [ + program for program in programs if (program.get("name") == "Test Program") + ] + assert len(created_program) == 1 + + +def test_get_specific_program(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/", + ) + assert get_req.status_code == 200 + + programs = get_req.json() + created_program = [ + program for program in programs if (program.get("name") == "Test Program") + ] + + specific_get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/{created_program[0]['id']}/", + ) + + assert specific_get_req.status_code == 200 + + created_program_specific = specific_get_req.json() + assert created_program_specific + assert created_program_specific.get("name") == "Test Program" + + +def test_put_specific_program(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/", + ) + assert get_req.status_code == 200 + + programs = get_req.json() + created_program = [ + program for program in programs if (program.get("name") == "Test Program") + ] + + data = { + "name": "Updated Program", + } + + put_req = super_user_client.put( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/{created_program[0]['id']}/", + json=data, + ) + + assert put_req.status_code == 200 + + specific_get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/{created_program[0]['id']}/", + ) + + assert specific_get_req.status_code == 200 + + created_program_specific = specific_get_req.json() + assert created_program_specific + assert created_program_specific.get("name") == "Updated Program" + + +def test_delete_program(super_user_client, program_id=None): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/", + ) + assert get_req.status_code == 200 + + programs = get_req.json() + if not program_id: + created_program = [ + program + for program in programs + if (program.get("name") == "Updated Program") + ] + else: + created_program = [ + program for program in programs if (program.get("id") == program_id) + ] + + delete_req = super_user_client.delete( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/{created_program[0]['id']}/", + ) + + assert delete_req.status_code == 200 + + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/program/", + ) + assert get_req.status_code == 200 + + programs = get_req.json() + filtered_program = [ + program + for program in programs + if ( + program.get("name") == "Updated Program" + or program.get("name") == "Test Program" + ) + ] + print(filtered_program) + assert not filtered_program + test_department.test_delete_department( + super_user_client, department_id=created_program[0]["department_id"] + ) diff --git a/tests/api/api_v1/test_school.py b/tests/api/api_v1/test_school.py new file mode 100644 index 0000000000000000000000000000000000000000..217ccee25b70ba4b8a62321acfd82ad617ec4f90 --- /dev/null +++ b/tests/api/api_v1/test_school.py @@ -0,0 +1,144 @@ +import json +import base64 + +import pytest +import requests +from core.config import settings +import email +from cruds import crud_user +import re + + + +headers = { + "accept": "application/json", + "Content-Type": "application/json", +} + + +def test_post_school(super_user_client): + data = {"name": "Test School", "address": "Testland"} + post_req = super_user_client.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/", + json=data, + ) + assert post_req.status_code == 200, "School post request failed" + + +def test_get_school(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/", + ) + assert get_req.status_code == 200 + + schools = get_req.json() + created_school = [ + school + for school in schools + if (school.get("name") == "Test School") + and (school.get("address") == "Testland") + ] + assert len(created_school) == 1 + + +def test_get_specific_school(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/", + ) + assert get_req.status_code == 200 + + schools = get_req.json() + created_school = [ + school + for school in schools + if (school.get("name") == "Test School") + and (school.get("address") == "Testland") + ] + + specific_get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/{created_school[0]['id']}/", + ) + + assert specific_get_req.status_code == 200 + + created_school_specific = specific_get_req.json() + assert created_school_specific + assert created_school_specific.get("name") == "Test School" + assert created_school_specific.get("address") == "Testland" + + +def test_put_specific_school(super_user_client): + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/", + ) + assert get_req.status_code == 200 + + schools = get_req.json() + created_school = [ + school + for school in schools + if (school.get("name") == "Test School") + and (school.get("address") == "Testland") + ] + + data = { + "name": "Updated School", + "address": "Newland", + } + + put_req = super_user_client.put( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/{created_school[0]['id']}/", + json=data, + ) + + assert put_req.status_code == 200 + + specific_get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/{created_school[0]['id']}/", + ) + + assert specific_get_req.status_code == 200 + + created_school_specific = specific_get_req.json() + assert created_school_specific + assert created_school_specific.get("name") == "Updated School" + assert created_school_specific.get("address") == "Newland" + + +def test_delete_school(super_user_client, school_id=None): + if not school_id: + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/", + ) + assert get_req.status_code == 200 + + schools = get_req.json() + created_school = [ + school + for school in schools + if (school.get("name") == "Updated School") + and (school.get("address") == "Newland") + ] + + else: + created_school = [{"id": school_id}] + + delete_req = super_user_client.delete( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/{created_school[0]['id']}/", + ) + + assert delete_req.status_code == 200 + + get_req = super_user_client.get( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/school/", + ) + assert get_req.status_code == 200 + + schools = get_req.json() + filtered_school = [ + school + for school in schools + if (school.get("name") == "Updated School") + and (school.get("address") == "Newland") + ] + assert not filtered_school diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..dbd51a58bd06962db0329f324f4718309d6234d6 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,71 @@ +from typing import Dict, Generator + +import pytest +from fastapi.testclient import TestClient +from sqlalchemy.orm import Session + +from core.config import settings +from core.db import SessionLocal +from misc.scripts.launch import app +from cruds import crud_user +from schemas import user as user_schemas +import datetime +import requests + + +headers = { + "accept": "application/json", + "Content-Type": "application/json", +} + + +@pytest.fixture(scope="session") +def db() -> Generator: + yield SessionLocal() + + +@pytest.fixture(scope="session") +def super_user_client() -> Generator: + db = SessionLocal() + super_user = user_schemas.UserCreate( + email="test_superadmin@test.local", + user_type=1, + password="test", + full_name="Test Super Admin", + is_active=True, + address="Test Land", + contact_number="9841111111", + dob=datetime.datetime(2021, 1, 1), + join_year=2021, + ) + super_user_obj = crud_user.create(db=db, obj_in=super_user) + crud_user.verify_user(db=db, db_obj=super_user_obj) + + data = { + "username": "test_superadmin@test.local", + "password": "test", + "remember_me": True, + } + + super_user_cookies = None + with TestClient(app) as c: + req = c.post( + f"{settings.BACKEND_URL_BASE}{settings.API_V1_STR}/auth/web/", + json=data, + ) + super_user_cookies = req.cookies + + assert req.status_code == 200 + assert req.cookies.get("session"), "Cookie not returned!" + + with TestClient(app) as c: + c.cookies = super_user_cookies + yield c + + crud_user.remove(db=SessionLocal(), id=super_user_obj.id) + + +@pytest.fixture(scope="session") +def client() -> Generator: + with TestClient(app) as c: + yield c diff --git a/tests/crud/__init__.py b/tests/crud/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2a58da821b7220f54da6ca7b7cc128da177388ca --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +from .utils import get_super_admin diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd02ad5ff0ac34fc8d1d3424b584b935a0b6378c Binary files /dev/null and b/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/utils/__pycache__/deps.cpython-310.pyc b/utils/__pycache__/deps.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55c867bae1f62a50790ac02682aa2742098d48a2 Binary files /dev/null and b/utils/__pycache__/deps.cpython-310.pyc differ diff --git a/utils/__pycache__/utils.cpython-310.pyc b/utils/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..325ebc8b6dcbe5f6cc0d159898623a1b69fdbac3 Binary files /dev/null and b/utils/__pycache__/utils.cpython-310.pyc differ diff --git a/utils/deps.py b/utils/deps.py new file mode 100644 index 0000000000000000000000000000000000000000..ad16159d47c244e9f69f921f99fcbfce0b2eec98 --- /dev/null +++ b/utils/deps.py @@ -0,0 +1,126 @@ +from typing import Generator +from typing import Optional + +from fastapi import Cookie +from fastapi import Depends, HTTPException, status +from sqlalchemy.orm import Session + +import cruds +import models +from core import settings +from core.db import SessionLocal +from core.db import redis_session_client + + +def get_db() -> Generator: + try: + db = SessionLocal() + yield db + finally: + db.close() + + +async def get_current_user( + db: Session = Depends(get_db), + session: str = Cookie(None), +) -> models.User: + if not session: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Error ID: 137" + ) # Invalid Session Token! + + user_id = await redis_session_client.client.get(session, encoding="utf-8") + if not user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Error ID: 138" + ) # Invalid Session Token! + user = cruds.crud_user.get(db, id=user_id) + + if not user: + raise HTTPException(status_code=404, detail="Error ID: 139") # User not found + return user + + +def get_current_active_user( + current_user: models.User = Depends(get_current_user), +) -> models.User: + if not cruds.crud_user.is_active(current_user): + raise HTTPException(status_code=400, detail="Error ID: 140") # Inactive user + return current_user + +def get_current_active_ws_users( + current_user: models.User = Depends(get_current_user), +) -> models.User: + if not cruds.crud_user.is_active(current_user): + raise HTTPException(status_code=400, detail="Error ID: 140") # Inactive user + return current_user + +async def auth_token(token: Optional[str] = None): + if token: + return {"token": token} + else: + return None + +async def get_current_active_ws_user( + db: Session = Depends(get_db), + params: dict = Depends(auth_token), + session: str = Cookie(None), +) -> models.User: + if not (session or params): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Error ID: 137" + ) # Invalid Session Token! + + if session: + session_token = session + else: + session_token = params.get("token") + + user_id = await redis_session_client.client.get(session_token, encoding="utf-8") + if not user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Error ID: 138" + ) # Invalid Session Token! + user = cruds.crud_user.get(db, id=user_id) + + if not user: + raise HTTPException(status_code=404, detail="Error ID: 139") # User not found + return user + + +def get_current_active_teacher( + current_user: models.User = Depends(get_current_active_user), +) -> models.User: + if current_user.user_type == settings.UserType.TEACHER.value: + return current_user + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + + +def get_current_active_teacher_or_above( + current_user: models.User = Depends(get_current_active_user), +) -> models.User: + if current_user.user_type <= settings.UserType.TEACHER.value: + return current_user + else: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + + +def get_current_active_superuser( + current_user: models.User = Depends(get_current_active_user), +) -> models.User: + if not cruds.crud_user.is_superuser(current_user): + raise HTTPException( + status_code=400, detail="Error ID: 141" + ) # The user doesn't have enough privileges + return current_user + + +def get_current_admin_or_above( + current_user: models.User = Depends(get_current_active_user), +) -> models.User: + if not current_user.user_type <= settings.UserType.ADMIN.value: + raise HTTPException( + status_code=400, detail="Error ID: 142" + ) # The user doesn't have enough privileges + return current_user diff --git a/utils/generator.py b/utils/generator.py new file mode 100644 index 0000000000000000000000000000000000000000..c4f953e1206773a1947c1c15db66057db83eec18 --- /dev/null +++ b/utils/generator.py @@ -0,0 +1,86 @@ +import os +import re + +from mako.template import Template + + +def pascal_case_to_snake(name): + name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() + + +def create_endpoint(name: str): + mytemp = Template(filename="templates/endpoint.pyt") + snake_case_name = pascal_case_to_snake(name) + pascal_case_name = name + rendered = mytemp.render(snake_case_name=snake_case_name, PascalCaseName=pascal_case_name) + file = f"api/endpoints/{snake_case_name}.py" + + if os.path.isfile(file): + raise Exception("File already exists") + + with open(file, "w") as f: + f.write(rendered) + + +def create_schema(name: str): + mytemp = Template(filename="templates/schema.pyt") + snake_case_name = pascal_case_to_snake(name) + pascal_case_name = name + rendered = mytemp.render(snake_case_name=snake_case_name, PascalCaseName=pascal_case_name) + file = f"schemas/{snake_case_name}.py" + + if os.path.isfile(file): + raise Exception("File already exists") + + with open(file, "w") as f: + f.write(rendered) + with open("schemas/__init__.py", "a") as f: + f.write("\n") + import_temp = Template( + "from .${snake_case_name} import ${PascalCaseName}, ${PascalCaseName}Create, ${PascalCaseName}Update, ${PascalCaseName}InDB") + f.write(import_temp.render(snake_case_name=snake_case_name, PascalCaseName=pascal_case_name)) + + +def create_model(name: str): + mytemp = Template(filename="templates/model.pyt") + snake_case_name = pascal_case_to_snake(name) + pascal_case_name = name + rendered = mytemp.render(snake_case_name=snake_case_name, PascalCaseName=pascal_case_name) + file = f"models/{snake_case_name}.py" + + if os.path.isfile(file): + raise Exception("File already exists") + + with open(file, "w") as f: + f.write(rendered) + + with open("models/__init__.py", "a") as f: + f.write("\n") + import_temp = Template( + "from .${snake_case_name} import ${PascalCaseName}") + f.write(import_temp.render(snake_case_name=snake_case_name, PascalCaseName=pascal_case_name)) + + +def create_crud(name: str): + mytemp = Template(filename="templates/crud.pyt") + snake_case_name = pascal_case_to_snake(name) + pascal_case_name = name + rendered = mytemp.render(snake_case_name=snake_case_name, PascalCaseName=pascal_case_name) + file = f"cruds/{snake_case_name}.py" + + if os.path.isfile(file): + raise Exception("File already exists") + + with open(file, "w") as f: + f.write(rendered) + + with open("cruds/__init__.py", "a") as f: + f.write("\n") + import_temp = Template( + "from .${snake_case_name} import crud_${snake_case_name}") + f.write(import_temp.render(snake_case_name=snake_case_name)) + + +if __name__ == '__main__': + print(create_endpoint(name="Department")) diff --git a/utils/populate.py b/utils/populate.py new file mode 100644 index 0000000000000000000000000000000000000000..555a29a50dd5bbe290f378811da9ecb2a0328d9e --- /dev/null +++ b/utils/populate.py @@ -0,0 +1,327 @@ +import json +from schemas.quiz_answer import QuizAnswerCreate +from fastapi.encoders import jsonable_encoder + +from pydantic.types import ConstrainedStr + +from utils.populationdata import ( + users, + schools, + departments, + courses, + programs, + groups, + personalNotes, + teacherNotes, + quizzes, + classSessions, + quizQuestions, + quizAnswers, + assignments, +) +from utils.utils import send_verification_email +from core.db import redis_session_client +import asyncio + +from core.db import SessionLocal + +from cruds import ( + crud_personal_note, + crud_user, + crud_course, + crud_school, + crud_department, + crud_teacher_note, + crud_group, + crud_program, + crud_quiz, + crud_class_session, + crud_question, + crud_quiz_answer, + crud_assignment, +) + +from schemas import ( + PersonalNoteCreate, + UserCreate, + CourseCreate, + SchoolCreate, + DepartmentCreate, + TeacherNoteCreate, + GroupCreate, + ProgramCreate, + Program, + ClassSessionCreate, + QuizCreate, + QuizQuestionCreate, + QuizAnswer, + AssignmentCreate, +) + +db = SessionLocal() + + +created_schools = [] + +def eprint(text): + print(f"\033[91m{text}\033[0m") + + +def populate_school(): + for school in schools: + print(f"Populating school: {school}") + created_school = crud_school.create(db, obj_in=school) + created_schools.append(created_school.id) + + +created_departments = [] +def populate_department(): + for department in departments: + try: + print(f"Populating department: {department}") + department.update({"school_id": created_schools[department["school_id"]-1]}) + created_department = crud_department.create(db, obj_in=department) + created_departments.append(created_department.id) + except Exception as e: # noqa + eprint(e) + + +created_courses = [] +def populate_course(): + for course in courses: + try: + print(f"Populating course: {course}") + course.update({"department_id": created_departments[course["department_id"]-1]}) + created_course = crud_course.create(db, obj_in=course) + created_courses.append(created_course.id) + except Exception as e: # noqa + eprint(e) + + +created_programs = [] +created_groups = [] +def populate_program(): + for program in programs: + try: + print(f"Populating program: {program}") + program.update({"department_id": created_departments[program["department_id"]-1]}) + program_xx = crud_program.create(db, obj_in=Program(**program)) + created_programs.append(program_xx.id) + for sem_iter in range(program.get("max_sems")): + group = GroupCreate( + program_id=program_xx.id, + sem=sem_iter + 1, + ) + created_group = crud_group.create(db=db, obj_in=group) + created_groups.append(created_group.id) + except Exception as e: # noqa + eprint(e) + + +def populate_group(): + for group in groups: + try: + print(f"Populating group: {group}") + group = GroupCreate( + program_id=group["program_id"], + sem=group["sem"], + course=[created_courses[i-1] for i in group["course"]], + ) + crud_group.update( + db, + db_obj=crud_group.get_by_program_and_sem( + db, program=group.program_id, sem=group.sem + ), + obj_in=group, + ) + except Exception as e: # noqa + eprint(e) + + +created_users = [] +def populate_user(): + for user in users: + asyncio.run(redis_session_client.initialize()) + + try: + print(f"Populating user: {user}") + group_id = created_groups[user["group_id"]-1] if user["group_id"] else None + teacher_department_id=created_departments[user.get("teacher_department_id")-1] if user.get("teacher_department_id") else None + user = UserCreate( + full_name=user["full_name"], + is_active=user["is_active"], + email=user["email"], + roll=user.get("roll"), + teacher_department_id=teacher_department_id, + group_id=group_id, + teacher_group=[[created_groups[i[0]-1], created_courses[i[1]-1]] for i in user["teacher_group"] ], + dob=user["dob"], + address=user["address"], + contact_number=user["contact_number"], + password=user["password"], + user_type=user["user_type"], + join_year=user["join_year"], + ) + + user_in = crud_user.create(db, obj_in=user) + created_users.append(user_in.id) + asyncio.run(send_verification_email(user=user_in)) + except Exception as e: # noqa + eprint(e) + pass + + +def populate_teacher_note(): + for teacherNote in teacherNotes: + try: + print(f"Populating teacher note: {teacherNotes}") + + user_id=created_users[teacherNote.get("user_id")-1] if teacherNote.get("user_id") else None + student_id=created_users[teacherNote.get("student_id")-1] if teacherNote.get("student_id") else None + + teacherNote = TeacherNoteCreate( + user_id=user_id, + student_id=student_id, + message=teacherNote["message"].strip(), + ) + crud_teacher_note.create(db, obj_in=teacherNote) + except Exception as e: # noqa + eprint(e) + + +def populate_personal_note(): + for personalNote in personalNotes: + try: + print(f"Populating personal note: {personalNote}") + + user_id=created_users[personalNote.get("user_id")-1] if personalNote.get("user_id") else None + + personalNote = PersonalNoteCreate( + user_id=user_id, + tags=personalNote["tags"], + title=personalNote["title"].strip(), + content=personalNote["content"], + ) + crud_personal_note.create(db, obj_in=personalNote) + except Exception as e: # noqa + eprint(e) + + +created_quizzes = [] +def populate_quiz(): + for quiz in quizzes: + try: + print(f"Populating quiz: {quiz}") + quiz = QuizCreate( + end_time=quiz["end_time"], + start_time=quiz["start_time"], + title=quiz["title"], + description=quiz["description"], + is_randomized=quiz["is_randomized"], + display_individual=quiz["display_individual"], + group=[ created_groups[i-1] for i in quiz["group"]], + instructor=[ created_users[i-1] for i in quiz["instructor"] ], + course_id=created_courses[quiz["course_id"]-1], + total_marks=quiz["total_marks"], + ) + + created_quiz = crud_quiz.create(db, obj_in=quiz) + created_quizzes.append(created_quiz.id) + except Exception as e: + eprint(e) + + +def populate_class_session(): + for class_session in classSessions: + try: + print(f"Populating class session: {class_session}") + + instructor=[created_users[i-1] for i in class_session["instructor"]] + course_id=created_courses[class_session["course_id"]-1] + group_id=created_groups[class_session["group_id"]-1] + + class_session = ClassSessionCreate( + start_time=class_session["start_time"], + is_active=class_session["is_active"], + instructor=instructor, + course_id=course_id, + group_id=group_id, + description=class_session["description"], + end_time=class_session["end_time"], + ) + crud_class_session.create(db, obj_in=class_session) + except Exception as e: + eprint(e) + + +def populate_quiz_question(): + for quiz_question in quizQuestions: + try: + print(f"Populating quiz question: {quiz_question}") + quiz_question = QuizQuestionCreate( + question_text=quiz_question["question_text"], + question_image=quiz_question["question_image"], + options=quiz_question["options"], + answer=quiz_question["answer"], + quiz_id=created_quizzes[quiz_question["quiz_id"]-1], + marks=quiz_question["marks"], + ) + crud_question.create(db, obj_in=quiz_question) + except Exception as e: + eprint(e) + + +def populate_quiz_answer(): + for quiz_answer in quizAnswers: + try: + print(f"Populating quiz answer: {quiz_answer}") + quiz_answer = QuizAnswerCreate( + marks_obtained=quiz_answer["marks_obtained"], + options_selected=quiz_answer["options_selected"], + quiz_id=created_quizzes[quiz_answer["quiz_id"]-1], + student_id=created_users[quiz_answer["student_id"]-1], + ) + + crud_quiz_answer.create(db, obj_in=quiz_answer) + except Exception as e: + eprint(e) + + +def populate_assignment(): + for assignment in assignments: + try: + print(f"Populating Assignments: {assignment}") + + instructor=[created_users[i-1] for i in assignment["instructor"]] + course_id=created_courses[assignment["course_id"]-1] + group=[created_groups[i-1] for i in assignment["group"]] + + assignment = AssignmentCreate( + due_date=assignment["due_date"], + marks=assignment["marks"], + instructor=instructor, + group=group, + course_id=course_id, + title=assignment["title"], + contents=assignment["contents"], + ) + crud_assignment.create(db, obj_in=assignment) + except Exception as e: + eprint(e) + + + +def populate_all(): + populate_school() + populate_department() + populate_course() + populate_program() + populate_group() + populate_user() + populate_personal_note() + populate_teacher_note() + populate_quiz() + populate_class_session() + populate_quiz_question() + populate_quiz_answer() + populate_assignment() \ No newline at end of file diff --git a/utils/populationdata/__init__.py b/utils/populationdata/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7b3d53b00a79f23580f567c3898b5cd97bdb8609 --- /dev/null +++ b/utils/populationdata/__init__.py @@ -0,0 +1,12 @@ +from .user import users +from .school import schools +from .program import programs +from .department import departments +from .course import courses +from .group import groups +from .personal_note import personalNotes +from .teacher_note import teacherNotes +from .quiz import quizzes, quizQuestions +from .class_session import classSessions +from .quiz_answer import quizAnswers +from .assignment import assignments \ No newline at end of file diff --git a/utils/populationdata/assignment.py b/utils/populationdata/assignment.py new file mode 100644 index 0000000000000000000000000000000000000000..ba0d85e68c6d6b5c680b434385af2dd1f28c0049 --- /dev/null +++ b/utils/populationdata/assignment.py @@ -0,0 +1,78 @@ +from typing import List +from pydantic import Json + +from datetime import datetime, timedelta + +assignments: List[Json] = [ + { + "due_date":datetime.utcnow() + timedelta(hours=4), + "marks": 10, + "title": "Assignment on Mathematical Structures", + "contents": "Complete the assignment looking at the following questions.", + "instructor": [10, 9], + "group": [3,1], + "course_id": 1, + }, + { + "due_date":datetime(2021, 8, 20, 14, 00, 00), + "marks": 20, + "title": "Assignment on Environmental Sustainability", + "contents": "Complete the assignment looking at the following questions.", + "instructor":[8, 9], + "group": [3,1,2], + "course_id": 3, + }, + { + "due_date":datetime.utcnow() + timedelta(hours=-4), + "marks": 30, + "title": "Assignment on Time Complexity of Algorithms", + "contents": "Descirbe the time complexity of the following algorithms: Merge Sort and Bubble Sort", + "instructor":[8], + "group": [3,1,2], + "course_id": 2, + }, + { + "due_date":datetime(2022, 5, 10, 14, 00, 00), + "marks": 10, + "title": "Assignment on Green Energy", + "contents": "Discuss Green Energy and its Principles indepth.", + "instructor":[8], + "group": [3,1], + "course_id": 4, + },{ + "due_date":datetime(2022, 5, 11, 14, 00, 00), + "marks": 10, + "title": "Assignment on Environmental Sustainability", + "contents": "What do you mean by environmental sustainability.", + "instructor":[9,10], + "group": [3,1,2], + "course_id": 3, + }, + { + "due_date":datetime(2022, 5, 12, 14, 00, 00), + "marks": 100, + "title": "Assignment on Database", + "contents": "Prepare a detailed paper on the Fundamentals of Normalization and the need for it.", + "instructor": [8], + "group": [3,1,2], + "course_id": 2, + }, + { + "due_date":datetime(2022, 5, 12, 14, 00, 00), + "marks": 15, + "title": "Assignment on Business", + "contents": "Discuss the dos and donts of getting started with business.", + "instructor": [9], + "group": [3,1,7], + "course_id": 10, + }, + { + "due_date":datetime(2021, 5, 12, 14, 00, 00), + "marks": 15, + "title": "Criminal Law Assignment", + "contents": "Criminal Law: What is it, and What are the current trails are proceeding under the offence listed in it.", + "instructor": [8,9,10], + "group": [3,1,9,2], + "course_id": 8, + }, +] diff --git a/utils/populationdata/class_session.py b/utils/populationdata/class_session.py new file mode 100644 index 0000000000000000000000000000000000000000..4c4809d33bda5c1e24ea9927edbc219d08da9c7e --- /dev/null +++ b/utils/populationdata/class_session.py @@ -0,0 +1,106 @@ +from typing import List +from pydantic import Json + +from datetime import datetime, timedelta + +classSessions: List[Json] = [ + { + "start_time": datetime.utcnow() + timedelta(hours=-1), + "is_active": True, + "instructor": [10, 9], + "course_id": 1, + "group_id": 3, + "description": "Mathematical Structures, what are they?", + "end_time": datetime.utcnow() + timedelta(hours=2), + }, + { + "start_time": datetime.utcnow() + timedelta(hours=2), + "is_active": True, + "instructor": [10, 9], + "course_id": 1, + "group_id": 3, + "description": "This is another class session", + "end_time": datetime.utcnow() + timedelta(hours=4), + }, + { + "start_time": datetime(2021, 8, 20, 16, 00, 00), + "is_active": True, + "instructor": [8, 9], + "course_id": 3, + "group_id": 3, + "description": "Starting with the Intro to Environmental Sustainability", + "end_time": datetime(2021, 8, 20, 18, 00, 00), + }, + { + "start_time": datetime(2021, 8, 19, 20, 00, 00), + "is_active": True, + "instructor": [8], + "course_id": 2, + "group_id": 3, + "description": "Starting with Algorithms, and Time Complexity of Algorithms", + "end_time": datetime(2021, 8, 19, 22, 00, 00), + }, + { + "start_time": datetime(2021, 3, 4, 13, 00, 00), + "is_active": True, + "instructor": [8], + "course_id": 4, + "group_id": 3, + "description": "Green Energy and its Principles indepth discussion", + "end_time": datetime(2021, 3, 4, 15, 00, 00), + }, + { + "start_time": datetime(2021, 4, 4, 16, 00, 00), + "is_active": True, + "instructor": [9, 10], + "course_id": 3, + "group_id": 1, + "description": "Starting with the Intro to Environmental Sustainability", + "end_time": datetime(2021, 4, 4, 19, 00, 00), + }, + { + "start_time": datetime(2021, 8, 2, 9, 00, 00), + "is_active": True, + "instructor": [10, 8], + "course_id": 7, + "group_id": 9, + "description": "Presentation of the Fundamental definitions stated in law, and Discussion based around it.", + "end_time": datetime(2021, 8, 2, 12, 00, 00), + }, + { + "start_time": datetime(2021, 7, 7, 15, 00, 00), + "is_active": True, + "instructor": [9], + "course_id": 12, + "group_id": 10, + "description": "Radiology.", + "end_time": datetime(2021, 7, 7, 18, 00, 00), + }, + { + "start_time": datetime(2021, 1, 6, 13, 00, 00), + "is_active": True, + "instructor": [9], + "course_id": 10, + "group_id": 7, + "description": "Discussion on the dos and donts of getting started with business.", + "end_time": datetime(2021, 1, 6, 16, 00, 00), + }, + { + "start_time": datetime(2021, 6, 4, 10, 00, 00), + "is_active": True, + "instructor": [8], + "course_id": 10, + "group_id": 8, + "description": "Discussion on the dos and donts of getting started with business.", + "end_time": datetime(2021, 6, 4, 12, 00, 00), + }, + { + "start_time": datetime(2021, 12, 9, 7, 00, 00), + "is_active": True, + "instructor": [8, 9, 10], + "course_id": 8, + "group_id": 9, + "description": "Criminal Law: What is it, and What are the current trails are proceeding under the offence listed in it.", + "end_time": datetime(2021, 12, 9, 12, 00, 00), + }, +] diff --git a/utils/populationdata/course.py b/utils/populationdata/course.py new file mode 100644 index 0000000000000000000000000000000000000000..d464d2328481b166462c96ac0eecdc071cef57e1 --- /dev/null +++ b/utils/populationdata/course.py @@ -0,0 +1,119 @@ +from typing import List +from pydantic import Json + +courses: List[Json] = [ + { + "course_code": "MCSC201", + "course_name": "Discrete Structures / Mathematics", + "course_credit": 3, + "department_id": 10, + }, + { + "course_code": "COMP202", + "course_name": "Data Structures and Algorithm", + "course_credit": 3, + "department_id": 1, + }, + { + "course_code": "MATH208", + "course_name": "Statistics and Probability", + "course_credit": 3, + "department_id": 10, + }, + { + "course_code": "EEEG211", + "course_name": "Electronics Engineering I", + "course_credit": 2, + "department_id": 3, + }, + { + "course_code": "EEEG202", + "course_name": "Digital Logics and Circuits", + "course_credit": 2, + "department_id": 3, + }, + { + "course_code": "ENVE101", + "course_name": "Introduction to Environmental Engineering", + "course_credit": 2, + "department_id": 11, + }, + { + "course_code": "ENVE201", + "course_name": "Environmental Engineering Advanced", + "course_credit": 2, + "department_id": 11, + }, + { + "course_code": "ENGG111", + "course_name": "Elements of Engineering", + "course_credit": 2, + "department_id": 2, + }, + { + "course_code": "ENGG211", + "course_name": "Advanced Engineering", + "course_credit": 2, + "department_id": 2, + }, + { + "course_code": "LLB101", + "course_name": "Basics of Law", + "course_credit": 3, + "department_id": 4, + }, + { + "course_code": "LLB202", + "course_name": "Criminal Law", + "course_credit": 4, + "department_id": 4, + }, + { + "course_code": "IST101", + "course_name": "Basic Information Systems and Tecnology", + "course_credit": 2, + "department_id": 5, + }, + { + "course_code": "BMS222", + "course_name": "Advanced Business and Management Studies", + "course_credit": 3, + "department_id": 5, + }, + { + "course_code": "ORTH321", + "course_name": "Orthology", + "course_credit": 4, + "department_id": 6, + }, + { + "course_code": "RAD298", + "course_name": "Radiology [indepth]", + "course_credit": 5, + "department_id": 6, + }, + { + "course_code": "MUSC101", + "course_name": "Introduction to Music Theory", + "course_credit": 2, + "department_id": 7, + }, + { + "course_code": "MUSC234", + "course_name": "Music Theory II [indepth]", + "course_credit": 4, + "department_id": 7, + }, + { + "course_code": "LIT101", + "course_name": "Eduction in Literature I [basic]", + "course_credit": 2, + "department_id": 8, + }, + { + "course_code": "LIT234", + "course_name": "Literature II [indepth]", + "course_credit": 4, + "department_id": 8, + }, +] diff --git a/utils/populationdata/department.py b/utils/populationdata/department.py new file mode 100644 index 0000000000000000000000000000000000000000..94faeefba7f50de6eb29f851841a165f41f6dc22 --- /dev/null +++ b/utils/populationdata/department.py @@ -0,0 +1,49 @@ +from typing import List +from pydantic import Json + +departments: List[Json] = [ + { + "name": "Department of Computer Science and Engineering", + "school_id": 1, + }, + { + "name": "Department of Mechanical Engineering", + "school_id": 2, + }, + { + "name": "Department of Electrical and Electronics Engineering", + "school_id": 2, + }, + { + "name": "Department of Management", + "school_id": 5, + }, + { + "name": "Department of Law", + "school_id": 4, + }, + { + "name": "Department of Medical Sciences", + "school_id": 3, + }, + { + "name": "Department of Music", + "school_id": 6, + }, + { + "name": "Department of Fine Arts", + "school_id": 6, + }, + { + "name": "Department of Education", + "school_id": 7, + }, + { + "name": "Department of Mathematics", + "school_id": 1, + }, + { + "name": "Department of Environemental Science and Engineering", + "school_id": 1, + }, +] diff --git a/utils/populationdata/group.py b/utils/populationdata/group.py new file mode 100644 index 0000000000000000000000000000000000000000..41521c3f0583863de27fb98bee79711a0c684136 --- /dev/null +++ b/utils/populationdata/group.py @@ -0,0 +1,90 @@ +from typing import List +from pydantic import Json + +groups: List[Json] = [ + { + "program_id": "1", + "sem": 2, + "course": [6, 7, 8, 9, 10], + }, + { + "program_id": "1", + "sem": 3, + "course": [1, 2, 3, 4, 5], + }, + { + "program_id": "2", + "sem": 2, + "course": [6, 7, 8, 9, 10], + }, + { + "program_id": "2", + "sem": 3, + "course": [1, 2, 3, 4, 5], + }, + { + "program_id": 3, + "sem": 1, + "course": [1, 3], + }, + { + "program_id": 4, + "sem": 4, + "course": [1, 3, 4], + }, + { + "program_id": 5, + "sem": 2, + "course": [9, 10, 11], + }, + { + "program_id": 7, + "sem": 3, + "course": [9, 10, 12], + }, + { + "program_id": 8, + "sem": 5, + "course": [7, 8, 13], + }, + { + "program_id": 9, + "sem": 6, + "course": [11, 12, 15], + }, + { + "program_id": 11, + "sem": 8, + "course": [12], + }, + { + "program_id": 12, + "sem": 4, + "course": [13, 14], + }, + { + "program_id": 13, + "sem": 5, + "course": [13, 14], + }, + { + "program_id": 17, + "sem": 4, + "course": [15, 16], + }, + { + "program_id": 18, + "sem": 3, + "course": [15, 16], + }, + { + "program_id": 14, + "sem": 3, + "course": [2], + }, + { + "program_id": 15, + "sem": 3, + "course": [4], + }, +] diff --git a/utils/populationdata/personal_note.py b/utils/populationdata/personal_note.py new file mode 100644 index 0000000000000000000000000000000000000000..288d190aeebfdae4c66855dcd340bfe2be98351f --- /dev/null +++ b/utils/populationdata/personal_note.py @@ -0,0 +1,165 @@ +from typing import List +from pydantic import Json +import json + +from datetime import datetime + + +contentAll = [ + {"attributes": {"font": "Roboto"}, "insert": "This is a pre-written test note."}, + {"insert": "\n"}, + {"attributes": {"font": "Roboto"}, "insert": "but"}, + {"insert": "\n"}, + { + "attributes": {"font": "Roboto"}, + "insert": "this can be modified. [made only for display purpose]", + }, + {"insert": "\n"}, +] + +personalNotes: List[Json] = [ + { + "user_id": 3, + "tags": ["Maths", "Discrete", "Lattice"], + "title": "Lattice Structure", + "content": json.dumps(contentAll), + }, + { + "user_id": 3, + "tags": ["mathematics", "computer", "physics"], + "title": "POSET ? What is it.", + "content": json.dumps(contentAll), + }, + { + "user_id": 3, + "tags": ["chemistry", "science", "mathematics"], + "title": "Boolean Algebra", + "content": json.dumps(contentAll), + }, + { + "user_id": 3, + "tags": ["mathematics", "computer", "physics"], + "title": "Homomorphism vs. Isomorphism", + "content": json.dumps(contentAll), + }, + { + "user_id": 3, + "tags": ["mathematics", "computer", "physics"], + "title": "Diagraph vs Graph Structure", + "content": json.dumps(contentAll), + }, + { + "user_id": 3, + "tags": ["mathematics", "computer", "physics"], + "title": "Binary Operations", + "content": json.dumps(contentAll), + }, + { + "user_id": 3, + "tags": ["mathematics", "computer", "physics"], + "title": "Mathematical Structures", + "content": json.dumps(contentAll), + }, + { + "user_id": 3, + "tags": ["mathematics", "computer", "physics"], + "title": "Verify that a group is Abelian ?", + "content": json.dumps(contentAll), + }, + { + "user_id": 3, + "tags": ["mathematics", "computer", "physics"], + "title": "Properties of Various Mathematical Structures", + "content": json.dumps(contentAll), + }, + { + "user_id": 2, + "tags": ["Maths", "Discrete", "Lattice"], + "title": "Lattice Structure", + "content": json.dumps(contentAll), + }, + { + "user_id": 2, + "tags": ["mathematics", "computer", "physics"], + "title": "POSET ? What is it.", + "content": json.dumps(contentAll), + }, + { + "user_id": 2, + "tags": ["chemistry", "science", "mathematics"], + "title": "Boolean Algebra", + "content": json.dumps(contentAll), + }, + { + "user_id": 2, + "tags": ["mathematics", "computer", "physics"], + "title": "Homomorphism vs. Isomorphism", + "content": json.dumps(contentAll), + }, + { + "user_id": 2, + "tags": ["mathematics", "computer", "physics"], + "title": "Diagraph vs Graph Structure", + "content": json.dumps(contentAll), + }, + { + "user_id": 2, + "tags": ["mathematics", "computer", "physics"], + "title": "Binary Operations", + "content": json.dumps(contentAll), + }, + { + "user_id": 2, + "tags": ["mathematics", "computer", "physics"], + "title": "Mathematical Structures", + "content": json.dumps(contentAll), + }, + { + "user_id": 2, + "tags": ["mathematics", "computer", "physics"], + "title": "Verify that a group is Abelian ?", + "content": json.dumps(contentAll), + }, + { + "user_id": 2, + "tags": ["mathematics", "computer", "physics"], + "title": "Properties of Various Mathematical Structures", + "content": json.dumps(contentAll), + }, + { + "user_id": 4, + "tags": ["mathematics", "computer", "physics"], + "title": "title ten", + "content": json.dumps(contentAll), + }, + { + "user_id": 5, + "tags": ["mathematics", "computer", "physics"], + "title": "title eleven", + "content": json.dumps(contentAll), + }, + { + "user_id": 7, + "tags": ["mathematics", "computer", "physics"], + "title": "title twelve", + "content": json.dumps(contentAll), + }, + { + "user_id": 8, + "tags": ["mathematics", "computer", "physics"], + "title": "title thirteen", + "content": json.dumps(contentAll), + }, + { + "user_id": 3, + "tags": ["mathematics", "computer", "physics"], + "title": "title fourteen", + "content": json.dumps(contentAll), + }, + { + "user_id": 9, + "tags": ["mathematics", "computer", "physics"], + "title": "title Fifteen", + "content": json.dumps(contentAll), + }, +] diff --git a/utils/populationdata/program.py b/utils/populationdata/program.py new file mode 100644 index 0000000000000000000000000000000000000000..78056106fd65aad8d14e8b3ade5c82447a58cd3a --- /dev/null +++ b/utils/populationdata/program.py @@ -0,0 +1,23 @@ +from typing import List +from pydantic import Json + +programs: List[Json] = [ + {"name": "Computer Science", "max_sems": 8, "department_id": 1}, + {"name": "Computer Engineering", "max_sems": 8, "department_id": 1}, + {"name": "Mechanical Engineering", "max_sems": 8, "department_id": 2}, + {"name": "Enivironmental Science", "max_sems": 8, "department_id": 3}, + {"name": "Environmental Engineering", "max_sems": 8, "department_id": 3}, + {"name": "Information System", "max_sems": 8, "department_id": 4}, + {"name": "Business Administration", "max_sems": 8, "department_id": 4}, + {"name": "Legislative Law", "max_sems": 8, "department_id": 5}, + {"name": "Medicine", "max_sems": 8, "department_id": 6}, + {"name": "Dental Surgery", "max_sems": 8, "department_id": 6}, + {"name": "Science in Nursing", "max_sems": 8, "department_id": 6}, + {"name": "Music Education", "max_sems": 8, "department_id": 7}, + {"name": "Arts in Music", "max_sems": 8, "department_id": 7}, + {"name": "Fine Arts", "max_sems": 8, "department_id": 8}, + {"name": "Culture and Arts", "max_sems": 8, "department_id": 8}, + {"name": "Photography", "max_sems": 8, "department_id": 8}, + {"name": "Education Studies", "max_sems": 8, "department_id": 9}, + {"name": "English Literature", "max_sems": 8, "department_id": 9}, +] diff --git a/utils/populationdata/quiz.py b/utils/populationdata/quiz.py new file mode 100644 index 0000000000000000000000000000000000000000..40d9b190b659307006cd018a1b7f0255732860f2 --- /dev/null +++ b/utils/populationdata/quiz.py @@ -0,0 +1,438 @@ +from typing import List +from pydantic import Json +from datetime import datetime, timedelta +import json + +quizQuestionOptionJSON = [ + {"image": "", "text": "this is first text."}, + {"image": "", "text": "this is first text."}, + {"image": "", "text": "this is first text."}, + {"image": "", "text": "this is first text."}, + {"image": "", "text": "this is first text."}, + {"image": "", "text": "this is first text."}, + {"image": "", "text": "this is first text."}, + {"image": "", "text": "this is first text."}, +] + +quizQuestionOptionOnewithImageJSON = [ + {"image": "", "text": "1946"}, + {"image": "", "text": "1945"}, + {"image": "", "text": "1942"}, + {"image": "", "text": "1948"}, +] + +quizQuestionOptionTwowithImageJSON = [ + {"image": "", "text": "Sludge"}, + {"image": "", "text": "Solid Cubes"}, + {"image": "", "text": "Drink"}, + {"image": "", "text": "Slurppy"}, +] + +quizQuestionOptionThreewithImageJSON = [ + { + "image": "quiz\\17ba0791499db908433b80f37c5fbc89b870084b\\bc33ea4e26e5e1af1408321416956113a4658763\\fb7e182fe941107926b16887993a6927619ff40d/c7cdfbe7307f1d894abdc7375556d2fb11e9ad61.jpg", + "text": "", + }, + { + "image": "quiz\\17ba0791499db908433b80f37c5fbc89b870084b\\bc33ea4e26e5e1af1408321416956113a4658763\\fb7e182fe941107926b16887993a6927619ff40d/5d36953ff5df1774e3d904e1f318d4272bbefe15.jpg", + "text": "", + }, + { + "image": "quiz\\17ba0791499db908433b80f37c5fbc89b870084b\\bc33ea4e26e5e1af1408321416956113a4658763\\fb7e182fe941107926b16887993a6927619ff40d/e36699bdcbacf3f4343f92e2b510495721333477.jpg", + "text": "", + }, + { + "image": "quiz\\17ba0791499db908433b80f37c5fbc89b870084b\\bc33ea4e26e5e1af1408321416956113a4658763\\fb7e182fe941107926b16887993a6927619ff40d/67a46f8a9919b5153169d425a8655f9bb34d67f5.jpg", + "text": "", + }, +] + +quizQuestionOptionFourwithImageJSON = [ + { + "image": "quiz\\17ba0791499db908433b80f37c5fbc89b870084b\\0a57cb53ba59c46fc4b692527a38a87c78d84028\\fb7e182fe941107926b16887993a6927619ff40d/a382c0edbe563fa4860840d8e916ca58e4c39b84.jpg", + "text": "", + }, + { + "image": "quiz\\17ba0791499db908433b80f37c5fbc89b870084b\\0a57cb53ba59c46fc4b692527a38a87c78d84028\\fb7e182fe941107926b16887993a6927619ff40d/5d6a61444c616ed86e5d545789eb8ef5a98ea7a5.jpg", + "text": "", + }, + { + "image": "quiz\\17ba0791499db908433b80f37c5fbc89b870084b\\0a57cb53ba59c46fc4b692527a38a87c78d84028\\fb7e182fe941107926b16887993a6927619ff40d/e822336366d583a989742b02719ecaabb2ced2b5.jpg", + "text": "", + }, + { + "image": "quiz\\17ba0791499db908433b80f37c5fbc89b870084b\\0a57cb53ba59c46fc4b692527a38a87c78d84028\\fb7e182fe941107926b16887993a6927619ff40d/ec0188f3a7f911e311e1e2f36602d10ba63cdef1.jpg", + "text": "", + }, +] + +quizQuestionOption = json.dumps(quizQuestionOptionJSON) +quizQuestionOptionOnewithImageJSON = json.dumps(quizQuestionOptionOnewithImageJSON) +quizQuestionOptionTwowithImageJSON = json.dumps(quizQuestionOptionTwowithImageJSON) +quizQuestionOptionThreewithImageJSON = json.dumps(quizQuestionOptionThreewithImageJSON) +quizQuestionOptionFourwithImageJSON = json.dumps(quizQuestionOptionFourwithImageJSON) + +quizzes: List[Json] = [ + { + "end_time": datetime.utcnow() + timedelta(hours=6), + "start_time": datetime.utcnow() - timedelta(hours=1), + "title": "Fundamentals of Computer First Quiz", + "description": "Quiz for Course COMP101, which is to test the understanding of students on the basic fundamentals of Computer Programming.", + "is_randomized": True, + "total_marks": 100, + "display_individual": False, + "group": [1, 3, 5, 6], + "instructor": [9], + "course_id": 1, + }, + { + "end_time": datetime(2021, 6, 28, 10, 00, 00), + "start_time": datetime(2021, 6, 28, 9, 00, 00), + "title": "Introduction to Environemental Engineering quiz", + "description": "Quiz for Course ENVE101, which is to test the understanding of students on the fundamentals of Environmental Enginnering.", + "is_randomized": False, + "total_marks": 200, + "display_individual": True, + "group": [1, 3, 5, 6, 17], + "instructor": [10], + "course_id": 3, + }, + { + "end_time": datetime(2021, 6, 28, 12, 00, 00), + "start_time": datetime(2021, 6, 28, 10, 00, 00), + "title": "Elements of Engineering First Quiz", + "description": "Quiz for Course ENGG101, which is to test the understanding of students on the elementaries of engineering.", + "is_randomized": False, + "total_marks": 50, + "display_individual": False, + "group": [4], + "instructor": [10], + "course_id": 5, + }, + { + "end_time": datetime(2021, 6, 28, 14, 00, 00), + "start_time": datetime(2021, 6, 28, 12, 00, 00), + "title": "Fundamentals of Computer First Quiz", + "description": "Quiz for Course COMP101, which is to test the understanding of students on the basic fundamentals of Computer Programming.", + "is_randomized": True, + "total_marks": 25, + "display_individual": False, + "group": [1, 3, 5, 6], + "instructor": [8], + "course_id": 2, + }, + { + "end_time": datetime(2021, 6, 28, 5, 00, 00), + "start_time": datetime(2021, 6, 28, 17, 00, 00), + "title": "Advanced Environmental Engineering Third Quiz", + "description": "Quiz for Course ENVE201, which is to test the understanding of students on the advanced knowledge of environmental engineering.", + "is_randomized": True, + "total_marks": 100, + "display_individual": False, + "group": [2, 6], + "instructor": [8], + "course_id": 4, + }, + { + "end_time": datetime(2021, 6, 28, 17, 00, 00), + "start_time": datetime(2021, 6, 28, 15, 00, 00), + "title": "Advanced Engineering Third Quiz", + "description": "Quiz for Course ENGG211, which is to test the understanding of students on the advanced knowledge of engineering.", + "is_randomized": True, + "total_marks": 20, + "display_individual": False, + "group": [4], + "instructor": [10], + "course_id": 6, + }, + { + "end_time": datetime(2021, 6, 28, 10, 00, 00), + "start_time": datetime(2021, 6, 28, 8, 00, 00), + "title": "Basics of Law First Quiz", + "description": "Quiz for Course LLB101, which is to test the understanding of students on the basic human rights.", + "is_randomized": True, + "total_marks": 50, + "display_individual": True, + "group": [9], + "instructor": [8, 9], + "course_id": 7, + }, + { + "end_time": datetime(2021, 6, 28, 14, 00, 00), + "start_time": datetime(2021, 6, 28, 13, 00, 00), + "title": "Criminal Law Second Quiz", + "description": "Quiz for Course LLB202, which is to test the understanding of students on criminal laws and rights surrounding them.", + "is_randomized": False, + "total_marks": 40, + "display_individual": False, + "group": [7], + "instructor": [9, 10], + "course_id": 8, + }, + { + "end_time": datetime(2021, 6, 28, 13, 00, 00), + "start_time": datetime(2021, 6, 28, 11, 00, 00), + "title": "Information Systems Basics Quiz", + "description": "Quiz for Course IST101, which is to test the understanding of students on the basic fundamentals of Computer Information Systems.", + "is_randomized": True, + "total_marks": 90, + "display_individual": False, + "group": [7, 8], + "instructor": [10, 8], + "course_id": 9, + }, + { + "end_time": datetime(2021, 6, 28, 12, 00, 00), + "start_time": datetime(2021, 6, 28, 12, 00, 00), + "title": "Business and Management Studies First Quiz", + "description": "Quiz for Course BMS222, which is to test the understanding of students on topics within the depths of Business and Management Studies", + "is_randomized": False, + "total_marks": 10, + "display_individual": True, + "group": [7], + "instructor": [9, 10, 8], + "course_id": 10, + }, + { + "end_time": datetime(2021, 6, 28, 23, 45, 00), + "start_time": datetime(2021, 6, 28, 14, 35, 00), + "title": "History", + "description": "If you are neutral in situations of injustice, you have chosen the side of the oppressor. If an elephant has its foot on the tail of a mouse and you say that you are neutral, the mouse will not appreciate your neutrality.", + "is_randomized": False, + "total_marks": 25, + "display_individual": False, + "group": [2], + "instructor": [9, 10, 8], + "course_id": 1, + }, +] + + +quizQuestions: List[Json] = [ + { + "question_text": "Question with only text?", + "question_image": ["this is questionOne"], + "options": quizQuestionOption, + "answer": [0, 3], + "marks": 6, + "quiz_id": 1, + }, + { + "question_text": "Question with only text?", + "question_image": None, + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 2, + }, + { + "question_text": "Question with only text?", + "question_image": None, + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 3, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 4, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 5, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 1, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 2, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 3, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 4, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 5, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 6, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 7, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 8, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 9, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 1, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 2, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 3, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 1, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 5, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 7, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 8, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 2, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 6, + }, + { + "question_text": "Question with only text?", + "question_image": ["questionImg"], + "options": quizQuestionOption, + "answer": [2, 3, 4], + "marks": 6, + "quiz_id": 3, + }, + { + "question_text": "What year did World War II end?", + "question_image": [ + """quiz/17ba0791499db908433b80f37c5fbc89b870084b/f6e1126cedebf23e1463aee73f9df08783640400/9a3c4946e15e1fecacfc8360f751d985e5b0eb36/d8e56f5a1118b55cdadebfb1e468c2ae142e0750.jpg""" + ], + "options": quizQuestionOptionOnewithImageJSON, + "answer": [1], + "marks": 6, + "quiz_id": 11, + }, + { + "question_text": "In what form was chocolate originally enjoyed?", + "question_image": [], + "options": quizQuestionOptionTwowithImageJSON, + "answer": [2], + "marks": 6, + "quiz_id": 11, + }, + { + "question_text": "Which one of the following is Alan Turing?", + "question_image": [], + "options": quizQuestionOptionThreewithImageJSON, + "answer": [0], + "marks": 6, + "quiz_id": 11, + }, + { + "question_text": "Tick all Female", + "question_image": [], + "options": quizQuestionOptionFourwithImageJSON, + "answer": [0, 2, 3], + "marks": 7, + "quiz_id": 11, + }, +] diff --git a/utils/populationdata/quiz_answer.py b/utils/populationdata/quiz_answer.py new file mode 100644 index 0000000000000000000000000000000000000000..ebe3039083a880ce5bf1d5ae24c731b689c39af1 --- /dev/null +++ b/utils/populationdata/quiz_answer.py @@ -0,0 +1,49 @@ +from typing import List +from pydantic import Json + +quizAnswers: List[Json] = [ + { + "student_id": 3, + "quiz_id": 11, + "marks_obtained": 20, + "options_selected": { + "25": 1, + "26": 2, + "27": 0, + "28": [0, 2, 3], + }, + }, + { + "student_id": 2, + "quiz_id": 11, + "marks_obtained": 20, + "options_selected": { + "25": 1, + "26": 0, + "27": 2, + "28": [0, 2], + }, + }, + { + "student_id": 4, + "quiz_id": 11, + "marks_obtained": 12, + "options_selected": { + "25": 1, + "26": 0, + "27": 2, + "28": [0, 2], + }, + }, + { + "student_id": 3, + "quiz_id": 1, + "marks_obtained": 32, + "options_selected": { + "1": [2, 3], + "6": 1, + "15": [1, 4], + "18": 3, + }, + }, +] diff --git a/utils/populationdata/school.py b/utils/populationdata/school.py new file mode 100644 index 0000000000000000000000000000000000000000..291289aa47c901e24698cadd5885b40cd5a5104e --- /dev/null +++ b/utils/populationdata/school.py @@ -0,0 +1,33 @@ +from typing import List +from pydantic import Json + +schools: List[Json] = [ + { + "name": "School Of Science", + "address": "Dhulikhel, Kavre" + }, + { + "name": "School Of Engineering", + "address": "Dhulikhel, Kavre" + }, + { + "name": "School Of Medical Sciences", + "address": "Dhulikhel, Kavre" + }, + { + "name": "School Of Law", + "address": "Dhulikhel, Kavre" + }, + { + "name": "School Of Management", + "address": "Balkumari, Lalitpur" + }, + { + "name": "School Of Arts", "address": + "Hattiban, Lalitpur" + }, + { + "name": "School Of Education", "address": + "Hattiban, Lalitpur" + }, +] diff --git a/utils/populationdata/teacher_note.py b/utils/populationdata/teacher_note.py new file mode 100644 index 0000000000000000000000000000000000000000..ae1de2a8ca2fc6c39776501a9a41e680d9196117 --- /dev/null +++ b/utils/populationdata/teacher_note.py @@ -0,0 +1,40 @@ +from typing import List +from pydantic import Json + +teacherNotes: List[Json] = [ + { + "user_id": 8, + "student_id": 4, + "message": "Is attentive, but weak." + }, + { + "user_id": 9, + "student_id": 3, + "message": "Negligent of the topic being taught." + }, + { + "user_id": 10, + "student_id": 6, + "message": "Could perform better, has potential. Practice will do the trick." + }, + { + "user_id": 8, + "student_id": 5, + "message": "Absolutely Brilliant." + }, + { + "user_id": 9, + "student_id": 5, + "message": "Is attentive, but weak." + }, + { + "user_id": 10, + "student_id": 3, + "message": "Absolutely Brilliant." + }, + { + "user_id": 9, + "student_id": 7, + "message": "Absolutely Brilliant." + }, +] diff --git a/utils/populationdata/user.py b/utils/populationdata/user.py new file mode 100644 index 0000000000000000000000000000000000000000..2db19a77242bf42f3c720d4c1e678d00b00a5a50 --- /dev/null +++ b/utils/populationdata/user.py @@ -0,0 +1,193 @@ +from typing import List +from pydantic import Json +import datetime + +users: List[Json] = [ + { + "full_name": "Mike Hunt", + "email": "mike.hunt@gmail.com", + "is_active": True, + "user_type": 2, + "address": "Dhulikhel, Kavre", + "group_id": None, + "contact_number": "9849721522", + "teacher_group": [], + "dob": datetime.datetime(1970, 12, 12), + "join_year": 1990, + "password": "test", + }, + { + "full_name": "Yugesh Upadhyaya Luitel", + "email": "yugu.luitel@gmail.com", + "is_active": True, + "user_type": 4, + "roll": 38, + "address": "Bafal, Kathmandu", + "group_id": 3, + "contact_number": "9861589390", + "teacher_group": [], + "dob": datetime.datetime(2001, 6, 13), + "join_year": 2019, + "password": "test", + }, + { + "full_name": "Arpan Koirala", + "email": "arpankoirala12@gmail.com", + "is_active": True, + "user_type": 4, + "roll": 34, + "address": "Rampur, Palpa", + "group_id": 3, + "contact_number": "9821694321", + "teacher_group": [], + "dob": datetime.datetime(2001, 8, 1), + "join_year": 2019, + "password": "test", + }, + { + "full_name": "Rushab Humagain", + "email": "rushab.humagain@gmail.com", + "is_active": True, + "user_type": 4, + "address": "Banepa, Kavre", + "group_id": 3, + "contact_number": "9854632157", + "teacher_group": [], + "dob": datetime.datetime(2002, 11, 30), + "join_year": 2020, + "password": "test", + }, + { + "full_name": "Abhijeet Poudel", + "email": "abhijeet.poudel@gmail.com", + "is_active": True, + "user_type": 4, + "address": "Pokhara Airport Side, Pokhara", + "group_id": 3, + "contact_number": "9852891559", + "teacher_group": [], + "dob": datetime.datetime(2000, 12, 10), + "join_year": 2018, + "password": "test", + }, + { + "full_name": "Aatish Shrestha", + "email": "aatish.shrestha@gmail.com", + "is_active": True, + "user_type": 4, + "address": "Koteshowr, Kathmandu", + "group_id": 3, + "contact_number": "9845427715", + "teacher_group": [], + "dob": datetime.datetime(1998, 4, 12), + "join_year": 2017, + "password": "test", + }, + { + "full_name": "Suraj Chapagain ", + "email": "suraj.chapagain@gmail.com", + "is_active": True, + "user_type": 3, + "teacher_department_id": 3, + "address": "Lake Side, Pokhara", + "group_id": None, + "contact_number": "9825852660", + "teacher_group": [[3, 4], [11, 4]], + "dob": datetime.datetime(1995, 1, 26), + "join_year": 2020, + "password": "test", + }, + { + "full_name": "Manoj Pandey", + "email": "manoj.pandey@gmail.com", + "is_active": True, + "user_type": 3, + "teacher_department_id": 2, + "address": "Jitgadhi, Butwal", + "group_id": None, + "contact_number": "9833584635", + "teacher_group": [[3, 3], [9, 2], [25, 1], [2, 3]], + "dob": datetime.datetime(1885, 9, 14), + "join_year": 2000, + "password": "test", + }, + { + "full_name": "Om Nath Acharya", + "email": "om.acharya@gmail.com", + "is_active": False, + "user_type": 3, + "teacher_department_id": 4, + "address": "Dhulikhel, Kavre", + "group_id": None, + "contact_number": "9875278464", + "teacher_group": [[3, 5], [2, 5]], + "dob": datetime.datetime(1980, 8, 14), + "join_year": 2005, + "password": "test", + }, + { + "full_name": "Sikshyalaya", + "email": "sikshyalaya@gmail.com", + "is_active": True, + "user_type": 1, + "address": "Sikshyalaya", + "group_id": None, + "contact_number": "9858630918", + "teacher_group": [], + "dob": datetime.datetime(2021, 1, 1), + "join_year": 2021, + "password": "test", + }, + { + "full_name": "Ishan Panta", + "email": "ishan.panta@gmail.com", + "is_active": True, + "user_type": 4, + "address": "Dhulikhel, Kathmandu", + "group_id": 3, + "contact_number": "9845234515", + "teacher_group": [], + "dob": datetime.datetime(1999, 2, 22), + "join_year": 2020, + "password": "test", + }, + { + "full_name": "Mullya Kun Thapa", + "email": "mullu.kun@gmail.com", + "is_active": True, + "user_type": 4, + "address": "Boys Hostel, Kathmandu University", + "group_id": 3, + "contact_number": "9846754715", + "teacher_group": [], + "dob": datetime.datetime(1999, 2, 22), + "join_year": 2020, + "password": "test", + }, + { + "full_name": "Mulyankan T. Sharma", + "email": "mulyankun.t@gmail.com", + "is_active": True, + "user_type": 4, + "address": "Kathmandu University, Kavre", + "group_id": 3, + "contact_number": "9842390715", + "teacher_group": [], + "dob": datetime.datetime(1999, 2, 22), + "join_year": 2020, + "password": "test", + }, + { + "full_name": "Sangharsha Paudel", + "email": "sangharsha.paudel@gmail.com", + "is_active": True, + "user_type": 4, + "address": "Boys Hostel, Kathmandu University", + "group_id": 3, + "contact_number": "9841265895", + "teacher_group": [], + "dob": datetime.datetime(1999, 2, 22), + "join_year": 2020, + "password": "test", + }, +] diff --git a/utils/utils.py b/utils/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..9fc7cd31d0b6f68115570ed110e4528fb8988463 --- /dev/null +++ b/utils/utils.py @@ -0,0 +1,173 @@ +import binascii +import logging +import os +from pathlib import Path +from typing import Any, Dict, Optional +import json + +import emails +from emails.template import JinjaTemplate +from fastapi import HTTPException, status + +from core.config import settings +from core.db import redis_session_client +from models import User +from cruds import crud_user +from core.db import SessionLocal + + +def send_email( + email_to: str, + subject_template: str = "", + html_template: str = "", + environment: Dict[str, Any] = {}, +) -> None: + assert settings.EMAILS_ENABLED, "no provided configuration for email variables" + message = emails.Message( + subject=JinjaTemplate(subject_template), + html=JinjaTemplate(html_template), + mail_from=(settings.EMAILS_FROM_NAME, settings.EMAILS_FROM_EMAIL), + ) + smtp_options = {"host": settings.SMTP_HOST, "port": settings.SMTP_PORT} + if settings.SMTP_TLS: + smtp_options["tls"] = True + if settings.SMTP_USER: + smtp_options["user"] = settings.SMTP_USER + if settings.SMTP_PASSWORD: + smtp_options["password"] = settings.SMTP_PASSWORD + + response = message.send(to=email_to, render=environment, smtp=smtp_options) + + print(response) + logging.info(f"send email result: {response}") + + +def send_test_email(email_to: str) -> None: + project_name = settings.PROJECT_NAME + subject = f"{project_name} - Test email" + with open(Path(settings.EMAIL_TEMPLATES_DIR) / "test_email.html") as f: + template_str = f.read() + send_email( + email_to=email_to, + subject_template=subject, + html_template=template_str, + environment={ + "project_name": settings.PROJECT_NAME, + "email": email_to, + "frontbase": settings.FRONTEND_URL_BASE, + }, + ) + + +async def send_reset_password_email(user: User) -> None: + project_name = settings.PROJECT_NAME + subject = f"{project_name} - Password Recovery" + with open(Path(settings.EMAIL_TEMPLATES_DIR) / "reset-password.html") as f: + template_str = f.read() + + server_host = settings.FRONTEND_URL_BASE + + reset_token = await generate_password_reset_token(uid=user.id) + link = f"{server_host}/reset?token={reset_token}" + send_email( + email_to=user.email, + subject_template=subject, + html_template=template_str, + environment={ + "project_name": settings.PROJECT_NAME, + "username": user.email, + "name": user.full_name, + "email": user.email, + "frontbase": settings.FRONTEND_URL_BASE, + "valid_hours": settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS, + "link": link, + }, + ) + + +async def send_verification_email(user: User) -> None: + project_name = settings.PROJECT_NAME + subject = f"{project_name} - Verification Email" + with open(Path(settings.EMAIL_TEMPLATES_DIR) / "verify-account.html") as f: + template_str = f.read() + verification_token = await generate_verify_token(user.id) + + server_host = settings.FRONTEND_URL_BASE + + link = f"{server_host}/verify?token={verification_token}" + send_email( + email_to=user.email, + subject_template=subject, + html_template=template_str, + environment={ + "name": user.full_name, + "link": link, + "frontbase": settings.FRONTEND_URL_BASE, + }, + ) + + +async def generate_password_reset_token(uid: str) -> str: + reset_token = binascii.hexlify(os.urandom(20)).decode() + await redis_session_client.client.set(f"pwr_token_{reset_token}", uid) + await redis_session_client.client.expire( + f"pwr_token_{reset_token}", + settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS * 60 * 60 * 1000, + ) + return reset_token + + +async def generate_verify_token(uid: str) -> str: + verify_token = binascii.hexlify(os.urandom(20)).decode() + await redis_session_client.client.set(f"verify_token_{verify_token}", uid) + await redis_session_client.client.expire( + f"verify_token_{verify_token}", + settings.EMAIL_VERIFY_EXPIRE_HOURS * 60 * 60 * 1000, + ) + return verify_token + + +async def verify_password_reset_token(token: str) -> Optional[int]: + uid = await redis_session_client.client.get(f"pwr_token_{token}", encoding="utf-8") + if not uid: + raise HTTPException( + status_code=401, detail="Token has expired!" + ) # Invalid token + await redis_session_client.client.expire(f"pwr_token_{token}", timeout=0) + return int(uid) + + +async def verify_user_verify_token(token: str) -> Optional[int]: + uid = await redis_session_client.client.get( + f"verify_token_{token}", encoding="utf-8" + ) + if not uid: + raise HTTPException( + status_code=401, detail="Token has expired!" + ) # Invalid token + await redis_session_client.client.expire(f"verify_token_{token}", timeout=0) + return int(uid) + + +async def expire_web_session(token: str) -> Any: + user_id = await redis_session_client.client.get(token, encoding="utf-8") + active_sessions = await redis_session_client.client.get( + f"user_{user_id}_sessions", encoding="utf-8" + ) + + if active_sessions: + active_sessions = json.loads(active_sessions) + new_active_sessions = [ + item for item in active_sessions["sessions"] if item["token"] != token + ] + + active_sessions["sessions"] = new_active_sessions + + data = {f"user_{user_id}_sessions": json.dumps(active_sessions)} + await redis_session_client.client.mset(data) + + return await redis_session_client.client.expire(token, 0) + + +def get_super_admin() -> User: + return crud_user.get_by_id(db=SessionLocal(), id=1) diff --git a/web_service b/web_service new file mode 100644 index 0000000000000000000000000000000000000000..3cd47f5c1a8743d2eb847931ff76a7ca4581022b --- /dev/null +++ b/web_service @@ -0,0 +1,10 @@ +digraph "Web Service" { + graph [fontcolor="#2D3436" fontname="Sans-Serif" fontsize=15 label="Web Service" nodesep=0.60 pad=2.0 rankdir=LR ranksep=0.75 splines=ortho] + node [fixedsize=true fontcolor="#2D3436" fontname="Sans-Serif" fontsize=13 height=1.4 imagescale=true labelloc=b shape=box style=rounded width=1.4] + edge [color="#7B8894"] + "853e7d758eb3495e87e978157e887240" [label=lb height=1.9 image="D:\VT Research\MAIN PROJECT\Gurukul\venv\lib\site-packages\resources/aws/network\elastic-load-balancing.png" shape=none] + acaa2fac07f545eda0d5afbf6edecd53 [label=web height=1.9 image="D:\VT Research\MAIN PROJECT\Gurukul\venv\lib\site-packages\resources/aws/compute\ec2.png" shape=none] + e3273da165854d4ba73e178cd49c6d49 [label=db height=1.9 image="D:\VT Research\MAIN PROJECT\Gurukul\venv\lib\site-packages\resources/aws/database\rds.png" shape=none] + "853e7d758eb3495e87e978157e887240" -> acaa2fac07f545eda0d5afbf6edecd53 [dir=forward fontcolor="#2D3436" fontname="Sans-Serif" fontsize=13] + acaa2fac07f545eda0d5afbf6edecd53 -> e3273da165854d4ba73e178cd49c6d49 [dir=forward fontcolor="#2D3436" fontname="Sans-Serif" fontsize=13] +}