From fd7802e6889dadd0619544a9209c88dac2691153 Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sat, 30 Mar 2024 20:58:36 +0100 Subject: [PATCH 01/13] Add more API routes #115 --- backend/domain/logic/project.py | 21 ++++++++++- .../routes/dependencies/role_dependencies.py | 11 ++++++ backend/routes/group.py | 18 ++++++++- backend/routes/project.py | 18 ++++++++- backend/routes/subject.py | 37 ++++++++++++------- backend/routes/teacher.py | 10 +++++ 6 files changed, 97 insertions(+), 18 deletions(-) diff --git a/backend/domain/logic/project.py b/backend/domain/logic/project.py index 91efcf89..fadb4ad2 100644 --- a/backend/domain/logic/project.py +++ b/backend/domain/logic/project.py @@ -4,7 +4,8 @@ from db.models.models import Project, Student, Subject, Teacher from domain.logic.basic_operations import get, get_all -from domain.models.ProjectDataclass import ProjectDataclass +from domain.models.ProjectDataclass import ProjectDataclass, ProjectInput +from domain.models.TeacherDataclass import TeacherDataclass def create_project( @@ -53,6 +54,12 @@ def get_projects_of_subject(session: Session, subject_id: int) -> list[ProjectDa return [project.to_domain_model() for project in projects] +def get_teachers_of_subject(session: Session, subject_id: int) -> list[TeacherDataclass]: + subject: Subject = get(session, Subject, ident=subject_id) + teachers = subject.teachers + return [teacher.to_domain_model() for teacher in teachers] + + def get_projects_of_student(session: Session, user_id: int) -> list[ProjectDataclass]: student = get(session, Student, ident=user_id) subjects = student.subjects @@ -69,3 +76,15 @@ def get_projects_of_teacher(session: Session, user_id: int) -> list[ProjectDatac for i in subjects: projects += i.projects return [project.to_domain_model() for project in projects] + + +def update_project(session: Session, project_id: int, project: ProjectInput) -> None: + project_db = get(session, Project, project_id) + project_db.archived = project.archived + project_db.deadline = project.deadline + project_db.description = project.description + project_db.max_students = project.max_students + project_db.name = project.name + project_db.requirements = project.requirements + project_db.visible = project.visible + session.commit() diff --git a/backend/routes/dependencies/role_dependencies.py b/backend/routes/dependencies/role_dependencies.py index 559c08ba..7d486dec 100644 --- a/backend/routes/dependencies/role_dependencies.py +++ b/backend/routes/dependencies/role_dependencies.py @@ -120,3 +120,14 @@ def ensure_student_authorized_for_group( if group.project_id not in [project.id for project in projects_of_student]: raise NoAccessToSubjectError return student + + +def ensure_user_authorized_for_group( + group_id: int, + session: Session = Depends(get_session), + uid: int = Depends(get_authenticated_user), +) -> None: + group = get_group(session, group_id) + project = get_project(session, group.project_id) + if not is_user_authorized_for_subject(project.subject_id, session, uid): + raise NoAccessToSubjectError diff --git a/backend/routes/group.py b/backend/routes/group.py index 33f1acf2..feb54beb 100644 --- a/backend/routes/group.py +++ b/backend/routes/group.py @@ -2,13 +2,14 @@ from sqlalchemy.orm import Session from db.sessions import get_session -from domain.logic.group import add_student_to_group, remove_student_from_group +from domain.logic.group import add_student_to_group, get_students_of_group, remove_student_from_group from domain.models.StudentDataclass import StudentDataclass -from routes.dependencies.role_dependencies import ensure_student_authorized_for_group +from routes.dependencies.role_dependencies import ensure_student_authorized_for_group, ensure_user_authorized_for_group from routes.tags.swagger_tags import Tags group_router = APIRouter() + @group_router.post("/groups/{group_id}/join", tags=[Tags.GROUP], summary="Join a certain group.") def group_join( group_id: int, @@ -25,3 +26,16 @@ def group_leave( session: Session = Depends(get_session), ) -> None: remove_student_from_group(session, student.id, group_id) + + +@group_router.get( + "/groups/{group_id}/members", + tags=[Tags.GROUP], + summary="List group members.", + dependencies=[Depends(ensure_user_authorized_for_group)], +) +def list_group_members( + group_id: int, + session: Session = Depends(get_session), +) -> list[StudentDataclass]: + return get_students_of_group(session, group_id) diff --git a/backend/routes/project.py b/backend/routes/project.py index 7387fea0..c2f59693 100644 --- a/backend/routes/project.py +++ b/backend/routes/project.py @@ -3,9 +3,9 @@ from db.sessions import get_session from domain.logic.group import create_group, get_groups_of_project -from domain.logic.project import get_project +from domain.logic.project import get_project, update_project from domain.models.GroupDataclass import GroupDataclass -from domain.models.ProjectDataclass import ProjectDataclass +from domain.models.ProjectDataclass import ProjectDataclass, ProjectInput from routes.dependencies.role_dependencies import ( ensure_teacher_authorized_for_project, ensure_user_authorized_for_project, @@ -53,3 +53,17 @@ def project_create_group( session: Session = Depends(get_session), ) -> GroupDataclass: return create_group(session, project_id) + + +@project_router.patch( + "/projects/{project_id}", + dependencies=[Depends(ensure_teacher_authorized_for_project)], + tags=[Tags.PROJECT], + summary="Update a project.", +) +def patch_update_project( + project_id: int, + project: ProjectInput, + session: Session = Depends(get_session), +) -> None: + update_project(session, project_id, project) diff --git a/backend/routes/subject.py b/backend/routes/subject.py index 8893129e..c1a9ebb0 100644 --- a/backend/routes/subject.py +++ b/backend/routes/subject.py @@ -2,10 +2,11 @@ from sqlalchemy.orm import Session from db.sessions import get_session -from domain.logic.project import create_project, get_projects_of_subject +from domain.logic.project import create_project, get_projects_of_subject, get_teachers_of_subject from domain.logic.subject import get_subject from domain.models.ProjectDataclass import ProjectDataclass, ProjectInput from domain.models.SubjectDataclass import SubjectDataclass +from domain.models.TeacherDataclass import TeacherDataclass from routes.dependencies.role_dependencies import ( ensure_teacher_authorized_for_subject, ensure_user_authorized_for_subject, @@ -17,30 +18,40 @@ @subject_router.get( - "/subjects/{subject_id}", - dependencies=[Depends(get_authenticated_user)], - tags=[Tags.SUBJECT], - summary="Get a certain subject.", + "/subjects/{subject_id}", + dependencies=[Depends(get_authenticated_user)], + tags=[Tags.SUBJECT], + summary="Get a certain subject.", ) def subject_get(subject_id: int, session: Session = Depends(get_session)) -> SubjectDataclass: return get_subject(session, subject_id) @subject_router.get( - "/subjects/{subject_id}/projects", - dependencies=[Depends(ensure_user_authorized_for_subject)], - tags=[Tags.SUBJECT], - summary="Get all projects of a certain subject.", + "/subjects/{subject_id}/projects", + dependencies=[Depends(ensure_user_authorized_for_subject)], + tags=[Tags.SUBJECT], + summary="Get all projects of a certain subject.", ) def get_subject_projects(subject_id: int, session: Session = Depends(get_session)) -> list[ProjectDataclass]: return get_projects_of_subject(session, subject_id) +@subject_router.get( + "/subjects/{subject_id}/teachers", + dependencies=[Depends(ensure_user_authorized_for_subject)], + tags=[Tags.SUBJECT], + summary="Get all teachers of a certain subject.", +) +def get_subject_teachers(subject_id: int, session: Session = Depends(get_session)) -> list[TeacherDataclass]: + return get_teachers_of_subject(session, subject_id) + + @subject_router.post( - "/subjects/{subject_id}/projects", - dependencies=[Depends(ensure_teacher_authorized_for_subject)], - tags=[Tags.SUBJECT], - summary="Create a new project linked to a certain subject.", + "/subjects/{subject_id}/projects", + dependencies=[Depends(ensure_teacher_authorized_for_subject)], + tags=[Tags.SUBJECT], + summary="Create a new project linked to a certain subject.", ) def new_project( subject_id: int, diff --git a/backend/routes/teacher.py b/backend/routes/teacher.py index 67d45b87..e0476527 100644 --- a/backend/routes/teacher.py +++ b/backend/routes/teacher.py @@ -2,7 +2,9 @@ from sqlalchemy.orm import Session from db.sessions import get_session +from domain.logic.project import get_projects_of_teacher from domain.logic.subject import add_teacher_to_subject, create_subject, get_subjects_of_teacher +from domain.models.ProjectDataclass import ProjectDataclass from domain.models.SubjectDataclass import SubjectDataclass, SubjectInput from domain.models.TeacherDataclass import TeacherDataclass from routes.dependencies.role_dependencies import get_authenticated_teacher @@ -19,6 +21,14 @@ def subjects_of_teacher_get( return get_subjects_of_teacher(session, teacher.id) +@teacher_router.get("/teacher/projects", tags=[Tags.TEACHER], summary="Get all projects of the teacher.") +def projects_of_teacher_get( + session: Session = Depends(get_session), + teacher: TeacherDataclass = Depends(get_authenticated_teacher), +) -> list[ProjectDataclass]: + return get_projects_of_teacher(session, teacher.id) + + @teacher_router.post("/teacher/subjects", tags=[Tags.TEACHER], summary="Create a new subject.") def create_subject_post( subject: SubjectInput, From 161c61382200b7d63d17acb7111be036be8a7e34 Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 13:04:09 +0200 Subject: [PATCH 02/13] Add submission API --- backend/app.py | 12 +++++-- backend/requirements.txt | 19 ++++++++--- .../routes/dependencies/role_dependencies.py | 28 ++++++++++----- backend/routes/errors/authentication.py | 4 +-- backend/routes/subject.py | 2 +- backend/routes/submission.py | 34 +++++++++++++++++++ backend/routes/tags/swagger_tags.py | 16 +++++---- backend/routes/teacher.py | 2 +- 8 files changed, 91 insertions(+), 26 deletions(-) create mode 100644 backend/routes/submission.py diff --git a/backend/app.py b/backend/app.py index a9216a67..0fa3d6e0 100644 --- a/backend/app.py +++ b/backend/app.py @@ -6,12 +6,17 @@ from starlette.responses import JSONResponse from db.errors.database_errors import ActionAlreadyPerformedError, ItemNotFoundError, NoSuchRelationError -from routes.errors.authentication import InvalidAuthenticationError, InvalidRoleCredentialsError, NoAccessToSubjectError +from routes.errors.authentication import ( + InvalidAuthenticationError, + InvalidRoleCredentialsError, + NoAccessToDataError, +) from routes.group import group_router from routes.login import login_router from routes.project import project_router from routes.student import student_router from routes.subject import subject_router +from routes.submission import submission_router from routes.tags.swagger_tags import tags_metadata from routes.teacher import teacher_router from routes.user import users_router @@ -26,6 +31,7 @@ app.include_router(project_router, prefix="/api") app.include_router(subject_router, prefix="/api") app.include_router(group_router, prefix="/api") +app.include_router(submission_router, prefix="/api") DEBUG = False # Should always be false in repo @@ -64,8 +70,8 @@ def item_not_found_error_handler(request: Request, exc: ItemNotFoundError) -> JS ) -@app.exception_handler(NoAccessToSubjectError) -def no_access_to_subject_error_handler(request: Request, exc: NoAccessToSubjectError) -> JSONResponse: +@app.exception_handler(NoAccessToDataError) +def no_access_to_data_error_handler(request: Request, exc: NoAccessToDataError) -> JSONResponse: return JSONResponse( status_code=status.HTTP_403_FORBIDDEN, content={"detail": str(exc)}, diff --git a/backend/requirements.txt b/backend/requirements.txt index 7041d175..1dd5cdbc 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,19 +1,33 @@ annotated-types==0.6.0 anyio==4.3.0 +certifi==2024.2.2 +cffi==1.16.0 +cfgv==3.4.0 click==8.1.7 +cryptography==42.0.5 +defusedxml==0.7.1 +distlib==0.3.8 dnspython==2.6.1 email_validator==2.1.1 fastapi==0.110.0 +filelock==3.13.1 greenlet==3.0.3 h11==0.14.0 +httpcore==1.0.4 httpx==0.27.0 +identify==2.5.35 idna==3.6 nodeenv==1.8.0 +platformdirs==4.2.0 pre-commit==3.6.2 psycopg2-binary==2.9.9 +pycparser==2.21 pydantic==2.6.3 pydantic_core==2.16.3 +PyJWT==2.8.0 pyright==1.1.352 +python-multipart==0.0.9 +PyYAML==6.0.1 ruff==0.2.2 setuptools==69.1.1 sniffio==1.3.1 @@ -21,7 +35,4 @@ SQLAlchemy==2.0.27 starlette==0.36.3 typing_extensions==4.10.0 uvicorn==0.27.1 -httpx==0.27.0 -defusedxml~=0.7.1 -cryptography~=42.0.5 -PyJWT~=2.8.0 \ No newline at end of file +virtualenv==20.25.1 diff --git a/backend/routes/dependencies/role_dependencies.py b/backend/routes/dependencies/role_dependencies.py index 7d486dec..d75acd88 100644 --- a/backend/routes/dependencies/role_dependencies.py +++ b/backend/routes/dependencies/role_dependencies.py @@ -5,7 +5,7 @@ from controllers.auth.token_controller import verify_token from db.sessions import get_session from domain.logic.admin import get_admin, is_user_admin -from domain.logic.group import get_group +from domain.logic.group import get_group, get_students_of_group from domain.logic.project import get_project, get_projects_of_student, get_projects_of_teacher from domain.logic.student import get_student, is_user_student from domain.logic.subject import get_subjects_of_student, get_subjects_of_teacher, is_user_authorized_for_subject @@ -18,7 +18,7 @@ InvalidAuthenticationError, InvalidStudentCredentialsError, InvalidTeacherCredentialsError, - NoAccessToSubjectError, + NoAccessToDataError, ) auth_scheme = APIKeyHeader(name="cas") @@ -64,7 +64,7 @@ def ensure_user_authorized_for_subject( uid: int = Depends(get_authenticated_user), ) -> None: if not is_user_authorized_for_subject(subject_id, session, uid): - raise NoAccessToSubjectError + raise NoAccessToDataError def ensure_user_authorized_for_project( @@ -74,7 +74,7 @@ def ensure_user_authorized_for_project( ) -> None: project = get_project(session, project_id) if not is_user_authorized_for_subject(project.subject_id, session, uid): - raise NoAccessToSubjectError + raise NoAccessToDataError def ensure_student_authorized_for_subject( @@ -84,7 +84,7 @@ def ensure_student_authorized_for_subject( ) -> StudentDataclass: subjects_of_student = get_subjects_of_student(session, student.id) if subject_id not in [subject.id for subject in subjects_of_student]: - raise NoAccessToSubjectError + raise NoAccessToDataError return student @@ -95,7 +95,7 @@ def ensure_teacher_authorized_for_subject( ) -> TeacherDataclass: subjects_of_teacher = get_subjects_of_teacher(session, teacher.id) if subject_id not in [subject.id for subject in subjects_of_teacher]: - raise NoAccessToSubjectError + raise NoAccessToDataError return teacher @@ -106,7 +106,7 @@ def ensure_teacher_authorized_for_project( ) -> TeacherDataclass: projects_of_teacher = get_projects_of_teacher(session, teacher.id) if project_id not in [project.id for project in projects_of_teacher]: - raise NoAccessToSubjectError + raise NoAccessToDataError return teacher @@ -118,7 +118,7 @@ def ensure_student_authorized_for_group( group = get_group(session, group_id) projects_of_student = get_projects_of_student(session, student.id) if group.project_id not in [project.id for project in projects_of_student]: - raise NoAccessToSubjectError + raise NoAccessToDataError return student @@ -130,4 +130,14 @@ def ensure_user_authorized_for_group( group = get_group(session, group_id) project = get_project(session, group.project_id) if not is_user_authorized_for_subject(project.subject_id, session, uid): - raise NoAccessToSubjectError + raise NoAccessToDataError + + +def ensure_student_in_group( + group_id: int, + session: Session = Depends(get_session), + student: StudentDataclass = Depends(get_authenticated_student), +) -> StudentDataclass: + if student not in get_students_of_group(session, group_id): + raise NoAccessToDataError + return student diff --git a/backend/routes/errors/authentication.py b/backend/routes/errors/authentication.py index 8c9b507d..8929ce30 100644 --- a/backend/routes/errors/authentication.py +++ b/backend/routes/errors/authentication.py @@ -14,8 +14,8 @@ class InvalidStudentCredentialsError(InvalidRoleCredentialsError): ERROR_MESSAGE = "User does not have the required student role" -class NoAccessToSubjectError(Exception): - ERROR_MESSAGE = "User doesn't have access to subject" +class NoAccessToDataError(Exception): + ERROR_MESSAGE = "User doesn't have access to object" class InvalidAuthenticationError(Exception): diff --git a/backend/routes/subject.py b/backend/routes/subject.py index c1a9ebb0..661c5f2d 100644 --- a/backend/routes/subject.py +++ b/backend/routes/subject.py @@ -50,7 +50,7 @@ def get_subject_teachers(subject_id: int, session: Session = Depends(get_session @subject_router.post( "/subjects/{subject_id}/projects", dependencies=[Depends(ensure_teacher_authorized_for_subject)], - tags=[Tags.SUBJECT], + tags=[Tags.PROJECT], summary="Create a new project linked to a certain subject.", ) def new_project( diff --git a/backend/routes/submission.py b/backend/routes/submission.py new file mode 100644 index 00000000..fa9cea26 --- /dev/null +++ b/backend/routes/submission.py @@ -0,0 +1,34 @@ +import datetime + +from fastapi import APIRouter, Depends, UploadFile +from sqlalchemy.orm import Session + +from db.sessions import get_session +from domain.logic.submission import create_submission +from domain.models.StudentDataclass import StudentDataclass +from domain.models.SubmissionDataclass import SubmissionDataclass, SubmissionState +from routes.dependencies.role_dependencies import ensure_student_in_group +from routes.tags.swagger_tags import Tags + +submission_router = APIRouter() + + +@submission_router.post( + "/groups/{group_id}/submit", + tags=[Tags.SUBMISSION], + summary="Make a submission.", +) +def make_submission( + group_id: int, + file: UploadFile, + session: Session = Depends(get_session), + student: StudentDataclass = Depends(ensure_student_in_group), +) -> SubmissionDataclass: + return create_submission( + session=session, + student_id=student.id, + group_id=group_id, + message="", + state=SubmissionState.Pending, + date_time=datetime.datetime.now(), + ) diff --git a/backend/routes/tags/swagger_tags.py b/backend/routes/tags/swagger_tags.py index 815479ec..e87e362a 100644 --- a/backend/routes/tags/swagger_tags.py +++ b/backend/routes/tags/swagger_tags.py @@ -2,21 +2,25 @@ class Tags(Enum): - GROUP = "Group methods" - LOGIN = "Login methods" + GROUP = "Group methods" + LOGIN = "Login methods" PROJECT = "Project methods" STUDENT = "Student methods" SUBJECT = "Subject methods" TEACHER = "Teacher methods" - USER = "User methods" + USER = "User methods" + SUBMISSION = "Submission methods" + class _TagsDescription(Enum): - GROUP = "All methods related to a group." - LOGIN = "All methods related to logging in." + GROUP = "All methods related to a group." + LOGIN = "All methods related to logging in." PROJECT = "All methods related to a project." STUDENT = "All methods related to a student." SUBJECT = "All methods related to a subject." TEACHER = "All methods related to a teacher." - USER = "All methods related to a user." + USER = "All methods related to a user." + SUBMISSION = "All methods related to a submission." + tags_metadata = [{"name": tag, "description": _TagsDescription[tag.name]} for tag in Tags] diff --git a/backend/routes/teacher.py b/backend/routes/teacher.py index e0476527..2677d000 100644 --- a/backend/routes/teacher.py +++ b/backend/routes/teacher.py @@ -29,7 +29,7 @@ def projects_of_teacher_get( return get_projects_of_teacher(session, teacher.id) -@teacher_router.post("/teacher/subjects", tags=[Tags.TEACHER], summary="Create a new subject.") +@teacher_router.post("/teacher/subjects", tags=[Tags.SUBJECT], summary="Create a new subject.") def create_subject_post( subject: SubjectInput, teacher: TeacherDataclass = Depends(get_authenticated_teacher), From 8846a7683641e804ef165c0bcf472c7865873d7b Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 13:28:14 +0200 Subject: [PATCH 03/13] Save file in submission --- .gitignore | 3 ++- backend/app.py | 3 +++ backend/db/models/models.py | 2 ++ backend/domain/logic/submission.py | 14 ++++++++------ backend/pyproject.toml | 1 + backend/routes/submission.py | 10 ++++++++-- 6 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index d5a428ac..e33c8200 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +submissions # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -160,4 +161,4 @@ cython_debug/ .idea/ # Ruff linter -.ruff_cache \ No newline at end of file +.ruff_cache diff --git a/backend/app.py b/backend/app.py index 0fa3d6e0..afe36e3d 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,3 +1,5 @@ +import pathlib + import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -21,6 +23,7 @@ from routes.teacher import teacher_router from routes.user import users_router +pathlib.Path.mkdir(pathlib.Path("submissions"), exist_ok=True) app = FastAPI(docs_url="/api/docs", openapi_tags=tags_metadata) # Koppel routes uit andere modules. diff --git a/backend/db/models/models.py b/backend/db/models/models.py index 2a255c85..09eb5e97 100644 --- a/backend/db/models/models.py +++ b/backend/db/models/models.py @@ -167,6 +167,7 @@ class Submission(Base, AbstractModel): date_time: Mapped[datetime] state: Mapped[SubmissionState] message: Mapped[str] + filename: Mapped[str] id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) group_id: Mapped[int] = mapped_column(ForeignKey(Group.id)) group: Mapped[Group] = relationship(back_populates="submissions") @@ -181,4 +182,5 @@ def to_domain_model(self) -> SubmissionDataclass: student_id=self.student_id, state=self.state, message=self.message, + filename=self.filename, ) diff --git a/backend/domain/logic/submission.py b/backend/domain/logic/submission.py index 63e02d9f..a4f44129 100644 --- a/backend/domain/logic/submission.py +++ b/backend/domain/logic/submission.py @@ -8,12 +8,13 @@ def create_submission( - session: Session, - student_id: int, - group_id: int, - message: str, - state: SubmissionState, - date_time: datetime, + session: Session, + student_id: int, + group_id: int, + message: str, + state: SubmissionState, + date_time: datetime, + filename: str, ) -> SubmissionDataclass: """ Create a submission for a certain project by a certain group. @@ -27,6 +28,7 @@ def create_submission( message=message, state=state, date_time=date_time, + filename=filename, ) session.add(new_submission) session.commit() diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1debc590..8c81611d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -97,6 +97,7 @@ ignore = [ "FBT003", "DTZ005", "ANN204", # Init function of object should not need return type annotation. + "PTH123" ] # Allow fix for all enabled rules (when `--fix`) is provided. diff --git a/backend/routes/submission.py b/backend/routes/submission.py index fa9cea26..2d18e73e 100644 --- a/backend/routes/submission.py +++ b/backend/routes/submission.py @@ -1,6 +1,8 @@ import datetime +import hashlib +from typing import Annotated -from fastapi import APIRouter, Depends, UploadFile +from fastapi import APIRouter, Depends, File from sqlalchemy.orm import Session from db.sessions import get_session @@ -20,10 +22,13 @@ ) def make_submission( group_id: int, - file: UploadFile, + file: Annotated[bytes, File()], session: Session = Depends(get_session), student: StudentDataclass = Depends(ensure_student_in_group), ) -> SubmissionDataclass: + filename = hashlib.sha256(file).hexdigest() + with open(f"submissions/{filename}", "wb") as f: + f.write(file) return create_submission( session=session, student_id=student.id, @@ -31,4 +36,5 @@ def make_submission( message="", state=SubmissionState.Pending, date_time=datetime.datetime.now(), + filename=filename, ) From 654e75d84dcd313a517e06ecd781706342f67022 Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 13:41:31 +0200 Subject: [PATCH 04/13] Fix pyright --- backend/domain/models/SubmissionDataclass.py | 1 + backend/tests/test_edge_cases.py | 11 +- backend/tests/test_stress.py | 25 +++- backend/tests/test_submission.py | 127 ++++++++++++++++--- 4 files changed, 137 insertions(+), 27 deletions(-) diff --git a/backend/domain/models/SubmissionDataclass.py b/backend/domain/models/SubmissionDataclass.py index 6dcc2854..9b778460 100644 --- a/backend/domain/models/SubmissionDataclass.py +++ b/backend/domain/models/SubmissionDataclass.py @@ -17,3 +17,4 @@ class SubmissionDataclass(BaseModel): student_id: int state: SubmissionState message: str + filename: str diff --git a/backend/tests/test_edge_cases.py b/backend/tests/test_edge_cases.py index 1550e287..16cd8a3f 100644 --- a/backend/tests/test_edge_cases.py +++ b/backend/tests/test_edge_cases.py @@ -28,8 +28,15 @@ def test_add_student_to_non_existent_group(self) -> None: def test_create_submission_for_non_existent_project(self) -> None: stud = student.create_student(self.session, "Test Student", "teststudent@gmail.com") with self.assertRaises(ItemNotFoundError): - submission.create_submission(self.session, stud.id, 999, "Test Message", SubmissionState.Pending, - datetime.now()) + submission.create_submission( + self.session, + stud.id, + 999, + "Test Message", + SubmissionState.Pending, + datetime.now(), + filename="test", + ) if __name__ == "__main__": diff --git a/backend/tests/test_stress.py b/backend/tests/test_stress.py index 167ab0a3..ca988df5 100644 --- a/backend/tests/test_stress.py +++ b/backend/tests/test_stress.py @@ -24,12 +24,27 @@ def test_stress(self) -> None: for i in range(100): stud = student.create_student(self.session, f"Test Student {i}", f"teststudent{i}@gmail.com") subj = subject.create_subject(self.session, f"Test Subject {i}") - proj = project.create_project(self.session, subj.id, f"Test Project {i}", datetime.now(), False, - "Test Description", - "Test Requirements", True, 2) + proj = project.create_project( + self.session, + subj.id, + f"Test Project {i}", + datetime.now(), + False, + "Test Description", + "Test Requirements", + True, + 2, + ) grp = group.create_group(self.session, proj.id) - subm = submission.create_submission(self.session, stud.id, grp.id, "Test Message", SubmissionState.Pending, - datetime.now()) + subm = submission.create_submission( + self.session, + stud.id, + grp.id, + "Test Message", + SubmissionState.Pending, + datetime.now(), + filename="test", + ) teach = teacher.create_teacher(self.session, f"Test Teacher {i}", f"testteacher{i}@gmail.com") adm = admin.create_admin(self.session, f"Test Admin {i}", f"testadmin{i}@gmail.com") diff --git a/backend/tests/test_submission.py b/backend/tests/test_submission.py index 9b32797c..f4dff3b0 100644 --- a/backend/tests/test_submission.py +++ b/backend/tests/test_submission.py @@ -32,11 +32,27 @@ def tearDown(self) -> None: def test_create_and_get_submission(self) -> None: student = create_student(self.session, "Test Student", "teststudent@gmail.com") subject = create_subject(self.session, "Test Subject") - project = create_project(self.session, subject.id, "Test Project", datetime.now(), False, "Test Description", - "Test Requirements", True, 2) + project = create_project( + self.session, + subject.id, + "Test Project", + datetime.now(), + False, + "Test Description", + "Test Requirements", + True, + 2, + ) group = create_group(self.session, project.id) - submission = create_submission(self.session, student.id, group.id, "Test Message", SubmissionState.Pending, - datetime.now()) + submission = create_submission( + self.session, + student.id, + group.id, + "Test Message", + SubmissionState.Pending, + datetime.now(), + filename="test", + ) retrieved_submission = get_submission(self.session, submission.id) self.assertEqual(submission.id, retrieved_submission.id) @@ -44,23 +60,71 @@ def test_get_all_submissions(self) -> None: student1 = create_student(self.session, "Test Student 1", "teststudent1@gmail.com") student2 = create_student(self.session, "Test Student 2", "teststudent2@gmail.com") subject = create_subject(self.session, "Test Subject") - project = create_project(self.session, subject.id, "Test Project", datetime.now(), False, "Test Description", - "Test Requirements", True, 2) + project = create_project( + self.session, + subject.id, + "Test Project", + datetime.now(), + False, + "Test Description", + "Test Requirements", + True, + 2, + ) group = create_group(self.session, project.id) - create_submission(self.session, student1.id, group.id, "Test Message 1", SubmissionState.Pending, - datetime.now()) - create_submission(self.session, student2.id, group.id, "Test Message 2", SubmissionState.Pending, - datetime.now()) + create_submission( + self.session, + student1.id, + group.id, + "Test Message 1", + SubmissionState.Pending, + datetime.now(), + filename="test", + ) + create_submission( + self.session, + student2.id, + group.id, + "Test Message 2", + SubmissionState.Pending, + datetime.now(), + filename="test", + ) self.assertEqual(len(get_all_submissions(self.session)), 2) def test_get_submissions_of_student(self) -> None: student = create_student(self.session, "Test Student", "teststudent@gmail.com") subject = create_subject(self.session, "Test Subject") - project = create_project(self.session, subject.id, "Test Project", datetime.now(), False, "Test Description", - "Test Requirements", True, 2) + project = create_project( + self.session, + subject.id, + "Test Project", + datetime.now(), + False, + "Test Description", + "Test Requirements", + True, + 2, + ) group = create_group(self.session, project.id) - create_submission(self.session, student.id, group.id, "Test Message 1", SubmissionState.Pending, datetime.now()) - create_submission(self.session, student.id, group.id, "Test Message 2", SubmissionState.Pending, datetime.now()) + create_submission( + self.session, + student.id, + group.id, + "Test Message 1", + SubmissionState.Pending, + datetime.now(), + filename="test", + ) + create_submission( + self.session, + student.id, + group.id, + "Test Message 2", + SubmissionState.Pending, + datetime.now(), + filename="test", + ) submissions_of_student = get_submissions_of_student(self.session, student.id) self.assertEqual(len(submissions_of_student), 2) @@ -68,13 +132,36 @@ def test_get_submissions_of_group(self) -> None: student1 = create_student(self.session, "Test Student 1", "teststudent1@gmail.com") student2 = create_student(self.session, "Test Student 2", "teststudent2@gmail.com") subject = create_subject(self.session, "Test Subject") - project = create_project(self.session, subject.id, "Test Project", datetime.now(), False, "Test Description", - "Test Requirements", True, 2) + project = create_project( + self.session, + subject.id, + "Test Project", + datetime.now(), + False, + "Test Description", + "Test Requirements", + True, + 2, + ) group = create_group(self.session, project.id) - create_submission(self.session, student1.id, group.id, "Test Message 1", SubmissionState.Pending, - datetime.now()) - create_submission(self.session, student2.id, group.id, "Test Message 2", SubmissionState.Pending, - datetime.now()) + create_submission( + self.session, + student1.id, + group.id, + "Test Message 1", + SubmissionState.Pending, + datetime.now(), + filename="test", + ) + create_submission( + self.session, + student2.id, + group.id, + "Test Message 2", + SubmissionState.Pending, + datetime.now(), + filename="test", + ) submissions_of_group = get_submissions_of_group(self.session, group.id) self.assertEqual(len(submissions_of_group), 2) From 155ea80d2e77c769ad45d05b355483561db67cd5 Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 14:10:35 +0200 Subject: [PATCH 05/13] Switch to Poetry Closes #126 --- README.md | 24 +- backend/poetry.lock | 881 +++++++++++++++++++++++++++++++++++++++ backend/pyproject.toml | 58 +-- backend/requirements.txt | 38 -- 4 files changed, 920 insertions(+), 81 deletions(-) create mode 100644 backend/poetry.lock delete mode 100644 backend/requirements.txt diff --git a/README.md b/README.md index dd19cbc3..30262800 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # UGent-2 De mockup van ons project kan [hier](https://www.figma.com/file/py6Qk9lgFtzbCy9by2qsYU/SELab2?type=design&node-id=617%3A4348&mode=design&t=N4FQR50wAYEyG8qx-1) -gevonden worden. +gevonden worden. ## Project setup @@ -10,7 +10,7 @@ gevonden worden. ``` ## Backend -De backend gebruikt Python 3.12. +De backend gebruikt Python 3.12 en Poetry. Volg deze stappen om de backend van het project op te zetten: @@ -18,15 +18,12 @@ Volg deze stappen om de backend van het project op te zetten: ```bash cd UGent-2/backend ``` - -2. Start de Python virtual environment: - ```bash - python3 -m venv venv - source venv/bin/activate - ``` -3. Installeer de benodigde Python packages met behulp van het `requirements.txt` bestand: +2. Installeer Poetry + +3. Installeer de benodigde Python packages met behulp van Poetry en voer de rest van de stappen uit in die virtual environment: ```bash - pip install -r requirements.txt + poetry install + poetry shell ``` 4. Installeer PostgreSQL: @@ -108,13 +105,12 @@ Volg deze stappen om de backend van het project op te zetten: npm run build ``` De gecompileerde html/css/js bevindt zich nu in de `dist` folder - + 5. Deploy: - + Zet de inhoud van de `dist` folder op de juiste plaats, zodat het geserveerd kan worden. - + 6. De testen kunnen uitgevoerd worden met: (nog niet geïmplementeerd) ```bash npm run tests ``` - diff --git a/backend/poetry.lock b/backend/poetry.lock new file mode 100644 index 00000000..672b95b4 --- /dev/null +++ b/backend/poetry.lock @@ -0,0 +1,881 @@ +# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "email-validator" +version = "2.1.1" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, + {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "fastapi" +version = "0.110.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"}, + {file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.36.3,<0.37.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "filelock" +version = "3.13.3" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"}, + {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "identify" +version = "2.5.35" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pre-commit" +version = "3.7.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.0-py2.py3-none-any.whl", hash = "sha256:5eae9e10c2b5ac51577c3452ec0a490455c45a0533f7960f993a0d01e59decab"}, + {file = "pre_commit-3.7.0.tar.gz", hash = "sha256:e209d61b8acdcf742404408531f0c37d49d2c734fd7cff2d6076083d191cb060"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "psycopg2-binary" +version = "2.9.9" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, +] + +[[package]] +name = "pydantic" +version = "2.6.4" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, + {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.16.3" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pyright" +version = "1.1.356" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.356-py3-none-any.whl", hash = "sha256:a101b0f375f93d7082f9046cfaa7ba15b7cf8e1939ace45e984c351f6e8feb99"}, + {file = "pyright-1.1.356.tar.gz", hash = "sha256:f05b8b29d06b96ed4a0885dad5a31d9dff691ca12b2f658249f583d5f2754021"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "python-multipart" +version = "0.0.9" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, +] + +[package.extras] +dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "ruff" +version = "0.3.4" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"}, + {file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"}, + {file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"}, + {file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"}, + {file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"}, + {file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"}, + {file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"}, + {file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"}, +] + +[[package]] +name = "setuptools" +version = "69.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.29" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c142852ae192e9fe5aad5c350ea6befe9db14370b34047e1f0f7cf99e63c63b"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99a1e69d4e26f71e750e9ad6fdc8614fbddb67cfe2173a3628a2566034e223c7"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef3fbccb4058355053c51b82fd3501a6e13dd808c8d8cd2561e610c5456013c"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d6753305936eddc8ed190e006b7bb33a8f50b9854823485eed3a886857ab8d1"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0f3ca96af060a5250a8ad5a63699180bc780c2edf8abf96c58af175921df847a"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c4520047006b1d3f0d89e0532978c0688219857eb2fee7c48052560ae76aca1e"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win32.whl", hash = "sha256:b2a0e3cf0caac2085ff172c3faacd1e00c376e6884b5bc4dd5b6b84623e29e4f"}, + {file = "SQLAlchemy-2.0.29-cp310-cp310-win_amd64.whl", hash = "sha256:01d10638a37460616708062a40c7b55f73e4d35eaa146781c683e0fa7f6c43fb"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:308ef9cb41d099099fffc9d35781638986870b29f744382904bf9c7dadd08513"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:296195df68326a48385e7a96e877bc19aa210e485fa381c5246bc0234c36c78e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a13b917b4ffe5a0a31b83d051d60477819ddf18276852ea68037a144a506efb9"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f6d971255d9ddbd3189e2e79d743ff4845c07f0633adfd1de3f63d930dbe673"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:61405ea2d563407d316c63a7b5271ae5d274a2a9fbcd01b0aa5503635699fa1e"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de7202ffe4d4a8c1e3cde1c03e01c1a3772c92858837e8f3879b497158e4cb44"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win32.whl", hash = "sha256:b5d7ed79df55a731749ce65ec20d666d82b185fa4898430b17cb90c892741520"}, + {file = "SQLAlchemy-2.0.29-cp311-cp311-win_amd64.whl", hash = "sha256:205f5a2b39d7c380cbc3b5dcc8f2762fb5bcb716838e2d26ccbc54330775b003"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d96710d834a6fb31e21381c6d7b76ec729bd08c75a25a5184b1089141356171f"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:52de4736404e53c5c6a91ef2698c01e52333988ebdc218f14c833237a0804f1b"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c7b02525ede2a164c5fa5014915ba3591730f2cc831f5be9ff3b7fd3e30958e"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dfefdb3e54cd15f5d56fd5ae32f1da2d95d78319c1f6dfb9bcd0eb15d603d5d"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a88913000da9205b13f6f195f0813b6ffd8a0c0c2bd58d499e00a30eb508870c"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fecd5089c4be1bcc37c35e9aa678938d2888845a134dd016de457b942cf5a758"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win32.whl", hash = "sha256:8197d6f7a3d2b468861ebb4c9f998b9df9e358d6e1cf9c2a01061cb9b6cf4e41"}, + {file = "SQLAlchemy-2.0.29-cp312-cp312-win_amd64.whl", hash = "sha256:9b19836ccca0d321e237560e475fd99c3d8655d03da80c845c4da20dda31b6e1"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:87a1d53a5382cdbbf4b7619f107cc862c1b0a4feb29000922db72e5a66a5ffc0"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0732dffe32333211801b28339d2a0babc1971bc90a983e3035e7b0d6f06b93"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90453597a753322d6aa770c5935887ab1fc49cc4c4fdd436901308383d698b4b"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ea311d4ee9a8fa67f139c088ae9f905fcf0277d6cd75c310a21a88bf85e130f5"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5f20cb0a63a3e0ec4e169aa8890e32b949c8145983afa13a708bc4b0a1f30e03"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win32.whl", hash = "sha256:e5bbe55e8552019c6463709b39634a5fc55e080d0827e2a3a11e18eb73f5cdbd"}, + {file = "SQLAlchemy-2.0.29-cp37-cp37m-win_amd64.whl", hash = "sha256:c2f9c762a2735600654c654bf48dad388b888f8ce387b095806480e6e4ff6907"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e614d7a25a43a9f54fcce4675c12761b248547f3d41b195e8010ca7297c369c"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:471fcb39c6adf37f820350c28aac4a7df9d3940c6548b624a642852e727ea586"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:988569c8732f54ad3234cf9c561364221a9e943b78dc7a4aaf35ccc2265f1930"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dddaae9b81c88083e6437de95c41e86823d150f4ee94bf24e158a4526cbead01"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:334184d1ab8f4c87f9652b048af3f7abea1c809dfe526fb0435348a6fef3d380"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:38b624e5cf02a69b113c8047cf7f66b5dfe4a2ca07ff8b8716da4f1b3ae81567"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win32.whl", hash = "sha256:bab41acf151cd68bc2b466deae5deeb9e8ae9c50ad113444151ad965d5bf685b"}, + {file = "SQLAlchemy-2.0.29-cp38-cp38-win_amd64.whl", hash = "sha256:52c8011088305476691b8750c60e03b87910a123cfd9ad48576d6414b6ec2a1d"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3071ad498896907a5ef756206b9dc750f8e57352113c19272bdfdc429c7bd7de"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dba622396a3170974f81bad49aacebd243455ec3cc70615aeaef9e9613b5bca5"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b184e3de58009cc0bf32e20f137f1ec75a32470f5fede06c58f6c355ed42a72"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c37f1050feb91f3d6c32f864d8e114ff5545a4a7afe56778d76a9aec62638ba"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bda7ce59b06d0f09afe22c56714c65c957b1068dee3d5e74d743edec7daba552"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25664e18bef6dc45015b08f99c63952a53a0a61f61f2e48a9e70cec27e55f699"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win32.whl", hash = "sha256:77d29cb6c34b14af8a484e831ab530c0f7188f8efed1c6a833a2c674bf3c26ec"}, + {file = "SQLAlchemy-2.0.29-cp39-cp39-win_amd64.whl", hash = "sha256:04c487305ab035a9548f573763915189fc0fe0824d9ba28433196f8436f1449c"}, + {file = "SQLAlchemy-2.0.29-py3-none-any.whl", hash = "sha256:dc4ee2d4ee43251905f88637d5281a8d52e916a021384ec10758826f5cbae305"}, + {file = "SQLAlchemy-2.0.29.tar.gz", hash = "sha256:bd9566b8e58cabd700bc367b60e90d9349cd16f0984973f98a9a09f9c64e86f0"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""} +typing-extensions = ">=4.6.0" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[[package]] +name = "starlette" +version = "0.36.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, + {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.10.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, +] + +[[package]] +name = "uvicorn" +version = "0.29.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, + {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "virtualenv" +version = "20.25.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "bc2c7b6e8ee30e19073cb071f804665e4b66c90fa6a85fc882268eafb27b0775" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8c81611d..027c6e60 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,18 +1,36 @@ -[project] +[tool.poetry] name = "Delphi" version = "0.1.0" description = "A Dodona clone" authors = [ - {name = "Mathieu Strypsteen", email = "mathieu.strypsteen@ugent.be"}, - {name = "Albéric Loos", email = "alberic.loos@ugent.be"}, - {name = "Emma Vandewalle", email = "emma.vandewalle@ugent.be"}, - {name = "Lukas Barragan Torres", email = "lukas.barragantorres@ugent.be"}, - {name = "Matthias Seghers", email = "matthias.seghers@ugent.be"}, - {name = "Robbe Van de Keere", email = "robbe.vandekeere@ugent.be"}, - {name = "Ruben Vandamme", email = "ruben.vandamme@ugent.be"}, - {name = "Stef Ossé", email = "stef.osse@ugent.be"} + "Mathieu Strypsteen ", + "Albéric Loos ", + "Emma Vandewalle ", + "Lukas Barragan Torres ", + "Matthias Seghers< matthias.seghers@ugent.be>", + "Robbe Van de Keere ", + "Ruben Vandamme ", + "Stef Ossé " ] -python = ">=3.12" + +[tool.poetry.dependencies] +python = "^3.12" +uvicorn = "^0.29.0" +sqlalchemy = "^2.0.29" +ruff = "^0.3.4" +python-multipart = "^0.0.9" +pyright = "^1.1.356" +pyjwt = "^2.8.0" +psycopg2-binary = "^2.9.9" +pre-commit = "^3.7.0" +fastapi = "^0.110.0" +email-validator = "^2.1.1" +httpx = "^0.27.0" +defusedxml = "^0.7.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" [tool.pyright] exclude = [ @@ -59,26 +77,8 @@ indent-width = 4 target-version = "py312" - - [tool.ruff.lint] -select = [ - "ALL", - # "F", # Pyflakes - # "E", # Pycodestyle - # "W", # Warnings - # "I", # Isort (sorted imports) - # "N", # PEP8 - # "A", # Flake8-builtins - # "C4", # Comprehensions - # "PIE", # flake8 pie - # "Q", # Quotes - # "RET", # returns - # "SIM", # simplify - # "ARG", # unused arguments - # "ERA", # no commented out code - # "PL" # all pylint -] +select = ["ALL"] ignore = [ "D", # Docstrings "ANN101", # type annotation for self diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index 1dd5cdbc..00000000 --- a/backend/requirements.txt +++ /dev/null @@ -1,38 +0,0 @@ -annotated-types==0.6.0 -anyio==4.3.0 -certifi==2024.2.2 -cffi==1.16.0 -cfgv==3.4.0 -click==8.1.7 -cryptography==42.0.5 -defusedxml==0.7.1 -distlib==0.3.8 -dnspython==2.6.1 -email_validator==2.1.1 -fastapi==0.110.0 -filelock==3.13.1 -greenlet==3.0.3 -h11==0.14.0 -httpcore==1.0.4 -httpx==0.27.0 -identify==2.5.35 -idna==3.6 -nodeenv==1.8.0 -platformdirs==4.2.0 -pre-commit==3.6.2 -psycopg2-binary==2.9.9 -pycparser==2.21 -pydantic==2.6.3 -pydantic_core==2.16.3 -PyJWT==2.8.0 -pyright==1.1.352 -python-multipart==0.0.9 -PyYAML==6.0.1 -ruff==0.2.2 -setuptools==69.1.1 -sniffio==1.3.1 -SQLAlchemy==2.0.27 -starlette==0.36.3 -typing_extensions==4.10.0 -uvicorn==0.27.1 -virtualenv==20.25.1 From dd9306a4f32dbf934cca6919d747bccdc1e3c11c Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 14:12:18 +0200 Subject: [PATCH 06/13] Fix Github actions with Poetry --- .github/workflows/backend.yml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index c8878fa4..2c2a7e95 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -9,12 +9,14 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.12' + - name: Install Poetry + run: pip install poetry - name: Install dependencies - run: pip install -r backend/requirements.txt + run: cd backend && poetry install - name: Lint code - run: ruff check backend + run: cd backend && poetry run ruff check - name: Run Pyright - run: pyright + run: cd backend && poetry run pyright test: runs-on: self-hosted steps: @@ -23,9 +25,11 @@ jobs: uses: actions/setup-python@v5 with: python-version: '3.12' + - name: Install Poetry + run: pip install poetry - name: Install dependencies - run: pip install -r backend/requirements.txt + run: cd backend && poetry install - name: Run tests - run: cd backend && python -m unittest discover tests + run: cd backend && poetry run python -m unittest discover tests env: TEST_DB_USER: githubactions From a8355491d72e726f1e037e60dacd471c35f3b8e7 Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 14:26:18 +0200 Subject: [PATCH 07/13] Update Dockerfile for Poetry --- backend/Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 70d9c316..e8791d44 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,6 +1,7 @@ FROM python:3.12-slim EXPOSE 8000 +RUN pip install poetry COPY . /backend WORKDIR /backend -RUN pip install -r requirements.txt -CMD uvicorn --host 0.0.0.0 app:app +RUN poetry install +CMD poetry run uvicorn --host 0.0.0.0 app:app From ce7b9274e139de9789247317423711976ad12d1f Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 17:16:25 +0200 Subject: [PATCH 08/13] Add API route to retrieve last submission --- backend/domain/logic/submission.py | 5 +++ .../routes/dependencies/role_dependencies.py | 18 +++++++++ backend/routes/submission.py | 37 +++++++++++++++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/backend/domain/logic/submission.py b/backend/domain/logic/submission.py index a4f44129..5e4c13fb 100644 --- a/backend/domain/logic/submission.py +++ b/backend/domain/logic/submission.py @@ -53,3 +53,8 @@ def get_submissions_of_group(session: Session, group_id: int) -> list[Submission group: Group = get(session, Group, ident=group_id) submissions: list[Submission] = group.submissions return [submission.to_domain_model() for submission in submissions] + + +def get_last_submission(session: Session, group_id: int) -> SubmissionDataclass: + submissions = get_submissions_of_group(session, group_id) + return max(submissions, key=lambda submission: submission.date_time) diff --git a/backend/routes/dependencies/role_dependencies.py b/backend/routes/dependencies/role_dependencies.py index d75acd88..71e50efb 100644 --- a/backend/routes/dependencies/role_dependencies.py +++ b/backend/routes/dependencies/role_dependencies.py @@ -141,3 +141,21 @@ def ensure_student_in_group( if student not in get_students_of_group(session, group_id): raise NoAccessToDataError return student + + +def ensure_user_authorized_for_submission( + group_id: int, + session: Session = Depends(get_session), + uid: int = Depends(get_authenticated_user), +) -> None: + group = get_group(session, group_id) + if is_user_student(session, uid): + student = get_student(session, uid) + if student in get_students_of_group(session, group_id): + return + if is_user_teacher(session, uid) and get_project(session, group.project_id) in get_projects_of_teacher( + session, + uid, + ): + return + raise NoAccessToDataError diff --git a/backend/routes/submission.py b/backend/routes/submission.py index 2d18e73e..567186d2 100644 --- a/backend/routes/submission.py +++ b/backend/routes/submission.py @@ -2,21 +2,21 @@ import hashlib from typing import Annotated -from fastapi import APIRouter, Depends, File +from fastapi import APIRouter, Depends, File, Response from sqlalchemy.orm import Session from db.sessions import get_session -from domain.logic.submission import create_submission +from domain.logic.submission import create_submission, get_last_submission from domain.models.StudentDataclass import StudentDataclass from domain.models.SubmissionDataclass import SubmissionDataclass, SubmissionState -from routes.dependencies.role_dependencies import ensure_student_in_group +from routes.dependencies.role_dependencies import ensure_student_in_group, ensure_user_authorized_for_submission from routes.tags.swagger_tags import Tags submission_router = APIRouter() @submission_router.post( - "/groups/{group_id}/submit", + "/groups/{group_id}/submission", tags=[Tags.SUBMISSION], summary="Make a submission.", ) @@ -38,3 +38,32 @@ def make_submission( date_time=datetime.datetime.now(), filename=filename, ) + + +@submission_router.get( + "/groups/{group_id}/submission", + tags=[Tags.SUBMISSION], + summary="Show the latest submission.", + dependencies=[Depends(ensure_user_authorized_for_submission)], +) +def retrieve_submission( + group_id: int, + session: Session = Depends(get_session), +) -> SubmissionDataclass: + return get_last_submission(session, group_id) + + +@submission_router.get( + "/groups/{group_id}/submission/file", + tags=[Tags.SUBMISSION], + summary="Retrieve the latest submission file.", + dependencies=[Depends(ensure_user_authorized_for_submission)], +) +def retrieve_submission_file( + group_id: int, + session: Session = Depends(get_session), +) -> Response: + submission = get_last_submission(session, group_id) + with open(f"submissions/{submission.filename}", "rb") as file: + content = file.read() + return Response(content, media_type="application/octet-stream") From a549d0d3c75d78835dee1baa0c83e212e1db1d4a Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 17:21:19 +0200 Subject: [PATCH 09/13] Add token validation API endpoint --- backend/domain/models/APIUser.py | 4 ++++ backend/routes/login.py | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/backend/domain/models/APIUser.py b/backend/domain/models/APIUser.py index fd9b15a4..8f7617a8 100644 --- a/backend/domain/models/APIUser.py +++ b/backend/domain/models/APIUser.py @@ -17,3 +17,7 @@ class APIUser(BaseModel): class LoginResponse(BaseModel): token: str + + +class ValidateResponse(BaseModel): + valid: bool diff --git a/backend/routes/login.py b/backend/routes/login.py index 98f88199..f9f658f3 100644 --- a/backend/routes/login.py +++ b/backend/routes/login.py @@ -2,9 +2,9 @@ from sqlalchemy.orm import Session from controllers.auth.authentication_controller import authenticate_user -from controllers.auth.token_controller import create_token +from controllers.auth.token_controller import create_token, verify_token from db.sessions import get_session -from domain.models.APIUser import LoginResponse +from domain.models.APIUser import LoginResponse, ValidateResponse from domain.models.UserDataclass import UserDataclass from routes.errors.authentication import InvalidAuthenticationError from routes.tags.swagger_tags import Tags @@ -13,6 +13,16 @@ login_router = APIRouter() +@login_router.post("/validate", tags=[Tags.LOGIN], summary="Validate a session token.") +def validate_token( + token: str, +) -> ValidateResponse: + uid = verify_token(token) + if uid: + return ValidateResponse(valid=True) + return ValidateResponse(valid=False) + + @login_router.post("/login", tags=[Tags.LOGIN], summary="Starts a session for the user.") def login( ticket: str, From 931c728b5330ea6d0be4c248f48e5ed50c30d22a Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 17:51:10 +0200 Subject: [PATCH 10/13] Add route for retrieving a student's group and improve group validation --- backend/app.py | 15 ++++++++++- backend/db/errors/database_errors.py | 12 +++++++++ backend/domain/logic/group.py | 14 +++++++++- .../routes/dependencies/role_dependencies.py | 11 ++++++++ backend/routes/group.py | 27 +++++++++++++++++-- 5 files changed, 75 insertions(+), 4 deletions(-) diff --git a/backend/app.py b/backend/app.py index afe36e3d..4575c0eb 100644 --- a/backend/app.py +++ b/backend/app.py @@ -7,7 +7,12 @@ from starlette.requests import Request from starlette.responses import JSONResponse -from db.errors.database_errors import ActionAlreadyPerformedError, ItemNotFoundError, NoSuchRelationError +from db.errors.database_errors import ( + ActionAlreadyPerformedError, + ConflictingRelationError, + ItemNotFoundError, + NoSuchRelationError, +) from routes.errors.authentication import ( InvalidAuthenticationError, InvalidRoleCredentialsError, @@ -105,5 +110,13 @@ def invalid_authentication_error_handler(request: Request, exc: NoSuchRelationEr ) +@app.exception_handler(ConflictingRelationError) +def conflicting_relation_error_handler(request: Request, exc: ConflictingRelationError) -> JSONResponse: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(exc)}, + ) + + if __name__ == "__main__": uvicorn.run("app:app") diff --git a/backend/db/errors/database_errors.py b/backend/db/errors/database_errors.py index 4d0ced8e..28068b88 100644 --- a/backend/db/errors/database_errors.py +++ b/backend/db/errors/database_errors.py @@ -2,6 +2,7 @@ class ItemNotFoundError(Exception): """ The specified item was not found in the database. """ + def __init__(self, message: str) -> None: super().__init__(message) @@ -11,6 +12,7 @@ class ActionAlreadyPerformedError(Exception): The specified action was already performed on the database once before and may not be performed again as to keep consistency. """ + def __init__(self, message: str) -> None: super().__init__(message) @@ -19,5 +21,15 @@ class NoSuchRelationError(Exception): """ There is no relation between the two specified elements in the database. """ + + def __init__(self, message: str) -> None: + super().__init__(message) + + +class ConflictingRelationError(Exception): + """ + There is a conflicting relation + """ + def __init__(self, message: str) -> None: super().__init__(message) diff --git a/backend/domain/logic/group.py b/backend/domain/logic/group.py index fff26c2c..4c7d5a5c 100644 --- a/backend/domain/logic/group.py +++ b/backend/domain/logic/group.py @@ -48,7 +48,10 @@ def add_student_to_group(session: Session, student_id: int, group_id: int) -> No if student in group.students: msg = f"Student with id {student_id} already in group with id {group_id}" raise ActionAlreadyPerformedError(msg) - + for i in group.project.groups: + if student in i.students: + msg = "Student is already in a group for this project" + raise ActionAlreadyPerformedError(msg) group.students.append(student) session.commit() @@ -69,3 +72,12 @@ def get_students_of_group(session: Session, group_id: int) -> list[StudentDatacl group: Group = get(session, Group, ident=group_id) students: list[Student] = group.students return [student.to_domain_model() for student in students] + + +def get_group_for_student_and_project(session: Session, student_id: int, project_id: int) -> GroupDataclass | None: + student: Student = get(session, Student, ident=student_id) + project: Project = get(session, Project, ident=project_id) + for group in project.groups: + if student in group.students: + return group.to_domain_model() + return None diff --git a/backend/routes/dependencies/role_dependencies.py b/backend/routes/dependencies/role_dependencies.py index 71e50efb..358ccdd5 100644 --- a/backend/routes/dependencies/role_dependencies.py +++ b/backend/routes/dependencies/role_dependencies.py @@ -99,6 +99,17 @@ def ensure_teacher_authorized_for_subject( return teacher +def ensure_student_authorized_for_project( + project_id: int, + session: Session = Depends(get_session), + student: StudentDataclass = Depends(get_authenticated_student), +) -> StudentDataclass: + projects_of_student = get_projects_of_student(session, student.id) + if project_id not in [project.id for project in projects_of_student]: + raise NoAccessToDataError + return student + + def ensure_teacher_authorized_for_project( project_id: int, session: Session = Depends(get_session), diff --git a/backend/routes/group.py b/backend/routes/group.py index feb54beb..a3152f11 100644 --- a/backend/routes/group.py +++ b/backend/routes/group.py @@ -2,9 +2,19 @@ from sqlalchemy.orm import Session from db.sessions import get_session -from domain.logic.group import add_student_to_group, get_students_of_group, remove_student_from_group +from domain.logic.group import ( + add_student_to_group, + get_group_for_student_and_project, + get_students_of_group, + remove_student_from_group, +) +from domain.models.GroupDataclass import GroupDataclass from domain.models.StudentDataclass import StudentDataclass -from routes.dependencies.role_dependencies import ensure_student_authorized_for_group, ensure_user_authorized_for_group +from routes.dependencies.role_dependencies import ( + ensure_student_authorized_for_group, + ensure_student_authorized_for_project, + ensure_user_authorized_for_group, +) from routes.tags.swagger_tags import Tags group_router = APIRouter() @@ -39,3 +49,16 @@ def list_group_members( session: Session = Depends(get_session), ) -> list[StudentDataclass]: return get_students_of_group(session, group_id) + + +@group_router.get( + "/projects/{project_id}/group", + tags=[Tags.GROUP], + summary="Get your group for a project.", +) +def project_get_group( + project_id: int, + session: Session = Depends(get_session), + student: StudentDataclass = Depends(ensure_student_authorized_for_project), +) -> GroupDataclass | None: + return get_group_for_student_and_project(session, student.id, project_id) From da1ebe1d6757ddf0e1375b9895d8505bafc8690d Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Sun, 31 Mar 2024 18:04:54 +0200 Subject: [PATCH 11/13] Add route for listing students of a subject --- backend/domain/logic/project.py | 7 ------- backend/domain/logic/subject.py | 14 ++++++++++++++ backend/routes/subject.py | 15 +++++++++++++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/backend/domain/logic/project.py b/backend/domain/logic/project.py index fadb4ad2..10f76cb8 100644 --- a/backend/domain/logic/project.py +++ b/backend/domain/logic/project.py @@ -5,7 +5,6 @@ from db.models.models import Project, Student, Subject, Teacher from domain.logic.basic_operations import get, get_all from domain.models.ProjectDataclass import ProjectDataclass, ProjectInput -from domain.models.TeacherDataclass import TeacherDataclass def create_project( @@ -54,12 +53,6 @@ def get_projects_of_subject(session: Session, subject_id: int) -> list[ProjectDa return [project.to_domain_model() for project in projects] -def get_teachers_of_subject(session: Session, subject_id: int) -> list[TeacherDataclass]: - subject: Subject = get(session, Subject, ident=subject_id) - teachers = subject.teachers - return [teacher.to_domain_model() for teacher in teachers] - - def get_projects_of_student(session: Session, user_id: int) -> list[ProjectDataclass]: student = get(session, Student, ident=user_id) subjects = student.subjects diff --git a/backend/domain/logic/subject.py b/backend/domain/logic/subject.py index 91aaf9c4..6234b7f5 100644 --- a/backend/domain/logic/subject.py +++ b/backend/domain/logic/subject.py @@ -5,7 +5,9 @@ from domain.logic.basic_operations import get, get_all from domain.logic.student import is_user_student from domain.logic.teacher import is_user_teacher +from domain.models.StudentDataclass import StudentDataclass from domain.models.SubjectDataclass import SubjectDataclass +from domain.models.TeacherDataclass import TeacherDataclass def create_subject(session: Session, name: str) -> SubjectDataclass: @@ -68,3 +70,15 @@ def is_user_authorized_for_subject(subject_id: int, session: Session, uid: int) if subject_id in [subject.id for subject in subjects]: return True return False + + +def get_teachers_of_subject(session: Session, subject_id: int) -> list[TeacherDataclass]: + subject: Subject = get(session, Subject, ident=subject_id) + teachers = subject.teachers + return [teacher.to_domain_model() for teacher in teachers] + + +def get_students_of_subject(session: Session, subject_id: int) -> list[StudentDataclass]: + subject: Subject = get(session, Subject, ident=subject_id) + students = subject.students + return [student.to_domain_model() for student in students] diff --git a/backend/routes/subject.py b/backend/routes/subject.py index 661c5f2d..a45e335a 100644 --- a/backend/routes/subject.py +++ b/backend/routes/subject.py @@ -2,9 +2,10 @@ from sqlalchemy.orm import Session from db.sessions import get_session -from domain.logic.project import create_project, get_projects_of_subject, get_teachers_of_subject -from domain.logic.subject import get_subject +from domain.logic.project import create_project, get_projects_of_subject +from domain.logic.subject import get_students_of_subject, get_subject, get_teachers_of_subject from domain.models.ProjectDataclass import ProjectDataclass, ProjectInput +from domain.models.StudentDataclass import StudentDataclass from domain.models.SubjectDataclass import SubjectDataclass from domain.models.TeacherDataclass import TeacherDataclass from routes.dependencies.role_dependencies import ( @@ -47,6 +48,16 @@ def get_subject_teachers(subject_id: int, session: Session = Depends(get_session return get_teachers_of_subject(session, subject_id) +@subject_router.get( + "/subjects/{subject_id}/students", + dependencies=[Depends(ensure_user_authorized_for_subject)], + tags=[Tags.SUBJECT], + summary="Get all students of a certain subject.", +) +def get_subject_students(subject_id: int, session: Session = Depends(get_session)) -> list[StudentDataclass]: + return get_students_of_subject(session, subject_id) + + @subject_router.post( "/subjects/{subject_id}/projects", dependencies=[Depends(ensure_teacher_authorized_for_subject)], From 38c6a3e0c316788049f1506f563359a89813506c Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Tue, 2 Apr 2024 12:09:40 +0200 Subject: [PATCH 12/13] Correct email address --- backend/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 027c6e60..31b6037f 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -7,7 +7,7 @@ authors = [ "Albéric Loos ", "Emma Vandewalle ", "Lukas Barragan Torres ", - "Matthias Seghers< matthias.seghers@ugent.be>", + "Matthias Seghers ", "Robbe Van de Keere ", "Ruben Vandamme ", "Stef Ossé " From 7fb1592a91d60a59aa1a2b37035b351e10f153f1 Mon Sep 17 00:00:00 2001 From: Mathieu Strypsteen Date: Tue, 2 Apr 2024 12:15:04 +0200 Subject: [PATCH 13/13] Correct another email address --- backend/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 31b6037f..7a9cd990 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -5,7 +5,7 @@ description = "A Dodona clone" authors = [ "Mathieu Strypsteen ", "Albéric Loos ", - "Emma Vandewalle ", + "Emma Vandewalle ", "Lukas Barragan Torres ", "Matthias Seghers ", "Robbe Van de Keere ",