From 78ac406986f07e92f4f71055f3fcd1aded4a1678 Mon Sep 17 00:00:00 2001 From: Minkyu Lee Date: Mon, 12 Aug 2024 15:27:29 +0900 Subject: [PATCH 01/36] =?UTF-8?q?feat:=20SSE=20=EA=B8=B0=EB=B3=B8=20ping?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=82=AC=EC=8B=A4=20=EC=83=81?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0=20/=20CodeSubmission=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80=20-=20SSE=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20ping=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B0=84=EA=B2=A9=203600=EC=B4=88=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20-=20CodeSubmissionResult=20=EA=B0=80=20INTERNAL=5FS?= =?UTF-8?q?ERVER=5FERROR=20=EC=9D=BC=20=EB=95=8C=20CodeSubmission=20?= =?UTF-8?q?=EB=A5=BC=20=20=20Error=EB=A1=9C=20=ED=8C=90=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wacruit/src/apps/problem/services_v2.py | 4 +++- wacruit/src/apps/problem/views_v2.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/wacruit/src/apps/problem/services_v2.py b/wacruit/src/apps/problem/services_v2.py index 4ec6e71d..efd12252 100644 --- a/wacruit/src/apps/problem/services_v2.py +++ b/wacruit/src/apps/problem/services_v2.py @@ -148,8 +148,10 @@ def merge_status( response = await self.hodu_api_repository.submit(hodu_request) if isinstance(response, HoduSubmitResponse): submission_result_status = response.status.to_submission_result_status() - if submission_result_status != CodeSubmissionResultStatus.CORRECT: + if submission_result_status != CodeSubmissionResultStatus.CORRECT and submission_result_status != CodeSubmissionResultStatus.INTERNAL_SERVER_ERROR: merge_status(CodeSubmissionStatus.WRONG) + elif submission_result_status == CodeSubmissionResultStatus.INTERNAL_SERVER_ERROR: + merge_status(CodeSubmissionStatus.ERROR) self.problem_repository.update_submission_result( submission_result, submission_result_status, diff --git a/wacruit/src/apps/problem/views_v2.py b/wacruit/src/apps/problem/views_v2.py index a557fa86..91038dee 100644 --- a/wacruit/src/apps/problem/views_v2.py +++ b/wacruit/src/apps/problem/views_v2.py @@ -58,5 +58,5 @@ async def get_submission( problem_service: Annotated[ProblemService, Depends()], ): return EventSourceResponse( - problem_service.get_recent_submission_result(request, user, problem_id) + problem_service.get_recent_submission_result(request, user, problem_id), ping=3600 ) From 657ead83cc9dccbbec37662c3b9461961cc2de44 Mon Sep 17 00:00:00 2001 From: Minkyu Lee Date: Tue, 13 Aug 2024 19:00:55 +0900 Subject: [PATCH 02/36] debug hodu api --- wacruit/src/apps/problem/services_v2.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/wacruit/src/apps/problem/services_v2.py b/wacruit/src/apps/problem/services_v2.py index efd12252..30f27e84 100644 --- a/wacruit/src/apps/problem/services_v2.py +++ b/wacruit/src/apps/problem/services_v2.py @@ -148,9 +148,22 @@ def merge_status( response = await self.hodu_api_repository.submit(hodu_request) if isinstance(response, HoduSubmitResponse): submission_result_status = response.status.to_submission_result_status() - if submission_result_status != CodeSubmissionResultStatus.CORRECT and submission_result_status != CodeSubmissionResultStatus.INTERNAL_SERVER_ERROR: + if ( + submission_result_status != CodeSubmissionResultStatus.CORRECT + and submission_result_status + != CodeSubmissionResultStatus.INTERNAL_SERVER_ERROR + ): + print(f"############################################") + print(f"hodu_request: {hodu_request.json()}") + print(f"status: {response.status}") + print(f"stdout: {response.fields.stdout}") + print(f"stderr: {response.fields.stderr}") + print("\n\n") merge_status(CodeSubmissionStatus.WRONG) - elif submission_result_status == CodeSubmissionResultStatus.INTERNAL_SERVER_ERROR: + elif ( + submission_result_status + == CodeSubmissionResultStatus.INTERNAL_SERVER_ERROR + ): merge_status(CodeSubmissionStatus.ERROR) self.problem_repository.update_submission_result( submission_result, From 5f7a94bfb87fba3dbda7da051a54c42684d9ae2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Thu, 3 Jul 2025 21:00:35 +0900 Subject: [PATCH 03/36] =?UTF-8?q?feat:=20db=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - member, project, history, recruting_info, review, seminar, sponsor, timeline, faq 테이블에 대해 모델 정의를 작성했습니다. - recruit와 recruiting_info 관계를 정의했습니다. - pre-commit hook에 있는 pycln 버전에 문제가 있어 2.5.0 버전으로 변경했습니다. - migration 파일을 추가했습니다. --- .pre-commit-config.yaml | 2 +- wacruit/src/apps/faq/models.py | 13 ++ wacruit/src/apps/history/models.py | 15 ++ wacruit/src/apps/member/models.py | 39 +++++ wacruit/src/apps/project/models.py | 52 ++++++ wacruit/src/apps/recruiting/models.py | 4 + wacruit/src/apps/recruiting_info/models.py | 27 +++ wacruit/src/apps/review/models.py | 25 +++ wacruit/src/apps/seminar/models.py | 14 ++ wacruit/src/apps/sponsor/models.py | 20 +++ wacruit/src/apps/timeline/models.py | 21 +++ wacruit/src/database/__init__.py | 9 + ...79_add_table_member_project_recruiting_.py | 163 ++++++++++++++++++ 13 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 wacruit/src/apps/faq/models.py create mode 100644 wacruit/src/apps/history/models.py create mode 100644 wacruit/src/apps/member/models.py create mode 100644 wacruit/src/apps/project/models.py create mode 100644 wacruit/src/apps/recruiting_info/models.py create mode 100644 wacruit/src/apps/review/models.py create mode 100644 wacruit/src/apps/seminar/models.py create mode 100644 wacruit/src/apps/sponsor/models.py create mode 100644 wacruit/src/apps/timeline/models.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_03_1434-dfb42ebdb879_add_table_member_project_recruiting_.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e71f897..6f864d39 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,7 @@ repos: - id: pyright - repo: https://github.com/hadialqattan/pycln - rev: v2.1.3 + rev: v2.5.0 hooks: - id: pycln args: [--config=pyproject.toml] diff --git a/wacruit/src/apps/faq/models.py b/wacruit/src/apps/faq/models.py new file mode 100644 index 00000000..c6cbcf5f --- /dev/null +++ b/wacruit/src/apps/faq/models.py @@ -0,0 +1,13 @@ +from sqlalchemy.orm import Mapped + +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk +from wacruit.src.database.base import str255 + + +class FAQ(DeclarativeBase): + __tablename__ = "faq" + + id: Mapped[intpk] + question: Mapped[str255] + answer: Mapped[str255] diff --git a/wacruit/src/apps/history/models.py b/wacruit/src/apps/history/models.py new file mode 100644 index 00000000..ba17e39c --- /dev/null +++ b/wacruit/src/apps/history/models.py @@ -0,0 +1,15 @@ +from datetime import datetime + +from sqlalchemy.orm import Mapped + +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk + + +class History(DeclarativeBase): + __tablename__ = "histroy" + + id: Mapped[intpk] + total_projects: Mapped[int] + total_users: Mapped[int] + total_members: Mapped[int] diff --git a/wacruit/src/apps/member/models.py b/wacruit/src/apps/member/models.py new file mode 100644 index 00000000..e10a43d7 --- /dev/null +++ b/wacruit/src/apps/member/models.py @@ -0,0 +1,39 @@ +from datetime import datetime +from typing import TYPE_CHECKING + +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship + +from wacruit.src.apps.common.sql import CURRENT_TIMESTAMP +from wacruit.src.apps.project.models import project_member_association +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk +from wacruit.src.database.base import str30 +from wacruit.src.database.base import str50 +from wacruit.src.database.base import str255 + +if TYPE_CHECKING: + from wacruit.src.apps.project.models import Project + from wacruit.src.apps.review.models import Review + + +class Member(DeclarativeBase): + __tablename__ = "member" + + id: Mapped[intpk] + first_name: Mapped[str30] + last_name: Mapped[str30] + department: Mapped[str50 | None] + college: Mapped[str50 | None] + phone_number: Mapped[str30 | None] + github_id: Mapped[str30 | None] + generation: Mapped[str30] + is_active: Mapped[bool] + created_at: Mapped[datetime] = mapped_column(server_default=CURRENT_TIMESTAMP) + + leading_projects: Mapped[list["Project"]] = relationship(back_populates="leader") + projects: Mapped[list["Project"]] = relationship( + secondary=project_member_association, back_populates="members" + ) + review: Mapped["Review"] = relationship(back_populates="member") diff --git a/wacruit/src/apps/project/models.py b/wacruit/src/apps/project/models.py new file mode 100644 index 00000000..ef9b6887 --- /dev/null +++ b/wacruit/src/apps/project/models.py @@ -0,0 +1,52 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import Column +from sqlalchemy import ForeignKey +from sqlalchemy import Table +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship + +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk +from wacruit.src.database.base import str30 +from wacruit.src.database.base import str50 +from wacruit.src.database.base import str255 + +if TYPE_CHECKING: + from wacruit.src.apps.member.models import Member + +project_member_association = Table( + "project_member_association", + DeclarativeBase.metadata, + Column("member_id", ForeignKey("member.id")), + Column("project_id", ForeignKey("project.id")), +) + + +class Project(DeclarativeBase): + __tablename__ = "project" + + id: Mapped[intpk] + name: Mapped[str30] + summary: Mapped[str50 | None] + introduction: Mapped[str255 | None] + thumbnail_url: Mapped[str255 | None] + leader_id: Mapped[int] = mapped_column(ForeignKey("member.id")) + is_service: Mapped[bool] + + leader: Mapped["Member"] = relationship(back_populates="leading_projects") + members: Mapped[list["Member"]] = relationship( + secondary=project_member_association, back_populates="projects" + ) + urls: Mapped[list["ProjectURL"]] = relationship(back_populates="source_project") + + +class ProjectURL(DeclarativeBase): + __tablename__ = "project_url" + + id: Mapped[intpk] + project_id: Mapped[int] = mapped_column(ForeignKey("project.id")) + url: Mapped[str255] + + source_project: Mapped["Project"] = relationship(back_populates="urls") diff --git a/wacruit/src/apps/recruiting/models.py b/wacruit/src/apps/recruiting/models.py index 3a1c92c9..52653162 100644 --- a/wacruit/src/apps/recruiting/models.py +++ b/wacruit/src/apps/recruiting/models.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from wacruit.src.apps.problem.models import Problem + from wacruit.src.apps.recruiting_info.models import RecruitingInfo from wacruit.src.apps.resume.models import ResumeQuestion from wacruit.src.apps.resume.models import ResumeSubmission @@ -54,6 +55,9 @@ class Recruiting(DeclarativeBase): applicants: Mapped[list["RecruitingApplication"]] = relationship( back_populates="recruiting" ) + recruiting_info: Mapped[list["RecruitingInfo"]] = relationship( + back_populates="recruiting" + ) @property def is_open(self): diff --git a/wacruit/src/apps/recruiting_info/models.py b/wacruit/src/apps/recruiting_info/models.py new file mode 100644 index 00000000..f088277b --- /dev/null +++ b/wacruit/src/apps/recruiting_info/models.py @@ -0,0 +1,27 @@ +from datetime import datetime +from typing import TYPE_CHECKING + +from sqlalchemy import DateTime +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship + +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk +from wacruit.src.database.base import str50 + +if TYPE_CHECKING: + from wacruit.src.apps.recruiting.models import Recruiting + + +class RecruitingInfo(DeclarativeBase): + __tablename__ = "recruiting_info" + + id: Mapped[intpk] + info_num: Mapped[int] + title: Mapped[str50] + date_info: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + recruiting_id: Mapped[int] = mapped_column(ForeignKey("recruiting.id")) + + recruiting: Mapped["Recruiting"] = relationship(back_populates="recruiting_info") diff --git a/wacruit/src/apps/review/models.py b/wacruit/src/apps/review/models.py new file mode 100644 index 00000000..168bcbff --- /dev/null +++ b/wacruit/src/apps/review/models.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship + +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk +from wacruit.src.database.base import str30 +from wacruit.src.database.base import str255 + +if TYPE_CHECKING: + from wacruit.src.apps.member.models import Member + + +class Review(DeclarativeBase): + __tablename__ = "review" + + id: Mapped[intpk] + title: Mapped[str30] + content: Mapped[str255] + member_id: Mapped[int] = mapped_column(ForeignKey("member.id")) + + member: Mapped["Member"] = relationship(back_populates="review") diff --git a/wacruit/src/apps/seminar/models.py b/wacruit/src/apps/seminar/models.py new file mode 100644 index 00000000..dfcb9a65 --- /dev/null +++ b/wacruit/src/apps/seminar/models.py @@ -0,0 +1,14 @@ +from sqlalchemy.orm import Mapped + +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk +from wacruit.src.database.base import str30 +from wacruit.src.database.base import str255 + + +class Seminar(DeclarativeBase): + __tablename__ = "seminar" + + id: Mapped[intpk] + title: Mapped[str30] + content: Mapped[str255] diff --git a/wacruit/src/apps/sponsor/models.py b/wacruit/src/apps/sponsor/models.py new file mode 100644 index 00000000..a9b823f0 --- /dev/null +++ b/wacruit/src/apps/sponsor/models.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from sqlalchemy import DateTime +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column + +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk +from wacruit.src.database.base import str30 + + +class Sponsor(DeclarativeBase): + __tablename__ = "sponsor" + + id: Mapped[intpk] + name: Mapped[str30] + amount: Mapped[str30] + email: Mapped[str30 | None] + phone_number: Mapped[str30 | None] + sponsored_date: Mapped[datetime] = mapped_column(DateTime(timezone=True)) diff --git a/wacruit/src/apps/timeline/models.py b/wacruit/src/apps/timeline/models.py new file mode 100644 index 00000000..a87a5d08 --- /dev/null +++ b/wacruit/src/apps/timeline/models.py @@ -0,0 +1,21 @@ +from datetime import datetime + +from sqlalchemy import DateTime +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column + +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk +from wacruit.src.database.base import str30 +from wacruit.src.database.base import str255 + + +class Timeline(DeclarativeBase): + __tablename__ = "timeline" + + id: Mapped[intpk] + title: Mapped[str30] + content: Mapped[str255 | None] + category: Mapped[str30] + start_date: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True)) diff --git a/wacruit/src/database/__init__.py b/wacruit/src/database/__init__.py index 3a7160ef..554a4bc9 100644 --- a/wacruit/src/database/__init__.py +++ b/wacruit/src/database/__init__.py @@ -4,3 +4,12 @@ import wacruit.src.apps.portfolio.url.models # isort: skip import wacruit.src.apps.problem.models # isort: skip import wacruit.src.apps.resume.models # isort: skip +import wacruit.src.apps.recruiting_info.models # isort: skip +import wacruit.src.apps.seminar.models # isort: skip +import wacruit.src.apps.sponsor.models # isort: skip +import wacruit.src.apps.member.models # isort: skip +import wacruit.src.apps.review.models # isort: skip +import wacruit.src.apps.project.models # isort: skip +import wacruit.src.apps.faq.models # isort: skip +import wacruit.src.apps.history.models # isort: skip +import wacruit.src.apps.timeline.models # isort: skip diff --git a/wacruit/src/database/migrations/versions/2025_07_03_1434-dfb42ebdb879_add_table_member_project_recruiting_.py b/wacruit/src/database/migrations/versions/2025_07_03_1434-dfb42ebdb879_add_table_member_project_recruiting_.py new file mode 100644 index 00000000..a96537c6 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_03_1434-dfb42ebdb879_add_table_member_project_recruiting_.py @@ -0,0 +1,163 @@ +"""Add table: +member, project, recruiting, recruting_info, +review, seminar, sponsor, timeline, faq + +Revision ID: dfb42ebdb879 +Revises: cc8ee03dfd6e +Create Date: 2025-07-03 14:34:37.206398 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "dfb42ebdb879" +down_revision = "cc8ee03dfd6e" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "faq", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("question", sa.String(length=255), nullable=False), + sa.Column("answer", sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "histroy", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("total_projects", sa.Integer(), nullable=False), + sa.Column("total_users", sa.Integer(), nullable=False), + sa.Column("total_members", sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "member", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("first_name", sa.String(length=30), nullable=False), + sa.Column("last_name", sa.String(length=30), nullable=False), + sa.Column("department", sa.String(length=50), nullable=True), + sa.Column("college", sa.String(length=50), nullable=True), + sa.Column("phone_number", sa.String(length=30), nullable=True), + sa.Column("github_id", sa.String(length=30), nullable=True), + sa.Column("generation", sa.String(length=30), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.Column( + "created_at", + sa.DateTime(), + server_default=sa.text("CURRENT_TIMESTAMP"), + nullable=False, + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "seminar", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=30), nullable=False), + sa.Column("content", sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "sponsor", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=30), nullable=False), + sa.Column("amount", sa.String(length=30), nullable=False), + sa.Column("email", sa.String(length=30), nullable=True), + sa.Column("phone_number", sa.String(length=30), nullable=True), + sa.Column("sponsored_date", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "timeline", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=30), nullable=False), + sa.Column("content", sa.String(length=255), nullable=True), + sa.Column("category", sa.String(length=30), nullable=False), + sa.Column("start_date", sa.DateTime(timezone=True), nullable=False), + sa.Column("end_date", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "project", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=30), nullable=False), + sa.Column("summary", sa.String(length=50), nullable=True), + sa.Column("introduction", sa.String(length=255), nullable=True), + sa.Column("thumbnail_url", sa.String(length=255), nullable=True), + sa.Column("leader_id", sa.Integer(), nullable=False), + sa.Column("is_service", sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint( + ["leader_id"], + ["member.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "recruiting_info", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("info_num", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=50), nullable=False), + sa.Column("date_info", sa.DateTime(timezone=True), nullable=True), + sa.Column("recruiting_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["recruiting_id"], + ["recruiting.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "review", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=30), nullable=False), + sa.Column("content", sa.String(length=255), nullable=False), + sa.Column("member_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["member_id"], + ["member.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "project_member_association", + sa.Column("member_id", sa.Integer(), nullable=True), + sa.Column("project_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint( + ["member_id"], + ["member.id"], + ), + sa.ForeignKeyConstraint( + ["project_id"], + ["project.id"], + ), + ) + op.create_table( + "project_url", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("project_id", sa.Integer(), nullable=False), + sa.Column("url", sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint( + ["project_id"], + ["project.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("project_url") + op.drop_table("project_member_association") + op.drop_table("review") + op.drop_table("recruiting_info") + op.drop_table("project") + op.drop_table("timeline") + op.drop_table("sponsor") + op.drop_table("seminar") + op.drop_table("member") + op.drop_table("histroy") + op.drop_table("faq") + # ### end Alembic commands ### From 375f4c9d455f07e3ecdca9e1d35647f99a7b0e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:34:52 +0900 Subject: [PATCH 04/36] =?UTF-8?q?fix:=20db=20model=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Project Image URL 테이블을 추가했습니다. - timeline 테이블에서 content 필드를 삭제했습니다. - pre_registration 테이블을 추가했습니다. --- wacruit/src/apps/pre_registration/models.py | 15 +++++++ wacruit/src/apps/project/models.py | 13 ++++++ wacruit/src/apps/timeline/models.py | 2 - ...49c_add_table_pre_registration_project_.py | 42 +++++++++++++++++++ 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 wacruit/src/apps/pre_registration/models.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_04_0924-f1b48402a49c_add_table_pre_registration_project_.py diff --git a/wacruit/src/apps/pre_registration/models.py b/wacruit/src/apps/pre_registration/models.py new file mode 100644 index 00000000..7a18bfe9 --- /dev/null +++ b/wacruit/src/apps/pre_registration/models.py @@ -0,0 +1,15 @@ +from sqlalchemy.orm import Mapped + +from wacruit.src.database.base import DeclarativeBase +from wacruit.src.database.base import intpk +from wacruit.src.database.base import str30 +from wacruit.src.database.base import str255 + + +class PreRegistration(DeclarativeBase): + __tablename__ = "pre_registration" + + id: Mapped[intpk] + url: Mapped[str255] + generation: Mapped[str30] + is_active: Mapped[bool] diff --git a/wacruit/src/apps/project/models.py b/wacruit/src/apps/project/models.py index ef9b6887..3b767a0c 100644 --- a/wacruit/src/apps/project/models.py +++ b/wacruit/src/apps/project/models.py @@ -40,6 +40,9 @@ class Project(DeclarativeBase): secondary=project_member_association, back_populates="projects" ) urls: Mapped[list["ProjectURL"]] = relationship(back_populates="source_project") + image_urls: Mapped[list["ProjectImageURL"]] = relationship( + back_populates="source_project" + ) class ProjectURL(DeclarativeBase): @@ -50,3 +53,13 @@ class ProjectURL(DeclarativeBase): url: Mapped[str255] source_project: Mapped["Project"] = relationship(back_populates="urls") + + +class ProjectImageURL(DeclarativeBase): + __tablename__ = "project_image_url" + + id: Mapped[intpk] + project_id: Mapped[int] = mapped_column(ForeignKey("project.id")) + url: Mapped[str255] + + source_project: Mapped["Project"] = relationship(back_populates="image_urls") diff --git a/wacruit/src/apps/timeline/models.py b/wacruit/src/apps/timeline/models.py index a87a5d08..9442b390 100644 --- a/wacruit/src/apps/timeline/models.py +++ b/wacruit/src/apps/timeline/models.py @@ -7,7 +7,6 @@ from wacruit.src.database.base import DeclarativeBase from wacruit.src.database.base import intpk from wacruit.src.database.base import str30 -from wacruit.src.database.base import str255 class Timeline(DeclarativeBase): @@ -15,7 +14,6 @@ class Timeline(DeclarativeBase): id: Mapped[intpk] title: Mapped[str30] - content: Mapped[str255 | None] category: Mapped[str30] start_date: Mapped[datetime] = mapped_column(DateTime(timezone=True)) end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True)) diff --git a/wacruit/src/database/migrations/versions/2025_07_04_0924-f1b48402a49c_add_table_pre_registration_project_.py b/wacruit/src/database/migrations/versions/2025_07_04_0924-f1b48402a49c_add_table_pre_registration_project_.py new file mode 100644 index 00000000..2d9a3954 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_04_0924-f1b48402a49c_add_table_pre_registration_project_.py @@ -0,0 +1,42 @@ +"""add table pre_registration, project image urls + +Revision ID: f1b48402a49c +Revises: dfb42ebdb879 +Create Date: 2025-07-04 09:24:20.626606 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "f1b48402a49c" +down_revision = "dfb42ebdb879" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "project_image_url", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("project_id", sa.Integer(), nullable=False), + sa.Column("url", sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint( + ["project_id"], + ["project.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.drop_column("timeline", "content") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "timeline", sa.Column("content", mysql.VARCHAR(length=255), nullable=True) + ) + op.drop_table("project_image_url") + # ### end Alembic commands ### From 48822366204daabab08eae330a41e1daa28e359d Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Mon, 7 Jul 2025 20:28:06 +0900 Subject: [PATCH 05/36] Feat/project api (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - project api와 recruiting api를 구현했습니다. - 어드민 권한이 필요한 api의 경우에는 수작업으로 user table에서 is_admin 값이 true인 데이터를 만들어서 테스트 해보았습니다 로컬 회원가입을 사용하기로 하였으니 이후에 이와 관련한 작업이 필요할 것 같습니다. - 만들고 보니 recruiting_info를 생성하는 api가 없습니다. recruiting_info 생성에 대한 api가 필요할 것 같습니다 - 지금 현재 리크루트 업데이트 api에서 resume_questions이나 problems의 개별 항목을 업데이트 하지는 못하고, 기존 질문들을 모두 제거하고 새로 추가하는 식으로 구현되어 있습니다 - ProjectMember 테이블을 추가했습니다. - Project 테이블에서 project_type, is_active 필드를 추가했습니다. - ProjectUrl 테이블에서 title 필드를 추가했습니다. --- wacruit/src/apps/common/enums.py | 16 ++ wacruit/src/apps/common/exceptions.py | 10 + wacruit/src/apps/member/exceptions.py | 11 + wacruit/src/apps/member/models.py | 7 +- wacruit/src/apps/member/repositories.py | 13 ++ wacruit/src/apps/problem/schemas_v2.py | 32 +++ wacruit/src/apps/problem/views_v2.py | 3 +- wacruit/src/apps/project/__init__.py | 1 + wacruit/src/apps/project/exceptions.py | 11 + wacruit/src/apps/project/models.py | 22 +- wacruit/src/apps/project/repositories.py | 47 +++++ wacruit/src/apps/project/schemas.py | 92 +++++++++ wacruit/src/apps/project/services.py | 99 +++++++++ wacruit/src/apps/project/views.py | 85 ++++++++ wacruit/src/apps/recruiting/__init__.py | 3 +- wacruit/src/apps/recruiting/models.py | 1 + wacruit/src/apps/recruiting/repositories.py | 69 ++++++- wacruit/src/apps/recruiting/schemas.py | 94 ++++++++- wacruit/src/apps/recruiting/services.py | 190 +++++++++++++++++- wacruit/src/apps/recruiting/views.py | 6 +- wacruit/src/apps/recruiting/views_v3.py | 82 ++++++++ .../src/apps/recruiting_info/exceptions.py | 6 + wacruit/src/apps/recruiting_info/models.py | 4 + wacruit/src/apps/resume/schemas.py | 9 + wacruit/src/apps/router.py | 5 +- .../versions/2025_07_06_1701-066dac40ece1_.py | 94 +++++++++ .../2025_07_06_1808-f656d4e56c64_fix_typo.py | 40 ++++ ..._remove_projectmember_table_and_leader_.py | 54 +++++ ...da1cdd4_add_unique_constraint_in_table_.py | 29 +++ ...b0d638c64db_change_member_position_type.py | 40 ++++ wacruit/src/settings.py | 1 + .../recruiting/test_recruiting_servcie.py | 4 +- 32 files changed, 1148 insertions(+), 32 deletions(-) create mode 100644 wacruit/src/apps/member/exceptions.py create mode 100644 wacruit/src/apps/member/repositories.py create mode 100644 wacruit/src/apps/project/__init__.py create mode 100644 wacruit/src/apps/project/exceptions.py create mode 100644 wacruit/src/apps/project/repositories.py create mode 100644 wacruit/src/apps/project/schemas.py create mode 100644 wacruit/src/apps/project/services.py create mode 100644 wacruit/src/apps/project/views.py create mode 100644 wacruit/src/apps/recruiting/views_v3.py create mode 100644 wacruit/src/apps/recruiting_info/exceptions.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_06_1701-066dac40ece1_.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_06_1808-f656d4e56c64_fix_typo.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_06_2229-2a2274e82630_remove_projectmember_table_and_leader_.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_07_1759-452f1da1cdd4_add_unique_constraint_in_table_.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_07_1815-5b0d638c64db_change_member_position_type.py diff --git a/wacruit/src/apps/common/enums.py b/wacruit/src/apps/common/enums.py index a3aa0cf2..05ebf037 100644 --- a/wacruit/src/apps/common/enums.py +++ b/wacruit/src/apps/common/enums.py @@ -73,3 +73,19 @@ class RecruitingApplicationStatus(Enum): IN_PROGRESS = 1 ACCEPTED = 2 REJECTED = 3 + + +class ProjectType(Enum): + SERVICE = 1 + STUDY = 2 + + +class Position(Enum): + FRONTEND = 1 + BACKEND = 2 + DESIGNER = 3 + ANDROID = 4 + IOS = 5 + # INFRA = 6 + # PM = 7 + # OPERATIONS = 8 diff --git a/wacruit/src/apps/common/exceptions.py b/wacruit/src/apps/common/exceptions.py index 7d457745..bd44b95d 100644 --- a/wacruit/src/apps/common/exceptions.py +++ b/wacruit/src/apps/common/exceptions.py @@ -36,3 +36,13 @@ def responses_from( for exc in exceptions: responses.update(exc().to_response()) return responses + + +class InvalidRecruitTypeException(WacruitException): + def __init__(self, type: str): + super().__init__(status_code=400, detail=f"Invalid recruiting type: {type}") + + +class InvalidProjectTypeException(WacruitException): + def __init__(self, type: str): + super().__init__(status_code=400, detail=f"Invalid project type: {type}") diff --git a/wacruit/src/apps/member/exceptions.py b/wacruit/src/apps/member/exceptions.py new file mode 100644 index 00000000..3e4a06bd --- /dev/null +++ b/wacruit/src/apps/member/exceptions.py @@ -0,0 +1,11 @@ +from fastapi import HTTPException + + +class MemberNotFoundException(HTTPException): + def __init__(self): + super().__init__(status_code=404, detail="Member not found.") + + +class MemberAlreadyExistsException(HTTPException): + def __init__(self): + super().__init__(status_code=400, detail="Member already exists.") diff --git a/wacruit/src/apps/member/models.py b/wacruit/src/apps/member/models.py index e10a43d7..77af8a48 100644 --- a/wacruit/src/apps/member/models.py +++ b/wacruit/src/apps/member/models.py @@ -5,8 +5,8 @@ from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship +from wacruit.src.apps.common.enums import Position from wacruit.src.apps.common.sql import CURRENT_TIMESTAMP -from wacruit.src.apps.project.models import project_member_association from wacruit.src.database.base import DeclarativeBase from wacruit.src.database.base import intpk from wacruit.src.database.base import str30 @@ -31,9 +31,6 @@ class Member(DeclarativeBase): generation: Mapped[str30] is_active: Mapped[bool] created_at: Mapped[datetime] = mapped_column(server_default=CURRENT_TIMESTAMP) + position: Mapped[Position | None] = mapped_column(default=None) - leading_projects: Mapped[list["Project"]] = relationship(back_populates="leader") - projects: Mapped[list["Project"]] = relationship( - secondary=project_member_association, back_populates="members" - ) review: Mapped["Review"] = relationship(back_populates="member") diff --git a/wacruit/src/apps/member/repositories.py b/wacruit/src/apps/member/repositories.py new file mode 100644 index 00000000..62f5e589 --- /dev/null +++ b/wacruit/src/apps/member/repositories.py @@ -0,0 +1,13 @@ +from fastapi import Depends +from sqlalchemy.orm import Session + +from wacruit.src.apps.member.models import Member +from wacruit.src.database.connection import get_db_session + + +class MemberRepository: + def __init__(self, session: Session = Depends(get_db_session)) -> None: + self.session = session + + def get_member_by_id(self, member_id: int) -> Member | None: + return self.session.query(Member).filter(Member.id == member_id).first() diff --git a/wacruit/src/apps/problem/schemas_v2.py b/wacruit/src/apps/problem/schemas_v2.py index 7cfa1c09..f2b6ae3c 100644 --- a/wacruit/src/apps/problem/schemas_v2.py +++ b/wacruit/src/apps/problem/schemas_v2.py @@ -13,6 +13,16 @@ class TestcaseDto(OrmModel): expected_output: str +class TestCaseCreateDto(BaseModel): + stdin: str = Field(..., max_length=10000) + expected_output: str = Field(..., max_length=10000) + time_limit: float = Field(..., ge=0.0) + extra_time: float = Field(..., ge=0.0) + memory_limit: int = Field(..., ge=0.0) + stack_limit: int = Field(..., ge=0.0) + is_example: bool + + class ProblemResponse(OrmModel): id: int num: int @@ -36,4 +46,26 @@ class CodeSubmissionResultResponse(BaseModel): memory: float | None = None +class ProblemCreateDto(BaseModel): + num: int + body: str + testcases: list[TestCaseCreateDto] = Field(..., min_items=1) + + @classmethod + def from_orm(cls, obj) -> "ProblemCreateDto": + testcases = [ + TestCaseCreateDto( + stdin=testcase.stdin, + expected_output=testcase.expected_output, + time_limit=float(testcase.time_limit), + extra_time=float(testcase.extra_time), + memory_limit=testcase.memory_limit, + stack_limit=testcase.stack_limit, + is_example=testcase.is_example, + ) + for testcase in obj.testcases + ] + return cls(num=obj.num, body=obj.body, testcases=testcases) + + TokenStr = str diff --git a/wacruit/src/apps/problem/views_v2.py b/wacruit/src/apps/problem/views_v2.py index 91038dee..a3cdf115 100644 --- a/wacruit/src/apps/problem/views_v2.py +++ b/wacruit/src/apps/problem/views_v2.py @@ -58,5 +58,6 @@ async def get_submission( problem_service: Annotated[ProblemService, Depends()], ): return EventSourceResponse( - problem_service.get_recent_submission_result(request, user, problem_id), ping=3600 + problem_service.get_recent_submission_result(request, user, problem_id), + ping=3600, ) diff --git a/wacruit/src/apps/project/__init__.py b/wacruit/src/apps/project/__init__.py new file mode 100644 index 00000000..6dc77762 --- /dev/null +++ b/wacruit/src/apps/project/__init__.py @@ -0,0 +1 @@ +from .views import v3_router as router diff --git a/wacruit/src/apps/project/exceptions.py b/wacruit/src/apps/project/exceptions.py new file mode 100644 index 00000000..f8811aa5 --- /dev/null +++ b/wacruit/src/apps/project/exceptions.py @@ -0,0 +1,11 @@ +from wacruit.src.apps.common.exceptions import WacruitException + + +class ProjectNotFoundException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="존재하지 않는 프로젝트입니다.") + + +class ProjectAlreadyExistsException(WacruitException): + def __init__(self): + super().__init__(status_code=409, detail="이미 존재하는 프로젝트입니다.") diff --git a/wacruit/src/apps/project/models.py b/wacruit/src/apps/project/models.py index 3b767a0c..46b00cfa 100644 --- a/wacruit/src/apps/project/models.py +++ b/wacruit/src/apps/project/models.py @@ -7,6 +7,7 @@ from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship +from wacruit.src.apps.common.enums import ProjectType from wacruit.src.database.base import DeclarativeBase from wacruit.src.database.base import intpk from wacruit.src.database.base import str30 @@ -16,13 +17,6 @@ if TYPE_CHECKING: from wacruit.src.apps.member.models import Member -project_member_association = Table( - "project_member_association", - DeclarativeBase.metadata, - Column("member_id", ForeignKey("member.id")), - Column("project_id", ForeignKey("project.id")), -) - class Project(DeclarativeBase): __tablename__ = "project" @@ -32,15 +26,12 @@ class Project(DeclarativeBase): summary: Mapped[str50 | None] introduction: Mapped[str255 | None] thumbnail_url: Mapped[str255 | None] - leader_id: Mapped[int] = mapped_column(ForeignKey("member.id")) - is_service: Mapped[bool] - - leader: Mapped["Member"] = relationship(back_populates="leading_projects") - members: Mapped[list["Member"]] = relationship( - secondary=project_member_association, back_populates="projects" + project_type: Mapped[ProjectType] + is_active: Mapped[bool] = mapped_column(default=True) + urls: Mapped[list["ProjectURL"] | None] = relationship( + back_populates="source_project" ) - urls: Mapped[list["ProjectURL"]] = relationship(back_populates="source_project") - image_urls: Mapped[list["ProjectImageURL"]] = relationship( + image_urls: Mapped[list["ProjectImageURL"] | None] = relationship( back_populates="source_project" ) @@ -50,6 +41,7 @@ class ProjectURL(DeclarativeBase): id: Mapped[intpk] project_id: Mapped[int] = mapped_column(ForeignKey("project.id")) + title: Mapped[str50] url: Mapped[str255] source_project: Mapped["Project"] = relationship(back_populates="urls") diff --git a/wacruit/src/apps/project/repositories.py b/wacruit/src/apps/project/repositories.py new file mode 100644 index 00000000..798111e8 --- /dev/null +++ b/wacruit/src/apps/project/repositories.py @@ -0,0 +1,47 @@ +from fastapi import Depends +from sqlalchemy.orm import joinedload +from sqlalchemy.orm import Session + +from wacruit.src.apps.project.models import Project +from wacruit.src.apps.project.models import ProjectImageURL +from wacruit.src.apps.project.models import ProjectURL +from wacruit.src.database.connection import get_db_session +from wacruit.src.database.connection import Transaction + + +class ProjectRepository: + def __init__( + self, + session: Session = Depends(get_db_session), + transaction: Transaction = Depends(), + ): + self.session = session + self.transaction = transaction + + def get_project_by_id(self, project_id: int) -> Project | None: + return ( + self.session.query(Project) + .options( + joinedload(Project.urls), + joinedload(Project.image_urls), + ) + .filter(Project.id == project_id) + .first() + ) + + def get_project_by_name(self, name: str) -> Project | None: + return self.session.query(Project).filter(Project.name == name).first() + + def get_projects(self) -> list[Project]: + return self.session.query(Project).all() + + def create_project(self, project: Project) -> Project: + with self.transaction: + self.session.add(project) + return project + + def update_project(self, project: Project) -> Project: + with self.transaction: + self.session.merge(project) + + return project diff --git a/wacruit/src/apps/project/schemas.py b/wacruit/src/apps/project/schemas.py new file mode 100644 index 00000000..8f44fec9 --- /dev/null +++ b/wacruit/src/apps/project/schemas.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from pydantic import BaseModel +from pydantic import Field + +from wacruit.src.apps.common.enums import ProjectType +from wacruit.src.apps.common.schemas import OrmModel + + +class ProjectLinkDto(BaseModel): + title: str + url: str + + +class ProjectCreateRequest(BaseModel): + name: str = Field(..., max_length=30) + summary: str | None = Field(None, max_length=50) + introduction: str | None = Field(None, max_length=255) + thumbnail_url: str | None = Field(None, max_length=255) + project_type: str + is_active: bool + images: list[str] | None + urls: list[ProjectLinkDto] | None + + +class ProjectUpdateRequest(BaseModel): + name: str | None = Field(None, max_length=30) + summary: str | None = Field(None, max_length=50) + introduction: str | None = Field(None, max_length=255) + thumbnail_url: str | None = Field(None, max_length=255) + project_type: str | None + is_active: bool | None + images: list[str] | None + urls: list[ProjectLinkDto] | None + + +class ProjectDetailResponse(OrmModel): + id: int + name: str + summary: str | None + introduction: str | None + thumbnail_url: str | None + project_type: str + is_active: bool + images: list[str] | None + urls: list[ProjectLinkDto] | None + + @classmethod + def from_orm(cls, obj): + urls = None + if obj.urls: + urls = [ProjectLinkDto(title=url.title, url=url.url) for url in obj.urls] + + images = None + if obj.image_urls: + images = [img.url for img in obj.image_urls] + + return cls( + id=obj.id, + name=obj.name, + summary=obj.summary, + introduction=obj.introduction, + thumbnail_url=obj.thumbnail_url, + project_type=obj.project_type.name, + is_active=obj.is_active, + images=images, + urls=urls, + ) + + +class ProjectBriefResponse(OrmModel): + id: int + name: str + summary: str | None + thumbnail_url: str | None + project_type: str + is_active: bool + + @classmethod + def from_orm(cls, obj): + return cls( + id=obj.id, + name=obj.name, + summary=obj.summary, + thumbnail_url=obj.thumbnail_url, + project_type=obj.project_type.name, + is_active=obj.is_active, + ) + + +class ProjectListResponse(OrmModel): + projects: list[ProjectBriefResponse] diff --git a/wacruit/src/apps/project/services.py b/wacruit/src/apps/project/services.py new file mode 100644 index 00000000..0d61adf6 --- /dev/null +++ b/wacruit/src/apps/project/services.py @@ -0,0 +1,99 @@ +from typing import Annotated, TYPE_CHECKING + +from fastapi import Depends + +from wacruit.src.apps.common.enums import ProjectType +from wacruit.src.apps.common.exceptions import InvalidProjectTypeException +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.member.exceptions import MemberAlreadyExistsException +from wacruit.src.apps.member.exceptions import MemberNotFoundException +from wacruit.src.apps.member.repositories import MemberRepository +from wacruit.src.apps.project.exceptions import ProjectAlreadyExistsException +from wacruit.src.apps.project.exceptions import ProjectNotFoundException +from wacruit.src.apps.project.models import Project +from wacruit.src.apps.project.models import ProjectImageURL +from wacruit.src.apps.project.models import ProjectURL +from wacruit.src.apps.project.repositories import ProjectRepository +from wacruit.src.apps.project.schemas import ProjectBriefResponse +from wacruit.src.apps.project.schemas import ProjectCreateRequest +from wacruit.src.apps.project.schemas import ProjectDetailResponse +from wacruit.src.apps.project.schemas import ProjectLinkDto +from wacruit.src.apps.project.schemas import ProjectUpdateRequest + + +class ProjectService: + def __init__( + self, + member_repository: MemberRepository = Depends(), + project_repository: ProjectRepository = Depends(), + ) -> None: + self.member_repository = member_repository + self.project_repository = project_repository + + def create_project(self, request: ProjectCreateRequest): + if self.project_repository.get_project_by_name(request.name): + raise ProjectAlreadyExistsException + + if request.project_type not in ProjectType.__members__: + raise InvalidProjectTypeException(request.project_type) + + project = Project( + name=request.name, + summary=request.summary, + introduction=request.introduction, + thumbnail_url=request.thumbnail_url, + project_type=request.project_type, + is_active=request.is_active, + ) + + # 프로젝트를 먼저 생성하여 project_id를 얻음 + created_project = self.project_repository.create_project(project) + + # 프로젝트가 생성된 후 이미지와 URL 추가 + if request.images: + if created_project.image_urls is None: + created_project.image_urls = [] + for image_url in request.images: + image_obj = ProjectImageURL( + project_id=created_project.id, url=image_url + ) + created_project.image_urls.append(image_obj) + + if request.urls: + if created_project.urls is None: + created_project.urls = [] + for url_dto in request.urls: + url_obj = ProjectURL( + project_id=created_project.id, title=url_dto.title, url=url_dto.url + ) + created_project.urls.append(url_obj) + + # 이미지와 URL이 추가된 프로젝트를 업데이트 + if request.images or request.urls: + created_project = self.project_repository.update_project(created_project) + + def get_project(self, project_id: int) -> ProjectDetailResponse: + project = self.project_repository.get_project_by_id(project_id) + if not project: + raise ProjectNotFoundException + return ProjectDetailResponse.from_orm(project) + + def list_projects(self) -> ListResponse[ProjectBriefResponse]: + projects = self.project_repository.get_projects() + items = [] + for project in projects: + items.append(ProjectBriefResponse.from_orm(project)) + return ListResponse(items=items) + + def update_project( + self, project_id: int, request: ProjectUpdateRequest + ) -> ProjectDetailResponse: + project = self.project_repository.get_project_by_id(project_id) + if not project: + raise ProjectNotFoundException + for key, value in request.dict(exclude_none=True).items(): + setattr(project, key, value) + updated_project = self.project_repository.update_project(project) + if not updated_project: + raise ProjectNotFoundException + return ProjectDetailResponse.from_orm(updated_project) diff --git a/wacruit/src/apps/project/views.py b/wacruit/src/apps/project/views.py new file mode 100644 index 00000000..8ffb7ad8 --- /dev/null +++ b/wacruit/src/apps/project/views.py @@ -0,0 +1,85 @@ +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.project.schemas import ProjectBriefResponse +from wacruit.src.apps.project.schemas import ProjectCreateRequest +from wacruit.src.apps.project.schemas import ProjectDetailResponse +from wacruit.src.apps.project.schemas import ProjectUpdateRequest +from wacruit.src.apps.project.services import ProjectService +from wacruit.src.apps.user.dependencies import AdminUser + +v3_router = APIRouter(prefix="/v3/projects", tags=["projects"]) + + +@v3_router.post("/") +def create_project( + request: ProjectCreateRequest, + project_service: Annotated[ProjectService, Depends()], + admin_user: AdminUser, +): + return project_service.create_project(request) + + +@v3_router.get("/{project_id}") +def get_project( + project_id: int, project_service: Annotated[ProjectService, Depends()] +) -> ProjectDetailResponse: + return project_service.get_project(project_id) + + +@v3_router.get("/") +def list_projects( + project_service: Annotated[ProjectService, Depends()] +) -> ListResponse[ProjectBriefResponse]: + return project_service.list_projects() + + +@v3_router.patch("/{project_id}") +def update_project( + admin_user: AdminUser, + project_id: int, + request: ProjectUpdateRequest, + project_service: Annotated[ProjectService, Depends()], +) -> ProjectDetailResponse: + return project_service.update_project(project_id, request) + + +# @v3_router.post("/{project_id}/members") +# def add_project_member( +# admin_user: AdminUser, +# project_id: int, +# request: ProjectMemberCreateRequest, +# project_service: Annotated[ProjectService, Depends()], +# ): +# return project_service.add_project_member(project_id, request) + + +# @v3_router.get("/{project_id}/members") +# def list_project_members( +# project_id: int, project_service: Annotated[ProjectService, Depends()] +# ) -> ListResponse[ProjectMemberResponse]: +# return project_service.list_project_members(project_id) + + +# @v3_router.patch("/{project_id}/members/{member_id}") +# def update_project_member( +# admin_user: AdminUser, +# project_id: int, +# member_id: int, +# request: ProjectMemberUpdateRequest, +# project_service: Annotated[ProjectService, Depends()], +# ) -> ListResponse[ProjectMemberResponse]: +# return project_service.update_project_member(project_id, member_id, request) + + +# @v3_router.delete("/{project_id}/members/{member_id}") +# def delete_project_member( +# admin_user: AdminUser, +# project_id: int, +# member_id: int, +# project_service: Annotated[ProjectService, Depends()], +# ) -> ListResponse[ProjectMemberResponse]: +# return project_service.delete_project_member(project_id, member_id) diff --git a/wacruit/src/apps/recruiting/__init__.py b/wacruit/src/apps/recruiting/__init__.py index 7548e10c..a3416682 100644 --- a/wacruit/src/apps/recruiting/__init__.py +++ b/wacruit/src/apps/recruiting/__init__.py @@ -1 +1,2 @@ -from .views import v1_router as router +from .views import v1_router +from .views_v3 import v3_router diff --git a/wacruit/src/apps/recruiting/models.py b/wacruit/src/apps/recruiting/models.py index 52653162..40bbff25 100644 --- a/wacruit/src/apps/recruiting/models.py +++ b/wacruit/src/apps/recruiting/models.py @@ -34,6 +34,7 @@ class Recruiting(DeclarativeBase): id: Mapped[intpk] name: Mapped[str30] + generation: Mapped[str30] = mapped_column(server_default=text("0.0")) type: Mapped[RecruitingType] = mapped_column(server_default=text("'ROOKIE'")) is_active: Mapped[bool] from_date: Mapped[datetime | None] = mapped_column( diff --git a/wacruit/src/apps/recruiting/repositories.py b/wacruit/src/apps/recruiting/repositories.py index fce89ff3..cd097aab 100644 --- a/wacruit/src/apps/recruiting/repositories.py +++ b/wacruit/src/apps/recruiting/repositories.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import Session from wacruit.src.apps.common.enums import RecruitingApplicationStatus +from wacruit.src.apps.common.enums import RecruitingType from wacruit.src.apps.problem.models import CodeSubmission from wacruit.src.apps.problem.models import Problem from wacruit.src.apps.recruiting.exceptions import RecruitingAlreadyAppliedException @@ -16,6 +17,7 @@ from wacruit.src.apps.recruiting.exceptions import RecruitingNotFoundException from wacruit.src.apps.recruiting.models import Recruiting from wacruit.src.apps.recruiting.models import RecruitingApplication +from wacruit.src.apps.recruiting_info.models import RecruitingInfo from wacruit.src.database.connection import get_db_session from wacruit.src.database.connection import Transaction @@ -46,7 +48,7 @@ def get_rookie_applicant_count(self, recruiting_id: int) -> int: def get_recruiting_by_id(self, recruiting_id: int) -> Recruiting | None: query = select(Recruiting).where(Recruiting.id == recruiting_id) - return self.session.execute(query).scalar_one() + return self.session.execute(query).scalar() def get_recruiting_with_code_submission_status_by_id( self, recruiting_id: int, user_id: int @@ -115,3 +117,68 @@ def delete_recruiting_application( with self.transaction: self.session.delete(application) return application + + def get_active_recruitings(self) -> Sequence[Recruiting]: + query = ( + select(Recruiting) + .where(Recruiting.is_active.is_(True)) + .order_by(Recruiting.from_date.desc()) + ) + return self.session.execute(query).scalars().all() + + def get_recruiting_infos_by_type( + self, recruiting_type: RecruitingType + ) -> Sequence[RecruitingInfo] | None: + # 먼저 해당 타입에서 to_date가 가장 큰 recruiting_id를 찾습니다 + latest_recruiting_subquery = ( + select(Recruiting.id) + .where(Recruiting.type == recruiting_type) + .order_by(Recruiting.to_date.desc()) + .limit(1) + ).scalar_subquery() + + query = ( + select(RecruitingInfo) + .where(RecruitingInfo.recruiting_id == latest_recruiting_subquery) + .order_by(RecruitingInfo.info_num.asc()) + ) + return self.session.execute(query).scalars().all() + + def get_recruiting_info_by_info_num( + self, info_num: int, recruiting_id: int + ) -> RecruitingInfo | None: + query = select(RecruitingInfo).where( + RecruitingInfo.info_num == info_num, + RecruitingInfo.recruiting_id == recruiting_id, + ) + return self.session.execute(query).scalar() + + def create_recruiting(self, recruiting: Recruiting) -> Recruiting: + with self.transaction: + self.session.add(recruiting) + self.session.flush() + return recruiting + + def update_recruiting(self, recruiting: Recruiting) -> Recruiting: + with self.transaction: + self.session.merge(recruiting) + self.session.flush() + return recruiting + + def create_recruiting_info(self, recruiting_info: RecruitingInfo): + with self.transaction: + self.session.add(recruiting_info) + self.session.flush() + return recruiting_info + + def get_recruiting_info_by_id( + self, recruiting_info_id: int + ) -> RecruitingInfo | None: + query = select(RecruitingInfo).where(RecruitingInfo.id == recruiting_info_id) + return self.session.execute(query).scalar() + + def update_recruiting_info(self, recruiting_info: RecruitingInfo) -> RecruitingInfo: + with self.transaction: + self.session.merge(recruiting_info) + self.session.flush() + return recruiting_info diff --git a/wacruit/src/apps/recruiting/schemas.py b/wacruit/src/apps/recruiting/schemas.py index 48a728e0..b8a21b4a 100644 --- a/wacruit/src/apps/recruiting/schemas.py +++ b/wacruit/src/apps/recruiting/schemas.py @@ -7,6 +7,8 @@ from wacruit.src.apps.common.enums import RecruitingApplicationStatus from wacruit.src.apps.common.enums import RecruitingType from wacruit.src.apps.common.schemas import OrmModel +from wacruit.src.apps.problem.schemas_v2 import ProblemCreateDto +from wacruit.src.apps.resume.schemas import ResumeQuestionCreateDto if TYPE_CHECKING: from wacruit.src.apps.problem.models import Problem @@ -15,6 +17,7 @@ class RecruitingSummaryResponse(OrmModel): id: int name: str + generation: str type: RecruitingType is_active: bool from_date: datetime | None @@ -29,7 +32,10 @@ class ProblemStatusDto(OrmModel): status: CodeSubmissionStatus | Literal[0] # 0 means NOT_SUBMITTED -class RecruitingResponse(OrmModel): +# 기존에 존재하던 RecruitingResponse을 UserRecruitingResponse로 변경 + + +class UserRecruitingResponse(OrmModel): id: int name: str type: RecruitingType @@ -41,5 +47,91 @@ class RecruitingResponse(OrmModel): problem_status: list[ProblemStatusDto] +class RecruitingResponse(OrmModel): + id: int + name: str + generation: str + type: RecruitingType + is_active: bool + from_date: datetime | None + to_date: datetime | None + short_description: str | None = None + description: str + resume_questions: list[ResumeQuestionCreateDto] + problems: list[ProblemCreateDto] | None = None + + @classmethod + def from_orm(cls, obj): + resume_questions = [ + ResumeQuestionCreateDto.from_orm(question) + for question in obj.resume_questions + ] + problems = ( + [ProblemCreateDto.from_orm(problem) for problem in obj.problems] + if obj.problems + else None + ) + return cls( + id=obj.id, + name=obj.name, + generation=obj.generation, + type=obj.type, + is_active=obj.is_active, + from_date=obj.from_date, + to_date=obj.to_date, + short_description=obj.short_description, + description=obj.description, + resume_questions=resume_questions, + problems=problems, + ) + + class RecruitingResultResponse(BaseModel): status: RecruitingApplicationStatus + + +class RecruitingCreateRequest(BaseModel): + name: str + type: str + generation: str + is_active: bool + from_date: datetime + to_date: datetime + short_description: str | None = None + description: str + resume_questions: list[ResumeQuestionCreateDto] + problems: list[ProblemCreateDto] | None = None + + +class RecruitingUpdateRequest(BaseModel): + name: str | None = None + type: RecruitingType | None = None + generation: str | None = None + is_active: bool | None = None + from_date: datetime | None = None + to_date: datetime | None = None + short_description: str | None = None + description: str | None = None + resume_questions: list[ResumeQuestionCreateDto] | None = None + problems: list[ProblemCreateDto] | None = None + + +class RecruitingInfoResponse(OrmModel): + id: int + type: str + info_num: int + title: str + date_info: datetime + + +class RecruitingInfoCreateRequest(BaseModel): + info_num: int + title: str + date_info: datetime + recruiting_id: int + + +class RecruitingInfoUpdateRequest(BaseModel): + info_num: int | None = None + title: str | None = None + date_info: datetime | None = None diff --git a/wacruit/src/apps/recruiting/services.py b/wacruit/src/apps/recruiting/services.py index d8b00c1b..00353b3b 100644 --- a/wacruit/src/apps/recruiting/services.py +++ b/wacruit/src/apps/recruiting/services.py @@ -1,17 +1,31 @@ from typing import Annotated from fastapi import Depends +from sqlalchemy.exc import IntegrityError from wacruit.src.apps.common.enums import RecruitingType +from wacruit.src.apps.common.exceptions import InvalidRecruitTypeException from wacruit.src.apps.common.schemas import ListResponse from wacruit.src.apps.portfolio.file.services import PortfolioFileService from wacruit.src.apps.portfolio.url.services import PortfolioUrlService +from wacruit.src.apps.problem.models import Problem +from wacruit.src.apps.problem.models import Testcase from wacruit.src.apps.recruiting.exceptions import RecruitingNotAppliedException from wacruit.src.apps.recruiting.exceptions import RecruitingNotFoundException +from wacruit.src.apps.recruiting.models import Recruiting from wacruit.src.apps.recruiting.repositories import RecruitingRepository +from wacruit.src.apps.recruiting.schemas import RecruitingCreateRequest +from wacruit.src.apps.recruiting.schemas import RecruitingInfoCreateRequest +from wacruit.src.apps.recruiting.schemas import RecruitingInfoResponse +from wacruit.src.apps.recruiting.schemas import RecruitingInfoUpdateRequest from wacruit.src.apps.recruiting.schemas import RecruitingResponse from wacruit.src.apps.recruiting.schemas import RecruitingResultResponse from wacruit.src.apps.recruiting.schemas import RecruitingSummaryResponse +from wacruit.src.apps.recruiting.schemas import RecruitingUpdateRequest +from wacruit.src.apps.recruiting.schemas import UserRecruitingResponse +from wacruit.src.apps.recruiting_info.exceptions import InfoAlreadyExistsException +from wacruit.src.apps.recruiting_info.models import RecruitingInfo +from wacruit.src.apps.resume.models import ResumeQuestion from wacruit.src.apps.resume.services import ResumeService from wacruit.src.apps.user.models import User from wacruit.src.apps.user.services import UserService @@ -64,9 +78,9 @@ def get_all_recruiting(self) -> ListResponse[RecruitingSummaryResponse]: ) return ListResponse(items=items) - def get_recruiting_by_id( + def get_user_recruiting_by_id( self, recruiting_id: int, user: User - ) -> RecruitingResponse: + ) -> UserRecruitingResponse: recruiting = ( self.recruiting_repository.get_recruiting_with_code_submission_status_by_id( recruiting_id, user.id @@ -75,7 +89,7 @@ def get_recruiting_by_id( if recruiting is None: raise RecruitingNotFoundException() - return RecruitingResponse.from_orm(recruiting) + return UserRecruitingResponse.from_orm(recruiting) def get_recruiting_result_by_id( self, recruiting_id: int, user: User @@ -97,3 +111,173 @@ def cancel_recruiting(self, recruiting_id: int, user: User) -> None: self.portfolio_url_service.delete_all_portfolio_urls(user.id, recruiting_id) self.resume_service.delete_all_resumes(user.id, recruiting_id) self.recruiting_repository.delete_recruiting_application(recruiting_id, user.id) + + def get_active_recruitings(self) -> ListResponse[RecruitingSummaryResponse]: + recruitings = self.recruiting_repository.get_active_recruitings() + items = [] + for recruiting in recruitings: + applicant_count = 0 + match recruiting.type: + case RecruitingType.ROOKIE: + applicant_count = ( + self.recruiting_repository.get_rookie_applicant_count( + recruiting.id + ) + ) + case RecruitingType.DESIGNER: + file_applicant_user_ids = ( + self.portfolio_file_service.get_all_applicant_user_ids() + ) + url_applicant_user_ids = ( + self.portfolio_url_service.get_all_applicant_user_ids() + ) + applicant_count = len( + set(file_applicant_user_ids + url_applicant_user_ids) + ) + case RecruitingType.PROGRAMMER: + # Not implemented + applicant_count = -1 + items.append( + RecruitingSummaryResponse( + **recruiting.__dict__, applicant_count=applicant_count + ) + ) + return ListResponse(items=items) + + def get_recruiting_infos_by_type( + self, recruiting_type: str + ) -> ListResponse[RecruitingInfoResponse] | None: + if recruiting_type not in RecruitingType.__members__: + raise InvalidRecruitTypeException(recruiting_type) + type = RecruitingType[recruiting_type] + info = self.recruiting_repository.get_recruiting_infos_by_type(type) + if not info: + return None + return ListResponse( + items=[ + RecruitingInfoResponse(**item.__dict__, type=recruiting_type) + for item in info + ] + ) + + def create_recruiting(self, request: RecruitingCreateRequest) -> RecruitingResponse: + if request.type not in RecruitingType.__members__: + raise InvalidRecruitTypeException(request.type) + + recruiting = Recruiting( + name=request.name, + type=RecruitingType[request.type], + generation=request.generation, + is_active=request.is_active, + from_date=request.from_date, + to_date=request.to_date, + short_description=request.short_description, + description=request.description, + resume_questions=[ + ResumeQuestion(**question.__dict__) + for question in request.resume_questions + ], + problems=[ + Problem( + num=problem.num, + body=problem.body, + testcases=[ + Testcase(**testcase.__dict__) for testcase in problem.testcases + ] + if problem.testcases + else None, + ) + for problem in request.problems + ] + if request.problems + else None, + ) + recruiting = self.recruiting_repository.create_recruiting(recruiting) + + return RecruitingResponse.from_orm(recruiting) + + def update_recruiting( + self, recruiting_id: int, request: RecruitingUpdateRequest + ) -> RecruitingResponse: + recruiting = self.recruiting_repository.get_recruiting_by_id(recruiting_id) + if not recruiting: + raise RecruitingNotFoundException() + + # 기본 필드들 업데이트 + for key, value in request.dict( + exclude={"resume_questions", "problems"} + ).items(): + if value is not None: + setattr(recruiting, key, value) + + # resume_questions 업데이트 + if request.resume_questions is not None: + # 이력서 질문 목록이 주어지면 기존 질문들을 모두 제거하고 새로 추가 + request.resume_questions.clear() + recruiting.resume_questions = [ + ResumeQuestion(**question.dict()) + for question in request.resume_questions + ] + + if request.problems is not None: + # 문제 목록이 주어지면 기존 문제들을 모두 제거하고 새로 추가 + request.problems.clear() + recruiting.problems = [ + Problem(**problem.dict()) for problem in request.problems + ] + + updated_recruiting = self.recruiting_repository.update_recruiting(recruiting) + + return RecruitingResponse.from_orm(updated_recruiting) + + def get_recruiting_by_id(self, recruiting_id: int) -> RecruitingResponse: + recruiting = self.recruiting_repository.get_recruiting_by_id(recruiting_id) + if recruiting is None: + raise RecruitingNotFoundException() + + return RecruitingResponse.from_orm(recruiting) + + def create_recruiting_info(self, request: RecruitingInfoCreateRequest): + recruiting = self.recruiting_repository.get_recruiting_by_id( + request.recruiting_id + ) + if not recruiting: + raise RecruitingNotFoundException() + if self.recruiting_repository.get_recruiting_info_by_info_num( + request.info_num, request.recruiting_id + ): + raise InfoAlreadyExistsException() + recruit_type = recruiting.type + recruiting_info = RecruitingInfo( + info_num=request.info_num, + title=request.title, + date_info=request.date_info, + recruiting_id=request.recruiting_id, + ) + self.recruiting_repository.create_recruiting_info(recruiting_info) + return RecruitingInfoResponse( + **recruiting_info.__dict__, type=recruit_type.name + ) + + def update_recruiting_info( + self, recruiting_info_id: int, request: RecruitingInfoUpdateRequest + ) -> RecruitingInfoResponse: + recruiting_info = self.recruiting_repository.get_recruiting_info_by_id( + recruiting_info_id + ) + if not recruiting_info: + raise RecruitingNotFoundException() + + recruiting_type = recruiting_info.recruiting.type + + for key, value in request.dict(exclude_none=True).items(): + setattr(recruiting_info, key, value) + try: + updated_recruiting_info = self.recruiting_repository.update_recruiting_info( + recruiting_info + ) + return RecruitingInfoResponse( + **updated_recruiting_info.__dict__, type=recruiting_type.name + ) + except IntegrityError as exc: + raise InfoAlreadyExistsException() from exc diff --git a/wacruit/src/apps/recruiting/views.py b/wacruit/src/apps/recruiting/views.py index b0125dad..eccf64b7 100644 --- a/wacruit/src/apps/recruiting/views.py +++ b/wacruit/src/apps/recruiting/views.py @@ -9,9 +9,9 @@ from wacruit.src.apps.recruiting.exceptions import RecruitingAlreadyAppliedException from wacruit.src.apps.recruiting.exceptions import RecruitingNotAppliedException from wacruit.src.apps.recruiting.exceptions import RecruitingNotFoundException -from wacruit.src.apps.recruiting.schemas import RecruitingResponse from wacruit.src.apps.recruiting.schemas import RecruitingResultResponse from wacruit.src.apps.recruiting.schemas import RecruitingSummaryResponse +from wacruit.src.apps.recruiting.schemas import UserRecruitingResponse from wacruit.src.apps.recruiting.services import RecruitingService from wacruit.src.apps.user.dependencies import CurrentUser @@ -32,8 +32,8 @@ def get_recruiting( user: CurrentUser, recruiting_id: int, recruiting_service: Annotated[RecruitingService, Depends()], -) -> RecruitingResponse: - return recruiting_service.get_recruiting_by_id(recruiting_id, user) +) -> UserRecruitingResponse: + return recruiting_service.get_user_recruiting_by_id(recruiting_id, user) @v1_router.get( diff --git a/wacruit/src/apps/recruiting/views_v3.py b/wacruit/src/apps/recruiting/views_v3.py new file mode 100644 index 00000000..ba8d7646 --- /dev/null +++ b/wacruit/src/apps/recruiting/views_v3.py @@ -0,0 +1,82 @@ +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends + +from wacruit.src.apps.common.enums import RecruitingType +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.recruiting.schemas import RecruitingCreateRequest +from wacruit.src.apps.recruiting.schemas import RecruitingInfoCreateRequest +from wacruit.src.apps.recruiting.schemas import RecruitingInfoResponse +from wacruit.src.apps.recruiting.schemas import RecruitingInfoUpdateRequest +from wacruit.src.apps.recruiting.schemas import RecruitingResponse +from wacruit.src.apps.recruiting.schemas import RecruitingSummaryResponse +from wacruit.src.apps.recruiting.schemas import RecruitingUpdateRequest +from wacruit.src.apps.recruiting.services import RecruitingService +from wacruit.src.apps.user.dependencies import AdminUser + +v3_router = APIRouter(prefix="/v3/recruitings", tags=["recruitings"]) + + +@v3_router.post("/") +def create_recruiting( + admin_user: AdminUser, + recruiting_service: Annotated[RecruitingService, Depends()], + request: RecruitingCreateRequest, +) -> RecruitingResponse: + return recruiting_service.create_recruiting(request) + + +@v3_router.get("/active") +def get_active_recruitings( + recruiting_service: Annotated[RecruitingService, Depends()] +) -> ListResponse[RecruitingSummaryResponse]: + return recruiting_service.get_active_recruitings() + + +@v3_router.get("/{recruiting_type}") +def get_recruitings_by_type( + recruiting_type: str, recruiting_service: Annotated[RecruitingService, Depends()] +) -> ListResponse[RecruitingInfoResponse]: + result = recruiting_service.get_recruiting_infos_by_type(recruiting_type) + if result is not None: + return result + return ListResponse(items=[]) + + +@v3_router.patch("/{recruiting_id}") +def update_recruiting( + admin_user: AdminUser, + recruiting_id: int, + recruiting_service: Annotated[RecruitingService, Depends()], + request: RecruitingUpdateRequest, +) -> RecruitingResponse: + return recruiting_service.update_recruiting(recruiting_id, request) + + +@v3_router.get("/{recruiting_id}") +def get_recruiting_by_id( + admin_user: AdminUser, + recruiting_id: int, + recruiting_service: Annotated[RecruitingService, Depends()], +) -> RecruitingResponse: + return recruiting_service.get_recruiting_by_id(recruiting_id) + + +@v3_router.post("/info") +def create_recruiting_info( + admin_user: AdminUser, + recruiting_service: Annotated[RecruitingService, Depends()], + request: RecruitingInfoCreateRequest, +) -> RecruitingInfoResponse: + return recruiting_service.create_recruiting_info(request) + + +@v3_router.patch("/info/{recruiting_info_id}") +def update_recruiting_info( + admin_user: AdminUser, + recruiting_info_id: int, + recruiting_service: Annotated[RecruitingService, Depends()], + request: RecruitingInfoUpdateRequest, +) -> RecruitingInfoResponse: + return recruiting_service.update_recruiting_info(recruiting_info_id, request) diff --git a/wacruit/src/apps/recruiting_info/exceptions.py b/wacruit/src/apps/recruiting_info/exceptions.py new file mode 100644 index 00000000..5d0f7d0a --- /dev/null +++ b/wacruit/src/apps/recruiting_info/exceptions.py @@ -0,0 +1,6 @@ +from wacruit.src.apps.common.exceptions import WacruitException + + +class InfoAlreadyExistsException(WacruitException): + def __init__(self): + super().__init__(status_code=409, detail="이미 존재하는 RecruitingInfo입니다.") diff --git a/wacruit/src/apps/recruiting_info/models.py b/wacruit/src/apps/recruiting_info/models.py index f088277b..0e306c8d 100644 --- a/wacruit/src/apps/recruiting_info/models.py +++ b/wacruit/src/apps/recruiting_info/models.py @@ -3,6 +3,7 @@ from sqlalchemy import DateTime from sqlalchemy import ForeignKey +from sqlalchemy import UniqueConstraint from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship @@ -17,6 +18,9 @@ class RecruitingInfo(DeclarativeBase): __tablename__ = "recruiting_info" + __table_args__ = ( + UniqueConstraint("recruiting_id", "info_num", name="uk_recruiting_info_num"), + ) id: Mapped[intpk] info_num: Mapped[int] diff --git a/wacruit/src/apps/resume/schemas.py b/wacruit/src/apps/resume/schemas.py index 2f58f08d..fb406830 100644 --- a/wacruit/src/apps/resume/schemas.py +++ b/wacruit/src/apps/resume/schemas.py @@ -21,6 +21,15 @@ class Config: orm_mode = True +class ResumeQuestionCreateDto(BaseModel): + question_num: int + content_limit: int + content: str + + class Config: + orm_mode = True + + class ResumeSubmissionDto(BaseModel): id: int user_id: int diff --git a/wacruit/src/apps/router.py b/wacruit/src/apps/router.py index 1a074ce9..8c463e06 100644 --- a/wacruit/src/apps/router.py +++ b/wacruit/src/apps/router.py @@ -4,6 +4,7 @@ from wacruit.src.apps import dummy from wacruit.src.apps import portfolio from wacruit.src.apps import problem +from wacruit.src.apps import project from wacruit.src.apps import recruiting from wacruit.src.apps import resume from wacruit.src.apps import user @@ -15,6 +16,8 @@ api_router.include_router(problem.v1_router) api_router.include_router(problem.v2_router) api_router.include_router(resume.router) -api_router.include_router(recruiting.router) +api_router.include_router(recruiting.v1_router) +api_router.include_router(recruiting.v3_router) api_router.include_router(portfolio.v1_router) api_router.include_router(portfolio.v2_router) +api_router.include_router(project.router) diff --git a/wacruit/src/database/migrations/versions/2025_07_06_1701-066dac40ece1_.py b/wacruit/src/database/migrations/versions/2025_07_06_1701-066dac40ece1_.py new file mode 100644 index 00000000..26ea073c --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_06_1701-066dac40ece1_.py @@ -0,0 +1,94 @@ +"""add field service_type & is_active in project table + +Revision ID: 066dac40ece1 +Revises: f1b48402a49c +Create Date: 2025-07-06 17:01:11.704940 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "066dac40ece1" +down_revision = "f1b48402a49c" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "project_member", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("project_id", sa.Integer(), nullable=False), + sa.Column("member_id", sa.Integer(), nullable=False), + sa.Column("member_name", sa.String(length=30), nullable=False), + sa.Column("position", sa.String(length=50), nullable=True), + sa.ForeignKeyConstraint( + ["member_id"], + ["member.id"], + ), + sa.ForeignKeyConstraint( + ["project_id"], + ["project.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.drop_table("project_member_association") + op.add_column( + "project", + sa.Column( + "service_type", + sa.Enum("SERVICE", "STUDY", name="projecttype"), + nullable=False, + ), + ) + op.add_column("project", sa.Column("is_active", sa.Boolean(), nullable=False)) + op.drop_column("project", "is_service") + op.add_column( + "project_url", sa.Column("title", sa.String(length=50), nullable=False) + ) + op.add_column( + "recruiting", + sa.Column( + "generation", + sa.String(length=30), + server_default=sa.text("0.0"), + nullable=False, + ), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("recruiting", "generation") + op.drop_column("project_url", "title") + op.add_column( + "project", + sa.Column( + "is_service", + mysql.TINYINT(display_width=1), + autoincrement=False, + nullable=False, + ), + ) + op.drop_column("project", "is_active") + op.drop_column("project", "service_type") + op.create_table( + "project_member_association", + sa.Column("member_id", mysql.INTEGER(), autoincrement=False, nullable=True), + sa.Column("project_id", mysql.INTEGER(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint( + ["member_id"], ["member.id"], name="project_member_association_ibfk_1" + ), + sa.ForeignKeyConstraint( + ["project_id"], ["project.id"], name="project_member_association_ibfk_2" + ), + mysql_collate="utf8mb4_0900_ai_ci", + mysql_default_charset="utf8mb4", + mysql_engine="InnoDB", + ) + op.drop_table("project_member") + # ### end Alembic commands ### diff --git a/wacruit/src/database/migrations/versions/2025_07_06_1808-f656d4e56c64_fix_typo.py b/wacruit/src/database/migrations/versions/2025_07_06_1808-f656d4e56c64_fix_typo.py new file mode 100644 index 00000000..7ebd47a6 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_06_1808-f656d4e56c64_fix_typo.py @@ -0,0 +1,40 @@ +"""fix typo + +Revision ID: f656d4e56c64 +Revises: 066dac40ece1 +Create Date: 2025-07-06 18:08:09.939208 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "f656d4e56c64" +down_revision = "066dac40ece1" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "project", + sa.Column( + "project_type", + sa.Enum("SERVICE", "STUDY", name="projecttype"), + nullable=False, + ), + ) + op.drop_column("project", "service_type") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "project", + sa.Column("service_type", mysql.ENUM("SERVICE", "STUDY"), nullable=False), + ) + op.drop_column("project", "project_type") + # ### end Alembic commands ### diff --git a/wacruit/src/database/migrations/versions/2025_07_06_2229-2a2274e82630_remove_projectmember_table_and_leader_.py b/wacruit/src/database/migrations/versions/2025_07_06_2229-2a2274e82630_remove_projectmember_table_and_leader_.py new file mode 100644 index 00000000..5e5cf61e --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_06_2229-2a2274e82630_remove_projectmember_table_and_leader_.py @@ -0,0 +1,54 @@ +"""remove ProjectMember table and leader field + +Revision ID: 2a2274e82630 +Revises: f656d4e56c64 +Create Date: 2025-07-06 22:29:11.865019 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "2a2274e82630" +down_revision = "f656d4e56c64" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("project_member") + op.add_column("member", sa.Column("position", sa.String(length=50), nullable=True)) + op.drop_constraint("project_ibfk_1", "project", type_="foreignkey") + op.drop_column("project", "leader_id") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "project", + sa.Column("leader_id", mysql.INTEGER(), autoincrement=False, nullable=False), + ) + op.create_foreign_key("project_ibfk_1", "project", "member", ["leader_id"], ["id"]) + op.drop_column("member", "position") + op.create_table( + "project_member", + sa.Column("id", mysql.INTEGER(), autoincrement=True, nullable=False), + sa.Column("project_id", mysql.INTEGER(), autoincrement=False, nullable=False), + sa.Column("member_id", mysql.INTEGER(), autoincrement=False, nullable=False), + sa.Column("member_name", mysql.VARCHAR(length=30), nullable=False), + sa.Column("position", mysql.VARCHAR(length=50), nullable=True), + sa.ForeignKeyConstraint( + ["member_id"], ["member.id"], name="project_member_ibfk_1" + ), + sa.ForeignKeyConstraint( + ["project_id"], ["project.id"], name="project_member_ibfk_2" + ), + sa.PrimaryKeyConstraint("id"), + mysql_collate="utf8mb4_0900_ai_ci", + mysql_default_charset="utf8mb4", + mysql_engine="InnoDB", + ) + # ### end Alembic commands ### diff --git a/wacruit/src/database/migrations/versions/2025_07_07_1759-452f1da1cdd4_add_unique_constraint_in_table_.py b/wacruit/src/database/migrations/versions/2025_07_07_1759-452f1da1cdd4_add_unique_constraint_in_table_.py new file mode 100644 index 00000000..45c673c5 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_07_1759-452f1da1cdd4_add_unique_constraint_in_table_.py @@ -0,0 +1,29 @@ +"""add unique constraint in table RecruitingInfo + +Revision ID: 452f1da1cdd4 +Revises: 2a2274e82630 +Create Date: 2025-07-07 17:59:08.832561 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "452f1da1cdd4" +down_revision = "2a2274e82630" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint( + "uk_recruiting_info_num", "recruiting_info", ["recruiting_id", "info_num"] + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("uk_recruiting_info_num", "recruiting_info", type_="unique") + # ### end Alembic commands ### diff --git a/wacruit/src/database/migrations/versions/2025_07_07_1815-5b0d638c64db_change_member_position_type.py b/wacruit/src/database/migrations/versions/2025_07_07_1815-5b0d638c64db_change_member_position_type.py new file mode 100644 index 00000000..e0977ae0 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_07_1815-5b0d638c64db_change_member_position_type.py @@ -0,0 +1,40 @@ +"""change member position type + +Revision ID: 5b0d638c64db +Revises: 452f1da1cdd4 +Create Date: 2025-07-07 18:15:47.298900 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "5b0d638c64db" +down_revision = "452f1da1cdd4" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "member", + "position", + existing_type=mysql.VARCHAR(length=50), + type_=sa.Enum("FRONTEND", "BACKEND", "DESIGNER", name="position"), + existing_nullable=True, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "member", + "position", + existing_type=sa.Enum("FRONTEND", "BACKEND", "DESIGNER", name="position"), + type_=mysql.VARCHAR(length=50), + existing_nullable=True, + ) + # ### end Alembic commands ### diff --git a/wacruit/src/settings.py b/wacruit/src/settings.py index a85f40d4..eac59d2f 100644 --- a/wacruit/src/settings.py +++ b/wacruit/src/settings.py @@ -8,6 +8,7 @@ class Settings(BaseSettings): env: Literal["dev", "prod", "local", "test"] = "local" + sql_echo: bool = False @property def is_dev(self) -> bool: diff --git a/wacruit/src/tests/recruiting/test_recruiting_servcie.py b/wacruit/src/tests/recruiting/test_recruiting_servcie.py index 51ea610d..c3297b35 100644 --- a/wacruit/src/tests/recruiting/test_recruiting_servcie.py +++ b/wacruit/src/tests/recruiting/test_recruiting_servcie.py @@ -19,7 +19,9 @@ def test_get_all_recruiting( def test_get_recruiting_by_id( recruiting_service: RecruitingService, recruiting: Recruiting, user: User ): - recruiting_response = recruiting_service.get_recruiting_by_id(recruiting.id, user) + recruiting_response = recruiting_service.get_user_recruiting_by_id( + recruiting.id, user + ) assert recruiting_response.name == recruiting.name assert recruiting_response.is_active == recruiting.is_active assert recruiting_response.from_date == recruiting.from_date From 705f541fbaa3b95350f4a7813d1216a337f150c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Thu, 10 Jul 2025 15:51:33 +0900 Subject: [PATCH 06/36] =?UTF-8?q?feat:=20seminar=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * seminar api를 구현했습니다. * seminar 모델에서 변경할 것들이 있어 변경 후 마이그레이션 파일 추가했습니다. --- wacruit/src/apps/common/enums.py | 8 +++ wacruit/src/apps/router.py | 2 + wacruit/src/apps/seminar/__init__.py | 1 + wacruit/src/apps/seminar/exceptions.py | 16 ++++++ wacruit/src/apps/seminar/models.py | 11 ++-- wacruit/src/apps/seminar/repositories.py | 40 +++++++++++++ wacruit/src/apps/seminar/schemas.py | 28 +++++++++ wacruit/src/apps/seminar/services.py | 57 +++++++++++++++++++ wacruit/src/apps/seminar/views.py | 56 ++++++++++++++++++ ..._09_1056-84b2b60d89a6_fix_seminar_table.py | 51 +++++++++++++++++ ...5_07_10_1211-e974b1acbdc9_fix_enum_type.py | 44 ++++++++++++++ 11 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 wacruit/src/apps/seminar/__init__.py create mode 100644 wacruit/src/apps/seminar/exceptions.py create mode 100644 wacruit/src/apps/seminar/repositories.py create mode 100644 wacruit/src/apps/seminar/schemas.py create mode 100644 wacruit/src/apps/seminar/services.py create mode 100644 wacruit/src/apps/seminar/views.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_09_1056-84b2b60d89a6_fix_seminar_table.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_10_1211-e974b1acbdc9_fix_enum_type.py diff --git a/wacruit/src/apps/common/enums.py b/wacruit/src/apps/common/enums.py index 05ebf037..5ecbb926 100644 --- a/wacruit/src/apps/common/enums.py +++ b/wacruit/src/apps/common/enums.py @@ -89,3 +89,11 @@ class Position(Enum): # INFRA = 6 # PM = 7 # OPERATIONS = 8 + + +class SeminarType(Enum): + SPRING = "SPRING" + FASTAPI = "FASTAPI" + FRONTEND = "FRONTEND" + ANDROID = "ANDROID" + IOS = "IOS" diff --git a/wacruit/src/apps/router.py b/wacruit/src/apps/router.py index 8c463e06..ebff8070 100644 --- a/wacruit/src/apps/router.py +++ b/wacruit/src/apps/router.py @@ -7,6 +7,7 @@ from wacruit.src.apps import project from wacruit.src.apps import recruiting from wacruit.src.apps import resume +from wacruit.src.apps import seminar from wacruit.src.apps import user api_router = APIRouter(prefix="/api") @@ -21,3 +22,4 @@ api_router.include_router(portfolio.v1_router) api_router.include_router(portfolio.v2_router) api_router.include_router(project.router) +api_router.include_router(seminar.router) diff --git a/wacruit/src/apps/seminar/__init__.py b/wacruit/src/apps/seminar/__init__.py new file mode 100644 index 00000000..6dc77762 --- /dev/null +++ b/wacruit/src/apps/seminar/__init__.py @@ -0,0 +1 @@ +from .views import v3_router as router diff --git a/wacruit/src/apps/seminar/exceptions.py b/wacruit/src/apps/seminar/exceptions.py new file mode 100644 index 00000000..d97a53d1 --- /dev/null +++ b/wacruit/src/apps/seminar/exceptions.py @@ -0,0 +1,16 @@ +from wacruit.src.apps.common.exceptions import WacruitException + + +class SeminarNotFoundException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="존재하지 않는 세미나입니다.") + + +class SeminarListEmptyException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="세미나 리스트가 비어있습니다.") + + +class SeminarNotActiveException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="활성화된 세미나가 존재하지 않습니다.") diff --git a/wacruit/src/apps/seminar/models.py b/wacruit/src/apps/seminar/models.py index dfcb9a65..43eae47c 100644 --- a/wacruit/src/apps/seminar/models.py +++ b/wacruit/src/apps/seminar/models.py @@ -1,14 +1,17 @@ +from sqlalchemy import Text from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from wacruit.src.apps.common.enums import SeminarType from wacruit.src.database.base import DeclarativeBase from wacruit.src.database.base import intpk -from wacruit.src.database.base import str30 -from wacruit.src.database.base import str255 class Seminar(DeclarativeBase): __tablename__ = "seminar" id: Mapped[intpk] - title: Mapped[str30] - content: Mapped[str255] + seminar_type: Mapped[SeminarType] + curriculum_info: Mapped[str] = mapped_column(Text) + prerequisite_info: Mapped[str] = mapped_column(Text) + is_active: Mapped[bool] diff --git a/wacruit/src/apps/seminar/repositories.py b/wacruit/src/apps/seminar/repositories.py new file mode 100644 index 00000000..0ca96855 --- /dev/null +++ b/wacruit/src/apps/seminar/repositories.py @@ -0,0 +1,40 @@ +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy import true +from sqlalchemy.orm import Session + +from wacruit.src.apps.seminar.models import Seminar +from wacruit.src.database.connection import get_db_session +from wacruit.src.database.connection import Transaction + + +class SeminarRepository: + def __init__( + self, + session: Session = Depends(get_db_session), + transaction: Transaction = Depends(), + ): + self.session = session + self.transaction = transaction + + def get_seminar_by_id(self, seminar_id: int) -> Seminar | None: + query = select(Seminar).where(Seminar.id == seminar_id) + return self.session.execute(query).scalar() + + def get_all_seminars(self) -> list[Seminar]: + query = select(Seminar) + return list(self.session.execute(query).scalars().all()) + + def get_active_seminars(self) -> list[Seminar]: + query = select(Seminar).where(Seminar.is_active == true()) + return list(self.session.execute(query).scalars().all()) + + def create_seminar(self, seminar: Seminar) -> Seminar: + with self.transaction: + self.session.add(seminar) + return seminar + + def update_seminar(self, seminar: Seminar) -> Seminar: + with self.transaction: + self.session.merge(seminar) + return seminar diff --git a/wacruit/src/apps/seminar/schemas.py b/wacruit/src/apps/seminar/schemas.py new file mode 100644 index 00000000..476979df --- /dev/null +++ b/wacruit/src/apps/seminar/schemas.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel +from pydantic import Field + +from wacruit.src.apps.common.enums import SeminarType + + +class CreateSeminarRequest(BaseModel): + seminar_type: SeminarType + curriculum_info: str + prerequisite_info: str + is_active: bool + + +class UpdateSeminarRequest(BaseModel): + seminar_type: SeminarType | None = None + curriculum_info: str | None = None + prerequisite_info: str | None = None + is_active: bool | None = None + + +class SeminarResponse(BaseModel): + id: int + seminar_type: SeminarType + curriculum_info: str + prerequisite_info: str + + class Config: + orm_mode = True diff --git a/wacruit/src/apps/seminar/services.py b/wacruit/src/apps/seminar/services.py new file mode 100644 index 00000000..72afa9b1 --- /dev/null +++ b/wacruit/src/apps/seminar/services.py @@ -0,0 +1,57 @@ +from typing import Annotated + +from fastapi import Depends + +from wacruit.src.apps.seminar.exceptions import SeminarListEmptyException +from wacruit.src.apps.seminar.exceptions import SeminarNotActiveException +from wacruit.src.apps.seminar.exceptions import SeminarNotFoundException +from wacruit.src.apps.seminar.models import Seminar +from wacruit.src.apps.seminar.repositories import SeminarRepository +from wacruit.src.apps.seminar.schemas import CreateSeminarRequest +from wacruit.src.apps.seminar.schemas import UpdateSeminarRequest + + +class SeminarService: + def __init__(self, seminar_repository: Annotated[SeminarRepository, Depends()]): + self.seminar_repository = seminar_repository + + def get_seminar_by_id(self, seminar_id: int) -> Seminar: + seminar = self.seminar_repository.get_seminar_by_id(seminar_id) + if seminar is None: + raise SeminarNotFoundException + return seminar + + def get_active_seminar(self) -> list[Seminar]: + seminar_list = self.seminar_repository.get_active_seminars() + if not seminar_list: + raise SeminarNotActiveException + return seminar_list + + def get_all_seminars(self) -> list[Seminar]: + seminar_list = self.seminar_repository.get_all_seminars() + if not seminar_list: + raise SeminarListEmptyException + return seminar_list + + def create_seminar(self, request: CreateSeminarRequest) -> Seminar: + seminar = Seminar( + seminar_type=request.seminar_type, + curriculum_info=request.curriculum_info, + prerequisite_info=request.prerequisite_info, + is_active=request.is_active, + ) + + created_seminar: Seminar = self.seminar_repository.create_seminar(seminar) + + return created_seminar + + def update_seminar(self, seminar_id: int, request: UpdateSeminarRequest) -> Seminar: + seminar = self.seminar_repository.get_seminar_by_id(seminar_id) + if seminar is None: + raise SeminarNotFoundException + for key, value in request.dict(exclude_none=True).items(): + setattr(seminar, key, value) + + updated_seminar = self.seminar_repository.update_seminar(seminar) + + return updated_seminar diff --git a/wacruit/src/apps/seminar/views.py b/wacruit/src/apps/seminar/views.py new file mode 100644 index 00000000..e51d1fec --- /dev/null +++ b/wacruit/src/apps/seminar/views.py @@ -0,0 +1,56 @@ +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.seminar.schemas import CreateSeminarRequest +from wacruit.src.apps.seminar.schemas import SeminarResponse +from wacruit.src.apps.seminar.schemas import UpdateSeminarRequest +from wacruit.src.apps.seminar.services import SeminarService +from wacruit.src.apps.user.dependencies import AdminUser + +v3_router = APIRouter(prefix="/v3/seminars", tags=["seminars"]) + + +@v3_router.get("/") +def get_seminars( + admin_user: AdminUser, seminar_service: Annotated[SeminarService, Depends()] +) -> ListResponse: + seminar_list = seminar_service.get_all_seminars() + + return ListResponse( + items=[SeminarResponse.from_orm(seminar) for seminar in seminar_list] + ) + + +@v3_router.post("/") +def create_seminar( + admin_user: AdminUser, + request: CreateSeminarRequest, + seminar_service: Annotated[SeminarService, Depends()], +) -> SeminarResponse: + created_seminar = seminar_service.create_seminar(request) + return SeminarResponse.from_orm(created_seminar) + + +@v3_router.patch("/{seminar_id}") +def update_seminar( + admin_user: AdminUser, + seminar_id: int, + request: UpdateSeminarRequest, + seminar_service: Annotated[SeminarService, Depends()], +) -> SeminarResponse: + updated_seminar = seminar_service.update_seminar(seminar_id, request) + return SeminarResponse.from_orm(updated_seminar) + + +@v3_router.get("/active") +def get_active_seminar( + seminar_service: Annotated[SeminarService, Depends()] +) -> ListResponse: + seminar_list = seminar_service.get_active_seminar() + + return ListResponse( + items=[SeminarResponse.from_orm(seminar) for seminar in seminar_list] + ) diff --git a/wacruit/src/database/migrations/versions/2025_07_09_1056-84b2b60d89a6_fix_seminar_table.py b/wacruit/src/database/migrations/versions/2025_07_09_1056-84b2b60d89a6_fix_seminar_table.py new file mode 100644 index 00000000..a7abda69 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_09_1056-84b2b60d89a6_fix_seminar_table.py @@ -0,0 +1,51 @@ +"""fix seminar table + +Revision ID: 84b2b60d89a6 +Revises: 5b0d638c64db +Create Date: 2025-07-09 10:56:31.259848 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "84b2b60d89a6" +down_revision = "5b0d638c64db" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "seminar", + sa.Column( + "seminar_type", + sa.Enum( + "SPRING", "FAST_API", "FRONTEND", "ANDROID", "IOS", name="seminartype" + ), + nullable=False, + ), + ) + op.add_column("seminar", sa.Column("curriculum_info", sa.Text(), nullable=False)) + op.add_column("seminar", sa.Column("prerequisite_info", sa.Text(), nullable=False)) + op.add_column("seminar", sa.Column("is_active", sa.Boolean(), nullable=False)) + op.drop_column("seminar", "title") + op.drop_column("seminar", "content") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "seminar", sa.Column("content", mysql.VARCHAR(length=255), nullable=False) + ) + op.add_column( + "seminar", sa.Column("title", mysql.VARCHAR(length=30), nullable=False) + ) + op.drop_column("seminar", "is_active") + op.drop_column("seminar", "prerequisite_info") + op.drop_column("seminar", "curriculum_info") + op.drop_column("seminar", "seminar_type") + # ### end Alembic commands ### diff --git a/wacruit/src/database/migrations/versions/2025_07_10_1211-e974b1acbdc9_fix_enum_type.py b/wacruit/src/database/migrations/versions/2025_07_10_1211-e974b1acbdc9_fix_enum_type.py new file mode 100644 index 00000000..b2d8e858 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_10_1211-e974b1acbdc9_fix_enum_type.py @@ -0,0 +1,44 @@ +"""fix: enum type + +Revision ID: e974b1acbdc9 +Revises: 84b2b60d89a6 +Create Date: 2025-07-10 12:11:29.679370 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "e974b1acbdc9" +down_revision = "84b2b60d89a6" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "seminar", + "seminar_type", + existing_type=mysql.ENUM("SPRING", "FAST_API", "FRONTEND", "ANDROID", "IOS"), + type_=sa.Enum( + "SPRING", "FASTAPI", "FRONTEND", "ANDROID", "IOS", name="seminartype" + ), + existing_nullable=False, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "seminar", + "seminar_type", + existing_type=sa.Enum( + "SPRING", "FASTAPI", "FRONTEND", "ANDROID", "IOS", name="seminartype" + ), + type_=mysql.ENUM("SPRING", "FAST_API", "FRONTEND", "ANDROID", "IOS"), + existing_nullable=False, + ) + # ### end Alembic commands ### From 360440fc13ea1135857d5c83282765dc27b92f84 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Thu, 10 Jul 2025 16:23:41 +0900 Subject: [PATCH 07/36] fix/project-api (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - enum class의 구조를 변경하였습니다 - uri 중복으로 인한 오류로 인해 uri 주소를 변경했습니다 --- wacruit/src/apps/common/enums.py | 20 ++++++++++---------- wacruit/src/apps/project/schemas.py | 8 ++++---- wacruit/src/apps/project/services.py | 3 --- wacruit/src/apps/recruiting/schemas.py | 2 +- wacruit/src/apps/recruiting/services.py | 13 ++++--------- wacruit/src/apps/recruiting/views_v3.py | 5 +++-- 6 files changed, 22 insertions(+), 29 deletions(-) diff --git a/wacruit/src/apps/common/enums.py b/wacruit/src/apps/common/enums.py index 5ecbb926..4c8611d0 100644 --- a/wacruit/src/apps/common/enums.py +++ b/wacruit/src/apps/common/enums.py @@ -64,9 +64,9 @@ class CodeSubmissionResultStatus(Enum): class RecruitingType(Enum): - ROOKIE = 1 - DESIGNER = 2 - PROGRAMMER = 3 + ROOKIE = "ROOKIE" + DESIGNER = "DESIGNER" + PROGRAMMER = "PROGRAMMER" class RecruitingApplicationStatus(Enum): @@ -76,16 +76,16 @@ class RecruitingApplicationStatus(Enum): class ProjectType(Enum): - SERVICE = 1 - STUDY = 2 + SERVICE = "SERVICE" + STUDY = "STUDY" class Position(Enum): - FRONTEND = 1 - BACKEND = 2 - DESIGNER = 3 - ANDROID = 4 - IOS = 5 + FRONTEND = "FRONTEND" + BACKEND = "BACKEND" + DESIGNER = "DESIGNER" + ANDROID = "ANDROID" + IOS = "IOS" # INFRA = 6 # PM = 7 # OPERATIONS = 8 diff --git a/wacruit/src/apps/project/schemas.py b/wacruit/src/apps/project/schemas.py index 8f44fec9..f14301f2 100644 --- a/wacruit/src/apps/project/schemas.py +++ b/wacruit/src/apps/project/schemas.py @@ -17,7 +17,7 @@ class ProjectCreateRequest(BaseModel): summary: str | None = Field(None, max_length=50) introduction: str | None = Field(None, max_length=255) thumbnail_url: str | None = Field(None, max_length=255) - project_type: str + project_type: ProjectType is_active: bool images: list[str] | None urls: list[ProjectLinkDto] | None @@ -28,7 +28,7 @@ class ProjectUpdateRequest(BaseModel): summary: str | None = Field(None, max_length=50) introduction: str | None = Field(None, max_length=255) thumbnail_url: str | None = Field(None, max_length=255) - project_type: str | None + project_type: ProjectType | None is_active: bool | None images: list[str] | None urls: list[ProjectLinkDto] | None @@ -40,7 +40,7 @@ class ProjectDetailResponse(OrmModel): summary: str | None introduction: str | None thumbnail_url: str | None - project_type: str + project_type: ProjectType is_active: bool images: list[str] | None urls: list[ProjectLinkDto] | None @@ -73,7 +73,7 @@ class ProjectBriefResponse(OrmModel): name: str summary: str | None thumbnail_url: str | None - project_type: str + project_type: ProjectType is_active: bool @classmethod diff --git a/wacruit/src/apps/project/services.py b/wacruit/src/apps/project/services.py index 0d61adf6..ced28840 100644 --- a/wacruit/src/apps/project/services.py +++ b/wacruit/src/apps/project/services.py @@ -34,9 +34,6 @@ def create_project(self, request: ProjectCreateRequest): if self.project_repository.get_project_by_name(request.name): raise ProjectAlreadyExistsException - if request.project_type not in ProjectType.__members__: - raise InvalidProjectTypeException(request.project_type) - project = Project( name=request.name, summary=request.summary, diff --git a/wacruit/src/apps/recruiting/schemas.py b/wacruit/src/apps/recruiting/schemas.py index b8a21b4a..61ece9d9 100644 --- a/wacruit/src/apps/recruiting/schemas.py +++ b/wacruit/src/apps/recruiting/schemas.py @@ -118,7 +118,7 @@ class RecruitingUpdateRequest(BaseModel): class RecruitingInfoResponse(OrmModel): id: int - type: str + type: RecruitingType info_num: int title: str date_info: datetime diff --git a/wacruit/src/apps/recruiting/services.py b/wacruit/src/apps/recruiting/services.py index 00353b3b..ad065b8b 100644 --- a/wacruit/src/apps/recruiting/services.py +++ b/wacruit/src/apps/recruiting/services.py @@ -145,12 +145,9 @@ def get_active_recruitings(self) -> ListResponse[RecruitingSummaryResponse]: return ListResponse(items=items) def get_recruiting_infos_by_type( - self, recruiting_type: str + self, recruiting_type: RecruitingType ) -> ListResponse[RecruitingInfoResponse] | None: - if recruiting_type not in RecruitingType.__members__: - raise InvalidRecruitTypeException(recruiting_type) - type = RecruitingType[recruiting_type] - info = self.recruiting_repository.get_recruiting_infos_by_type(type) + info = self.recruiting_repository.get_recruiting_infos_by_type(recruiting_type) if not info: return None return ListResponse( @@ -255,9 +252,7 @@ def create_recruiting_info(self, request: RecruitingInfoCreateRequest): recruiting_id=request.recruiting_id, ) self.recruiting_repository.create_recruiting_info(recruiting_info) - return RecruitingInfoResponse( - **recruiting_info.__dict__, type=recruit_type.name - ) + return RecruitingInfoResponse(**recruiting_info.__dict__, type=recruit_type) def update_recruiting_info( self, recruiting_info_id: int, request: RecruitingInfoUpdateRequest @@ -277,7 +272,7 @@ def update_recruiting_info( recruiting_info ) return RecruitingInfoResponse( - **updated_recruiting_info.__dict__, type=recruiting_type.name + **updated_recruiting_info.__dict__, type=recruiting_type ) except IntegrityError as exc: raise InfoAlreadyExistsException() from exc diff --git a/wacruit/src/apps/recruiting/views_v3.py b/wacruit/src/apps/recruiting/views_v3.py index ba8d7646..90e6c1cf 100644 --- a/wacruit/src/apps/recruiting/views_v3.py +++ b/wacruit/src/apps/recruiting/views_v3.py @@ -34,9 +34,10 @@ def get_active_recruitings( return recruiting_service.get_active_recruitings() -@v3_router.get("/{recruiting_type}") +@v3_router.get("/info/{recruiting_type}") def get_recruitings_by_type( - recruiting_type: str, recruiting_service: Annotated[RecruitingService, Depends()] + recruiting_type: RecruitingType, + recruiting_service: Annotated[RecruitingService, Depends()], ) -> ListResponse[RecruitingInfoResponse]: result = recruiting_service.get_recruiting_infos_by_type(recruiting_type) if result is not None: From 14a849abf42a75f0dfe183c9b5cb1506a8d6c1a4 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Thu, 10 Jul 2025 21:01:13 +0900 Subject: [PATCH 08/36] Feat/member api (#78) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - member api를 구현했습니다 - get_members 함수의 경우 헤더로 받는 sso_id가 어드민인지 아닌지에 따라 반환하는 응답이 다릅니다 - erd 그림에는 포함되어 있지만 member model에 introduction이 추가되어 있지 않아서 introduction 필드를 추가했습니다. 커밋이 약간 꼬여서 member api 구현 커밋이 두번 있게 됐습니다... --- wacruit/src/apps/common/exceptions.py | 5 ++ wacruit/src/apps/member/__init__.py | 1 + wacruit/src/apps/member/models.py | 1 + wacruit/src/apps/member/repositories.py | 21 ++++- wacruit/src/apps/member/schemas.py | 52 ++++++++++++ wacruit/src/apps/member/services.py | 79 +++++++++++++++++++ wacruit/src/apps/member/views.py | 60 ++++++++++++++ wacruit/src/apps/router.py | 2 + ..._add_introduction_field_at_member_table.py | 29 +++++++ 9 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 wacruit/src/apps/member/__init__.py create mode 100644 wacruit/src/apps/member/schemas.py create mode 100644 wacruit/src/apps/member/services.py create mode 100644 wacruit/src/apps/member/views.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_10_1641-22f4d37c8e9f_add_introduction_field_at_member_table.py diff --git a/wacruit/src/apps/common/exceptions.py b/wacruit/src/apps/common/exceptions.py index bd44b95d..09345171 100644 --- a/wacruit/src/apps/common/exceptions.py +++ b/wacruit/src/apps/common/exceptions.py @@ -46,3 +46,8 @@ def __init__(self, type: str): class InvalidProjectTypeException(WacruitException): def __init__(self, type: str): super().__init__(status_code=400, detail=f"Invalid project type: {type}") + + +class InvalidFieldFormatError(WacruitException): + def __init__(self, field: str = "Field"): + super().__init__(status_code=400, detail=f"Invalid format for {field}") diff --git a/wacruit/src/apps/member/__init__.py b/wacruit/src/apps/member/__init__.py new file mode 100644 index 00000000..6dc77762 --- /dev/null +++ b/wacruit/src/apps/member/__init__.py @@ -0,0 +1 @@ +from .views import v3_router as router diff --git a/wacruit/src/apps/member/models.py b/wacruit/src/apps/member/models.py index 77af8a48..77e668ed 100644 --- a/wacruit/src/apps/member/models.py +++ b/wacruit/src/apps/member/models.py @@ -24,6 +24,7 @@ class Member(DeclarativeBase): id: Mapped[intpk] first_name: Mapped[str30] last_name: Mapped[str30] + introduction: Mapped[str255 | None] department: Mapped[str50 | None] college: Mapped[str50 | None] phone_number: Mapped[str30 | None] diff --git a/wacruit/src/apps/member/repositories.py b/wacruit/src/apps/member/repositories.py index 62f5e589..46654a86 100644 --- a/wacruit/src/apps/member/repositories.py +++ b/wacruit/src/apps/member/repositories.py @@ -3,11 +3,30 @@ from wacruit.src.apps.member.models import Member from wacruit.src.database.connection import get_db_session +from wacruit.src.database.connection import Transaction class MemberRepository: - def __init__(self, session: Session = Depends(get_db_session)) -> None: + def __init__( + self, + session: Session = Depends(get_db_session), + transaction: Transaction = Depends(), + ) -> None: self.session = session + self.transaction = transaction def get_member_by_id(self, member_id: int) -> Member | None: return self.session.query(Member).filter(Member.id == member_id).first() + + def create_member(self, member: Member) -> Member: + with self.transaction: + self.session.add(member) + return member + + def get_all_members(self) -> list[Member]: + return self.session.query(Member).all() + + def update_member(self, member: Member) -> Member: + with self.transaction: + self.session.merge(member) + return member diff --git a/wacruit/src/apps/member/schemas.py b/wacruit/src/apps/member/schemas.py new file mode 100644 index 00000000..17bdda6c --- /dev/null +++ b/wacruit/src/apps/member/schemas.py @@ -0,0 +1,52 @@ +from pydantic import BaseModel + +from wacruit.src.apps.common.enums import Position +from wacruit.src.apps.common.schemas import OrmModel + + +class MemberCreateRequest(BaseModel): + first_name: str + last_name: str + introduction: str | None = None + department: str + college: str + phone_number: str + github_id: str + is_active: bool + generation: str + position: Position + + +class MemberInfoResponse(OrmModel): + first_name: str + last_name: str + introduction: str | None = None + department: str + college: str + phone_number: str + github_id: str + is_active: bool + generation: str + position: Position + + +class MemberBriefResponse(OrmModel): + id: int + first_name: str + last_name: str + is_active: bool + generation: str + position: Position | None = None + + +class MemberUpdateRequest(BaseModel): + first_name: str | None = None + last_name: str | None = None + introduction: str | None = None + department: str | None = None + college: str | None = None + phone_number: str | None = None + github_id: str | None = None + is_active: bool | None = None + generation: str | None = None + position: Position | None = None diff --git a/wacruit/src/apps/member/services.py b/wacruit/src/apps/member/services.py new file mode 100644 index 00000000..664951f3 --- /dev/null +++ b/wacruit/src/apps/member/services.py @@ -0,0 +1,79 @@ +from fastapi import Depends +from sqlalchemy.exc import IntegrityError + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.member.exceptions import MemberAlreadyExistsException +from wacruit.src.apps.member.exceptions import MemberNotFoundException +from wacruit.src.apps.member.models import Member +from wacruit.src.apps.member.repositories import MemberRepository +from wacruit.src.apps.member.schemas import MemberBriefResponse +from wacruit.src.apps.member.schemas import MemberCreateRequest +from wacruit.src.apps.member.schemas import MemberInfoResponse +from wacruit.src.apps.member.schemas import MemberUpdateRequest +from wacruit.src.apps.user.repositories import UserRepository + + +class MemberService: + def __init__( + self, + user_repository: UserRepository = Depends(), + member_repository: MemberRepository = Depends(), + ): + self.user_repository = user_repository + self.member_repository = member_repository + + def create_member(self, request: MemberCreateRequest): + member = Member( + first_name=request.first_name, + last_name=request.last_name, + introduction=request.introduction, + department=request.department, + college=request.college, + phone_number=request.phone_number, + github_id=request.github_id, + is_active=request.is_active, + generation=request.generation, + position=request.position, + ) + try: + member = self.member_repository.create_member(member) + except IntegrityError as exc: + raise MemberAlreadyExistsException() from exc + return MemberInfoResponse.from_orm(member) + + def get_member_by_id(self, member_id: int) -> MemberInfoResponse: + member = self.member_repository.get_member_by_id(member_id) + if member is None: + raise MemberNotFoundException + return MemberInfoResponse.from_orm(member) + + def get_members( + self, waffle_sso_id: str | None + ) -> ListResponse[MemberBriefResponse] | ListResponse[MemberInfoResponse]: + members = self.member_repository.get_all_members() + if waffle_sso_id: + user = self.user_repository.get_user_by_sso_id(waffle_sso_id) + if user and user.is_admin: + return ListResponse[MemberInfoResponse]( + items=[MemberInfoResponse.from_orm(member) for member in members] + ) + return ListResponse[MemberBriefResponse]( + items=[MemberBriefResponse.from_orm(member) for member in members] + ) + + def update_member( + self, member_id: int, request: MemberUpdateRequest + ) -> MemberInfoResponse: + member = self.member_repository.get_member_by_id(member_id) + if member is None: + raise MemberNotFoundException + + for field, value in request.dict(exclude_unset=True).items(): + setattr(member, field, value) + + try: + member = self.member_repository.update_member(member) + except IntegrityError as exc: + raise MemberAlreadyExistsException() from exc + + return MemberInfoResponse.from_orm(member) diff --git a/wacruit/src/apps/member/views.py b/wacruit/src/apps/member/views.py new file mode 100644 index 00000000..4ef65a24 --- /dev/null +++ b/wacruit/src/apps/member/views.py @@ -0,0 +1,60 @@ +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends +from fastapi import Security +from fastapi.security import APIKeyHeader + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.member.schemas import MemberBriefResponse +from wacruit.src.apps.member.schemas import MemberCreateRequest +from wacruit.src.apps.member.schemas import MemberInfoResponse +from wacruit.src.apps.member.schemas import MemberUpdateRequest +from wacruit.src.apps.member.services import MemberService +from wacruit.src.apps.user.dependencies import AdminUser +from wacruit.src.apps.user.dependencies import CurrentUser + +v3_router = APIRouter(prefix="/v3/members", tags=["members"]) + + +@v3_router.post("") +def create_member( + admin_user: AdminUser, + request: MemberCreateRequest, + member_service: Annotated[MemberService, Depends()], +): + return member_service.create_member(request) + + +@v3_router.get("/{member_id}") +def get_member( + admin_user: AdminUser, + member_id: int, + member_service: Annotated[MemberService, Depends()], +) -> MemberInfoResponse: + return member_service.get_member_by_id(member_id) + + +@v3_router.get("") +def get_members( + waffle_sso_id: Annotated[ + str | None, + Security( + APIKeyHeader( + name="waffle-user-id", scheme_name="waffle-user-id", auto_error=False + ) + ), + ], + member_service: Annotated[MemberService, Depends()], +) -> ListResponse[MemberInfoResponse] | ListResponse[MemberBriefResponse]: + return member_service.get_members(waffle_sso_id) + + +@v3_router.patch("/{member_id}") +def update_member( + admin_user: AdminUser, + member_id: int, + request: MemberUpdateRequest, + member_service: Annotated[MemberService, Depends()], +) -> MemberInfoResponse: + return member_service.update_member(member_id, request) diff --git a/wacruit/src/apps/router.py b/wacruit/src/apps/router.py index ebff8070..cc104b82 100644 --- a/wacruit/src/apps/router.py +++ b/wacruit/src/apps/router.py @@ -2,6 +2,7 @@ from wacruit.src.apps import announcement from wacruit.src.apps import dummy +from wacruit.src.apps import member from wacruit.src.apps import portfolio from wacruit.src.apps import problem from wacruit.src.apps import project @@ -14,6 +15,7 @@ api_router.include_router(dummy.router) api_router.include_router(announcement.router) api_router.include_router(user.router) +api_router.include_router(member.router) api_router.include_router(problem.v1_router) api_router.include_router(problem.v2_router) api_router.include_router(resume.router) diff --git a/wacruit/src/database/migrations/versions/2025_07_10_1641-22f4d37c8e9f_add_introduction_field_at_member_table.py b/wacruit/src/database/migrations/versions/2025_07_10_1641-22f4d37c8e9f_add_introduction_field_at_member_table.py new file mode 100644 index 00000000..e9ceb8fb --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_10_1641-22f4d37c8e9f_add_introduction_field_at_member_table.py @@ -0,0 +1,29 @@ +"""add introduction field at member table + +Revision ID: 22f4d37c8e9f +Revises: e974b1acbdc9 +Create Date: 2025-07-10 16:41:08.160574 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "22f4d37c8e9f" +down_revision = "e974b1acbdc9" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "member", sa.Column("introduction", sa.String(length=255), nullable=True) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("member", "introduction") + # ### end Alembic commands ### From 889583b2f044f9da35e3febcba1800e9a1c2f4cd Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Sun, 13 Jul 2025 13:12:01 +0900 Subject: [PATCH 09/36] Feat/review api (#79) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리뷰 api를 구현했습니다 --- wacruit/src/apps/review/__init__.py | 1 + wacruit/src/apps/review/exceptions.py | 6 ++ wacruit/src/apps/review/repositories.py | 36 ++++++++ wacruit/src/apps/review/schemas.py | 26 ++++++ wacruit/src/apps/review/services.py | 115 ++++++++++++++++++++++++ wacruit/src/apps/review/views.py | 58 ++++++++++++ wacruit/src/apps/router.py | 2 + 7 files changed, 244 insertions(+) create mode 100644 wacruit/src/apps/review/__init__.py create mode 100644 wacruit/src/apps/review/exceptions.py create mode 100644 wacruit/src/apps/review/repositories.py create mode 100644 wacruit/src/apps/review/schemas.py create mode 100644 wacruit/src/apps/review/services.py create mode 100644 wacruit/src/apps/review/views.py diff --git a/wacruit/src/apps/review/__init__.py b/wacruit/src/apps/review/__init__.py new file mode 100644 index 00000000..6dc77762 --- /dev/null +++ b/wacruit/src/apps/review/__init__.py @@ -0,0 +1 @@ +from .views import v3_router as router diff --git a/wacruit/src/apps/review/exceptions.py b/wacruit/src/apps/review/exceptions.py new file mode 100644 index 00000000..94c9e829 --- /dev/null +++ b/wacruit/src/apps/review/exceptions.py @@ -0,0 +1,6 @@ +from wacruit.src.apps.common.exceptions import WacruitException + + +class ReviewNotFoundException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="존재하지 않는 리뷰입니다.") diff --git a/wacruit/src/apps/review/repositories.py b/wacruit/src/apps/review/repositories.py new file mode 100644 index 00000000..b2b06bcb --- /dev/null +++ b/wacruit/src/apps/review/repositories.py @@ -0,0 +1,36 @@ +from fastapi import Depends +from sqlalchemy.orm import Session + +from wacruit.src.apps.review.models import Review +from wacruit.src.database.connection import get_db_session +from wacruit.src.database.connection import Transaction + + +class ReviewRepository: + def __init__( + self, + session: Session = Depends(get_db_session), + transaction: Transaction = Depends(), + ): + self.session = session + self.transaction = transaction + + def create_review(self, review: Review) -> Review: + with self.transaction: + self.session.add(review) + return review + + def get_review(self, review_id: int) -> Review | None: + return self.session.query(Review).filter(Review.id == review_id).first() + + def get_reviews(self, offset: int = 0, limit: int = 20) -> list[Review]: + return self.session.query(Review).offset(offset).limit(limit).all() + + def update_review(self, updated_review: Review) -> Review: + with self.transaction: + self.session.merge(updated_review) + return updated_review + + def delete_review(self, review: Review): + with self.transaction: + self.session.delete(review) diff --git a/wacruit/src/apps/review/schemas.py b/wacruit/src/apps/review/schemas.py new file mode 100644 index 00000000..0bf4e755 --- /dev/null +++ b/wacruit/src/apps/review/schemas.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel + +from wacruit.src.apps.common.enums import Position + + +class ReviewCreateRequest(BaseModel): + title: str + content: str + member_id: int + + +class ReviewResponse(BaseModel): + id: int + title: str + content: str + member_id: int + member_first_name: str + member_last_name: str + member_position: Position | None + is_active: bool + + +class ReviewUpdateRequest(BaseModel): + title: str | None = None + content: str | None = None + member_id: int | None = None diff --git a/wacruit/src/apps/review/services.py b/wacruit/src/apps/review/services.py new file mode 100644 index 00000000..3b8c574e --- /dev/null +++ b/wacruit/src/apps/review/services.py @@ -0,0 +1,115 @@ +from typing import Annotated + +from fastapi import Depends + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.member.exceptions import MemberNotFoundException +from wacruit.src.apps.member.repositories import MemberRepository +from wacruit.src.apps.review.exceptions import ReviewNotFoundException +from wacruit.src.apps.review.models import Review +from wacruit.src.apps.review.repositories import ReviewRepository +from wacruit.src.apps.review.schemas import ReviewCreateRequest +from wacruit.src.apps.review.schemas import ReviewResponse +from wacruit.src.apps.review.schemas import ReviewUpdateRequest + + +class ReviewService: + def __init__( + self, + member_repository: Annotated[MemberRepository, Depends()], + review_repository: Annotated[ReviewRepository, Depends()], + ): + self.member_repository = member_repository + self.review_repository = review_repository + + def create_review(self, request: ReviewCreateRequest): + member = self.member_repository.get_member_by_id(request.member_id) + if not member: + raise MemberNotFoundException + review = Review( + title=request.title, content=request.content, member_id=member.id + ) + return self.review_repository.create_review(review) + + def get_review(self, review_id: int): + review = self.review_repository.get_review(review_id) + if not review: + raise ReviewNotFoundException + writer = review.member + + return ReviewResponse( + id=review.id, + title=review.title, + content=review.content, + member_id=review.member_id, + member_first_name=writer.first_name, + member_last_name=writer.last_name, + member_position=writer.position if writer.position else None, + is_active=writer.is_active, + ) + + def get_reviews( + self, offset: int = 0, limit: int = 20 + ) -> ListResponse[ReviewResponse]: + reviews = self.review_repository.get_reviews(offset=offset, limit=limit) + + return ListResponse( + items=[ + ReviewResponse( + id=review.id, + title=review.title, + content=review.content, + member_id=review.member_id, + member_first_name=review.member.first_name, + member_last_name=review.member.last_name, + member_position=review.member.position + if review.member.position + else None, + is_active=review.member.is_active, + ) + for review in reviews + ] + ) + + def update_review( + self, review_id: int, request: ReviewUpdateRequest + ) -> ReviewResponse: + review = self.review_repository.get_review(review_id) + if not review: + raise ReviewNotFoundException + + if request.member_id: + member_to_update = self.member_repository.get_member_by_id( + request.member_id + ) + if not member_to_update: + raise MemberNotFoundException + review.member_id = member_to_update.id + + if request.title: + review.title = request.title + + if request.content: + review.content = request.content + + updated_review = self.review_repository.update_review(review) + + return ReviewResponse( + id=updated_review.id, + title=updated_review.title, + content=updated_review.content, + member_id=updated_review.member_id, + member_first_name=updated_review.member.first_name, + member_last_name=updated_review.member.last_name, + member_position=updated_review.member.position + if updated_review.member.position + else None, + is_active=updated_review.member.is_active, + ) + + def delete_review(self, review_id: int): + review = self.review_repository.get_review(review_id) + if not review: + raise ReviewNotFoundException + + self.review_repository.delete_review(review) diff --git a/wacruit/src/apps/review/views.py b/wacruit/src/apps/review/views.py new file mode 100644 index 00000000..123ee6cb --- /dev/null +++ b/wacruit/src/apps/review/views.py @@ -0,0 +1,58 @@ +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.review.schemas import ReviewCreateRequest +from wacruit.src.apps.review.schemas import ReviewResponse +from wacruit.src.apps.review.schemas import ReviewUpdateRequest +from wacruit.src.apps.review.services import ReviewService +from wacruit.src.apps.user.dependencies import AdminUser + +v3_router = APIRouter(prefix="/v3/reviews", tags=["reviews"]) + + +@v3_router.post("") +def create_review( + admin_user: AdminUser, + request: ReviewCreateRequest, + review_service: Annotated[ReviewService, Depends()], +): + return review_service.create_review(request) + + +@v3_router.get("/{review_id}") +def get_review( + review_id: int, + review_service: Annotated[ReviewService, Depends()], +) -> ReviewResponse: + return review_service.get_review(review_id) + + +@v3_router.get("") +def get_reviews( + review_service: Annotated[ReviewService, Depends()], + offset: int = 0, + limit: int = 20, +) -> ListResponse[ReviewResponse]: + return review_service.get_reviews(offset=offset, limit=limit) + + +@v3_router.patch("/{review_id}") +def update_review( + admin_user: AdminUser, + review_id: int, + request: ReviewUpdateRequest, + review_service: Annotated[ReviewService, Depends()], +) -> ReviewResponse: + return review_service.update_review(review_id, request) + + +@v3_router.delete("/{review_id}") +def delete_review( + admin_user: AdminUser, + review_id: int, + review_service: Annotated[ReviewService, Depends()], +): + return review_service.delete_review(review_id) diff --git a/wacruit/src/apps/router.py b/wacruit/src/apps/router.py index cc104b82..2df0837e 100644 --- a/wacruit/src/apps/router.py +++ b/wacruit/src/apps/router.py @@ -8,6 +8,7 @@ from wacruit.src.apps import project from wacruit.src.apps import recruiting from wacruit.src.apps import resume +from wacruit.src.apps import review from wacruit.src.apps import seminar from wacruit.src.apps import user @@ -25,3 +26,4 @@ api_router.include_router(portfolio.v2_router) api_router.include_router(project.router) api_router.include_router(seminar.router) +api_router.include_router(review.router) From 79175943618391e11463e7b8c5b44be573774678 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Sun, 13 Jul 2025 14:15:53 +0900 Subject: [PATCH 10/36] Feat/review api (#81) --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6cd687f5..f1d6ff16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-buster as builder +FROM python:3.11-bookworm as builder RUN pip install poetry==1.5.0 @@ -13,7 +13,7 @@ COPY pyproject.toml poetry.lock ./ RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --without dev --no-root -FROM python:3.11-slim-buster as runtime +FROM python:3.11-slim-bookworm as runtime ARG ENV From 71ac91d74a9d75a645f6e2d9b0bf4dfc1492f17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Sun, 13 Jul 2025 14:16:19 +0900 Subject: [PATCH 11/36] =?UTF-8?q?feat:=20history=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * History 관련 api를 구현했습니다. * History table을 수정하고 마이그레이션 파일을 추가했습니다. --- wacruit/src/apps/history/__init__.py | 1 + wacruit/src/apps/history/exceptions.py | 6 ++ wacruit/src/apps/history/models.py | 8 +-- wacruit/src/apps/history/repositories.py | 46 +++++++++++++ wacruit/src/apps/history/schemas.py | 10 +++ wacruit/src/apps/history/services.py | 45 +++++++++++++ wacruit/src/apps/history/views.py | 35 ++++++++++ wacruit/src/apps/router.py | 2 + ..._12_2306-d7157419824c_fix_history_table.py | 64 +++++++++++++++++++ 9 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 wacruit/src/apps/history/__init__.py create mode 100644 wacruit/src/apps/history/exceptions.py create mode 100644 wacruit/src/apps/history/repositories.py create mode 100644 wacruit/src/apps/history/schemas.py create mode 100644 wacruit/src/apps/history/services.py create mode 100644 wacruit/src/apps/history/views.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_12_2306-d7157419824c_fix_history_table.py diff --git a/wacruit/src/apps/history/__init__.py b/wacruit/src/apps/history/__init__.py new file mode 100644 index 00000000..6dc77762 --- /dev/null +++ b/wacruit/src/apps/history/__init__.py @@ -0,0 +1 @@ +from .views import v3_router as router diff --git a/wacruit/src/apps/history/exceptions.py b/wacruit/src/apps/history/exceptions.py new file mode 100644 index 00000000..27113d20 --- /dev/null +++ b/wacruit/src/apps/history/exceptions.py @@ -0,0 +1,6 @@ +from wacruit.src.apps.common.exceptions import WacruitException + + +class HistoryEmptyException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="History 테이블이 비어 있습니다.") diff --git a/wacruit/src/apps/history/models.py b/wacruit/src/apps/history/models.py index ba17e39c..1bf9e83e 100644 --- a/wacruit/src/apps/history/models.py +++ b/wacruit/src/apps/history/models.py @@ -1,15 +1,13 @@ -from datetime import datetime - from sqlalchemy.orm import Mapped from wacruit.src.database.base import DeclarativeBase from wacruit.src.database.base import intpk +from wacruit.src.database.base import str50 class History(DeclarativeBase): __tablename__ = "histroy" id: Mapped[intpk] - total_projects: Mapped[int] - total_users: Mapped[int] - total_members: Mapped[int] + history_key: Mapped[str50] + history_value: Mapped[str50] diff --git a/wacruit/src/apps/history/repositories.py b/wacruit/src/apps/history/repositories.py new file mode 100644 index 00000000..e18b4008 --- /dev/null +++ b/wacruit/src/apps/history/repositories.py @@ -0,0 +1,46 @@ +from datetime import datetime + +from fastapi import Depends +from sqlalchemy import select +from sqlalchemy.orm import Session + +from wacruit.src.apps.history.models import History +from wacruit.src.database.connection import get_db_session +from wacruit.src.database.connection import Transaction + + +class HistoryRepository: + def __init__( + self, + session: Session = Depends(get_db_session), + transaction: Transaction = Depends(), + ): + self.session = session + self.transaction = transaction + + def get_history(self) -> list[History]: + query = select(History) + return list(self.session.execute(query).scalars().all()) + + def update_history(self, history: History) -> History: + history_key = history.history_key + history_value = history.history_value + + with self.transaction: + query = select(History).where(History.history_key == history_key) + result = self.session.execute(query).scalar_one_or_none() + if result: + result.history_value = history_value + else: + result = History(history_key=history_key, history_value=history_value) + self.session.add(result) + + return history + + def get_start_date(self) -> datetime | None: + query = select(History).where(History.history_key == "start_date") + result = self.session.execute(query).scalar_one_or_none() + + if result is None: + return None + return datetime.strptime(result.history_value, "%Y-%m-%d") diff --git a/wacruit/src/apps/history/schemas.py b/wacruit/src/apps/history/schemas.py new file mode 100644 index 00000000..f829fc94 --- /dev/null +++ b/wacruit/src/apps/history/schemas.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class UpdateHistoryRequest(BaseModel): + class Config: + extra = "allow" + + +class HistoryResponse(BaseModel): + __root__: dict[str, str] diff --git a/wacruit/src/apps/history/services.py b/wacruit/src/apps/history/services.py new file mode 100644 index 00000000..c81b4d45 --- /dev/null +++ b/wacruit/src/apps/history/services.py @@ -0,0 +1,45 @@ +from datetime import datetime + +from fastapi import Depends + +from wacruit.src.apps.history.exceptions import HistoryEmptyException +from wacruit.src.apps.history.models import History +from wacruit.src.apps.history.repositories import HistoryRepository +from wacruit.src.apps.history.schemas import UpdateHistoryRequest + + +class HistoryService: + def __init__(self, history_repository: HistoryRepository = Depends()): + self.history_repository = history_repository + + def get_history(self) -> list[History]: + history_list = self.history_repository.get_history() + operation_period = self.calculate_operation_period() + + if operation_period is not None: + history_list.append( + History( + history_key="operation_period", history_value=str(operation_period) + ) + ) + + if not history_list: + raise HistoryEmptyException + return history_list + + def update_history(self, update_request: UpdateHistoryRequest): + req = update_request.dict() + + for k, v in req.items(): + to_update = History(history_key=k, history_value=v) + self.history_repository.update_history(to_update) + + return self.get_history() + + def calculate_operation_period(self) -> int | None: + start_date = self.history_repository.get_start_date() + now = datetime.now() + + if start_date is None: + return None + return now.year - start_date.year diff --git a/wacruit/src/apps/history/views.py b/wacruit/src/apps/history/views.py new file mode 100644 index 00000000..e5f0b762 --- /dev/null +++ b/wacruit/src/apps/history/views.py @@ -0,0 +1,35 @@ +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends + +from wacruit.src.apps.history.schemas import HistoryResponse +from wacruit.src.apps.history.schemas import UpdateHistoryRequest +from wacruit.src.apps.history.services import HistoryService +from wacruit.src.apps.user.dependencies import AdminUser + +v3_router = APIRouter(prefix="/v3/history", tags=["history"]) + + +@v3_router.patch("/") +def update_history( + admin_user: AdminUser, + history_service: Annotated[HistoryService, Depends()], + update_request: UpdateHistoryRequest, +): + updated_history = history_service.update_history(update_request) + response_dict = {} + for h in updated_history: + response_dict[h.history_key] = h.history_value + + return HistoryResponse(__root__=response_dict) + + +@v3_router.get("/") +def get_history(history_service: Annotated[HistoryService, Depends()]): + history_list = history_service.get_history() + response_dict = {} + for h in history_list: + response_dict[h.history_key] = h.history_value + + return HistoryResponse(__root__=response_dict) diff --git a/wacruit/src/apps/router.py b/wacruit/src/apps/router.py index 2df0837e..c9e09dd8 100644 --- a/wacruit/src/apps/router.py +++ b/wacruit/src/apps/router.py @@ -2,6 +2,7 @@ from wacruit.src.apps import announcement from wacruit.src.apps import dummy +from wacruit.src.apps import history from wacruit.src.apps import member from wacruit.src.apps import portfolio from wacruit.src.apps import problem @@ -26,4 +27,5 @@ api_router.include_router(portfolio.v2_router) api_router.include_router(project.router) api_router.include_router(seminar.router) +api_router.include_router(history.router) api_router.include_router(review.router) diff --git a/wacruit/src/database/migrations/versions/2025_07_12_2306-d7157419824c_fix_history_table.py b/wacruit/src/database/migrations/versions/2025_07_12_2306-d7157419824c_fix_history_table.py new file mode 100644 index 00000000..00c6b431 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_12_2306-d7157419824c_fix_history_table.py @@ -0,0 +1,64 @@ +"""fix history table + +Revision ID: d7157419824c +Revises: 22f4d37c8e9f +Create Date: 2025-07-12 23:06:01.005810 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "d7157419824c" +down_revision = "22f4d37c8e9f" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "histroy", sa.Column("history_key", sa.String(length=50), nullable=False) + ) + op.add_column( + "histroy", sa.Column("history_value", sa.String(length=50), nullable=False) + ) + op.drop_column("histroy", "total_projects") + op.drop_column("histroy", "total_users") + op.drop_column("histroy", "total_members") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "histroy", + sa.Column( + "total_members", + mysql.INTEGER(display_width=11), + autoincrement=False, + nullable=False, + ), + ) + op.add_column( + "histroy", + sa.Column( + "total_users", + mysql.INTEGER(display_width=11), + autoincrement=False, + nullable=False, + ), + ) + op.add_column( + "histroy", + sa.Column( + "total_projects", + mysql.INTEGER(display_width=11), + autoincrement=False, + nullable=False, + ), + ) + op.drop_column("histroy", "history_value") + op.drop_column("histroy", "history_key") + # ### end Alembic commands ### From 03b2b704f7b6ed590abda70052e61744a8cab279 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Mon, 14 Jul 2025 22:26:39 +0900 Subject: [PATCH 12/36] =?UTF-8?q?URI=20=EC=88=98=EC=A0=95=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wacruit/src/apps/history/views.py | 4 ++-- wacruit/src/apps/project/views.py | 4 ++-- wacruit/src/apps/recruiting/views_v3.py | 2 +- wacruit/src/apps/seminar/views.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/wacruit/src/apps/history/views.py b/wacruit/src/apps/history/views.py index e5f0b762..4f00392d 100644 --- a/wacruit/src/apps/history/views.py +++ b/wacruit/src/apps/history/views.py @@ -11,7 +11,7 @@ v3_router = APIRouter(prefix="/v3/history", tags=["history"]) -@v3_router.patch("/") +@v3_router.patch("") def update_history( admin_user: AdminUser, history_service: Annotated[HistoryService, Depends()], @@ -25,7 +25,7 @@ def update_history( return HistoryResponse(__root__=response_dict) -@v3_router.get("/") +@v3_router.get("") def get_history(history_service: Annotated[HistoryService, Depends()]): history_list = history_service.get_history() response_dict = {} diff --git a/wacruit/src/apps/project/views.py b/wacruit/src/apps/project/views.py index 8ffb7ad8..4111ba84 100644 --- a/wacruit/src/apps/project/views.py +++ b/wacruit/src/apps/project/views.py @@ -14,7 +14,7 @@ v3_router = APIRouter(prefix="/v3/projects", tags=["projects"]) -@v3_router.post("/") +@v3_router.post("") def create_project( request: ProjectCreateRequest, project_service: Annotated[ProjectService, Depends()], @@ -30,7 +30,7 @@ def get_project( return project_service.get_project(project_id) -@v3_router.get("/") +@v3_router.get("") def list_projects( project_service: Annotated[ProjectService, Depends()] ) -> ListResponse[ProjectBriefResponse]: diff --git a/wacruit/src/apps/recruiting/views_v3.py b/wacruit/src/apps/recruiting/views_v3.py index 90e6c1cf..733f6a53 100644 --- a/wacruit/src/apps/recruiting/views_v3.py +++ b/wacruit/src/apps/recruiting/views_v3.py @@ -18,7 +18,7 @@ v3_router = APIRouter(prefix="/v3/recruitings", tags=["recruitings"]) -@v3_router.post("/") +@v3_router.post("") def create_recruiting( admin_user: AdminUser, recruiting_service: Annotated[RecruitingService, Depends()], diff --git a/wacruit/src/apps/seminar/views.py b/wacruit/src/apps/seminar/views.py index e51d1fec..e660e721 100644 --- a/wacruit/src/apps/seminar/views.py +++ b/wacruit/src/apps/seminar/views.py @@ -13,7 +13,7 @@ v3_router = APIRouter(prefix="/v3/seminars", tags=["seminars"]) -@v3_router.get("/") +@v3_router.get("") def get_seminars( admin_user: AdminUser, seminar_service: Annotated[SeminarService, Depends()] ) -> ListResponse: @@ -24,7 +24,7 @@ def get_seminars( ) -@v3_router.post("/") +@v3_router.post("") def create_seminar( admin_user: AdminUser, request: CreateSeminarRequest, From 9fea1890927bc27304079b9a5961fb9d2367c388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Tue, 15 Jul 2025 18:12:02 +0900 Subject: [PATCH 13/36] fix: project url enum type (#83) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * project url type을 enum으로 수정 후 마이그레이션 파일을 추가했습니다. --- wacruit/src/apps/common/enums.py | 12 ++++ wacruit/src/apps/history/models.py | 2 +- wacruit/src/apps/project/models.py | 3 +- wacruit/src/apps/project/schemas.py | 7 +- wacruit/src/apps/project/services.py | 4 +- ..._0058-8964bb6498ca_fix_project_url_type.py | 70 +++++++++++++++++++ 6 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 wacruit/src/database/migrations/versions/2025_07_15_0058-8964bb6498ca_fix_project_url_type.py diff --git a/wacruit/src/apps/common/enums.py b/wacruit/src/apps/common/enums.py index 4c8611d0..aecf2734 100644 --- a/wacruit/src/apps/common/enums.py +++ b/wacruit/src/apps/common/enums.py @@ -97,3 +97,15 @@ class SeminarType(Enum): FRONTEND = "FRONTEND" ANDROID = "ANDROID" IOS = "IOS" + + +class ProjectURLType(Enum): + ANDROID_STORE = "ANDROID_STORE" + IOS_STORE = "IOS_STORE" + FRONTEND_GITHUB = "FRONTEND_GITHUB" + BACKEND_GITHUB = "BACKEND_GITHUB" + ANDROID_GITHUB = "ANDROID_GITHUB" + WEBSITE = "WEBSITE" + IOS_GITHUB = "IOS_GITHUB" + CRAWLER_GITHUB = "CRAWLER_GITHUB" + BEHANCE = "BEHANCE" diff --git a/wacruit/src/apps/history/models.py b/wacruit/src/apps/history/models.py index 1bf9e83e..4176e684 100644 --- a/wacruit/src/apps/history/models.py +++ b/wacruit/src/apps/history/models.py @@ -6,7 +6,7 @@ class History(DeclarativeBase): - __tablename__ = "histroy" + __tablename__ = "history" id: Mapped[intpk] history_key: Mapped[str50] diff --git a/wacruit/src/apps/project/models.py b/wacruit/src/apps/project/models.py index 46b00cfa..c1ce764c 100644 --- a/wacruit/src/apps/project/models.py +++ b/wacruit/src/apps/project/models.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import relationship from wacruit.src.apps.common.enums import ProjectType +from wacruit.src.apps.common.enums import ProjectURLType from wacruit.src.database.base import DeclarativeBase from wacruit.src.database.base import intpk from wacruit.src.database.base import str30 @@ -41,7 +42,7 @@ class ProjectURL(DeclarativeBase): id: Mapped[intpk] project_id: Mapped[int] = mapped_column(ForeignKey("project.id")) - title: Mapped[str50] + url_type: Mapped[ProjectURLType] url: Mapped[str255] source_project: Mapped["Project"] = relationship(back_populates="urls") diff --git a/wacruit/src/apps/project/schemas.py b/wacruit/src/apps/project/schemas.py index f14301f2..6395218a 100644 --- a/wacruit/src/apps/project/schemas.py +++ b/wacruit/src/apps/project/schemas.py @@ -4,11 +4,12 @@ from pydantic import Field from wacruit.src.apps.common.enums import ProjectType +from wacruit.src.apps.common.enums import ProjectURLType from wacruit.src.apps.common.schemas import OrmModel class ProjectLinkDto(BaseModel): - title: str + url_type: ProjectURLType url: str @@ -49,7 +50,9 @@ class ProjectDetailResponse(OrmModel): def from_orm(cls, obj): urls = None if obj.urls: - urls = [ProjectLinkDto(title=url.title, url=url.url) for url in obj.urls] + urls = [ + ProjectLinkDto(url_type=url.url_type, url=url.url) for url in obj.urls + ] images = None if obj.image_urls: diff --git a/wacruit/src/apps/project/services.py b/wacruit/src/apps/project/services.py index ced28840..bc291c41 100644 --- a/wacruit/src/apps/project/services.py +++ b/wacruit/src/apps/project/services.py @@ -61,7 +61,9 @@ def create_project(self, request: ProjectCreateRequest): created_project.urls = [] for url_dto in request.urls: url_obj = ProjectURL( - project_id=created_project.id, title=url_dto.title, url=url_dto.url + project_id=created_project.id, + url_type=url_dto.url_type, + url=url_dto.url, ) created_project.urls.append(url_obj) diff --git a/wacruit/src/database/migrations/versions/2025_07_15_0058-8964bb6498ca_fix_project_url_type.py b/wacruit/src/database/migrations/versions/2025_07_15_0058-8964bb6498ca_fix_project_url_type.py new file mode 100644 index 00000000..30898643 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_15_0058-8964bb6498ca_fix_project_url_type.py @@ -0,0 +1,70 @@ +"""fix project url type + +Revision ID: 8964bb6498ca +Revises: d7157419824c +Create Date: 2025-07-15 00:58:52.147111 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "8964bb6498ca" +down_revision = "d7157419824c" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "history", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("history_key", sa.String(length=50), nullable=False), + sa.Column("history_value", sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.drop_table("histroy") + op.add_column( + "project_url", + sa.Column( + "url_type", + sa.Enum( + "ANDROID_STORE", + "IOS_STORE", + "FRONTEND_GITHUB", + "BACKEND_GITHUB", + "ANDROID_GITHUB", + "WEBSITE", + "IOS_GITHUB", + "CRAWLER_GITHUB", + "BEHANCE", + name="projecturltype", + ), + nullable=False, + ), + ) + op.drop_column("project_url", "title") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "project_url", sa.Column("title", mysql.VARCHAR(length=50), nullable=False) + ) + op.drop_column("project_url", "url_type") + op.create_table( + "histroy", + sa.Column( + "id", mysql.INTEGER(display_width=11), autoincrement=True, nullable=False + ), + sa.Column("history_key", mysql.VARCHAR(length=50), nullable=False), + sa.Column("history_value", mysql.VARCHAR(length=50), nullable=False), + sa.PrimaryKeyConstraint("id"), + mysql_default_charset="latin1", + mysql_engine="InnoDB", + ) + op.drop_table("history") + # ### end Alembic commands ### From 7b8069676e6bf47a98337d3cd37e5965ef55a57a Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Thu, 17 Jul 2025 00:45:15 +0900 Subject: [PATCH 14/36] =?UTF-8?q?sponsor=20api=20=EA=B5=AC=ED=98=84=20(#84?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wacruit/src/apps/router.py | 2 + wacruit/src/apps/sponsor/__init__.py | 1 + wacruit/src/apps/sponsor/exceptions.py | 11 +++++ wacruit/src/apps/sponsor/repositories.py | 35 ++++++++++++++ wacruit/src/apps/sponsor/schemas.py | 35 ++++++++++++++ wacruit/src/apps/sponsor/services.py | 60 ++++++++++++++++++++++++ wacruit/src/apps/sponsor/views.py | 50 ++++++++++++++++++++ 7 files changed, 194 insertions(+) create mode 100644 wacruit/src/apps/sponsor/__init__.py create mode 100644 wacruit/src/apps/sponsor/exceptions.py create mode 100644 wacruit/src/apps/sponsor/repositories.py create mode 100644 wacruit/src/apps/sponsor/schemas.py create mode 100644 wacruit/src/apps/sponsor/services.py create mode 100644 wacruit/src/apps/sponsor/views.py diff --git a/wacruit/src/apps/router.py b/wacruit/src/apps/router.py index c9e09dd8..5bffd3d4 100644 --- a/wacruit/src/apps/router.py +++ b/wacruit/src/apps/router.py @@ -11,12 +11,14 @@ from wacruit.src.apps import resume from wacruit.src.apps import review from wacruit.src.apps import seminar +from wacruit.src.apps import sponsor from wacruit.src.apps import user api_router = APIRouter(prefix="/api") api_router.include_router(dummy.router) api_router.include_router(announcement.router) api_router.include_router(user.router) +api_router.include_router(sponsor.router) api_router.include_router(member.router) api_router.include_router(problem.v1_router) api_router.include_router(problem.v2_router) diff --git a/wacruit/src/apps/sponsor/__init__.py b/wacruit/src/apps/sponsor/__init__.py new file mode 100644 index 00000000..6dc77762 --- /dev/null +++ b/wacruit/src/apps/sponsor/__init__.py @@ -0,0 +1 @@ +from .views import v3_router as router diff --git a/wacruit/src/apps/sponsor/exceptions.py b/wacruit/src/apps/sponsor/exceptions.py new file mode 100644 index 00000000..f60d304a --- /dev/null +++ b/wacruit/src/apps/sponsor/exceptions.py @@ -0,0 +1,11 @@ +from wacruit.src.apps.common.exceptions import WacruitException + + +class SponsorAlreadyExistsException(WacruitException): + def __init__(self): + super().__init__(status_code=400, detail="이미 존재하는 후원자입니다.") + + +class SponsorNotFoundException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="후원자를 찾을 수 없습니다.") diff --git a/wacruit/src/apps/sponsor/repositories.py b/wacruit/src/apps/sponsor/repositories.py new file mode 100644 index 00000000..6594b46c --- /dev/null +++ b/wacruit/src/apps/sponsor/repositories.py @@ -0,0 +1,35 @@ +from fastapi import Depends +from sqlalchemy.orm import Session + +from wacruit.src.apps.sponsor.models import Sponsor +from wacruit.src.database.connection import get_db_session +from wacruit.src.database.connection import Transaction + + +class SponsorRepository: + def __init__( + self, + session: Session = Depends(get_db_session), + transaction: Transaction = Depends(), + ): + self.session = session + self.transaction = transaction + + def create_sponsor(self, sponsor: Sponsor): + with self.transaction: + self.session.add(sponsor) + return sponsor + + def get_sponsor_by_sponsor_name(self, sponsor_name: str) -> Sponsor | None: + return self.session.query(Sponsor).filter(Sponsor.name == sponsor_name).first() + + def get_sponsor_by_id(self, sponsor_id: str) -> Sponsor | None: + return self.session.query(Sponsor).filter(Sponsor.id == sponsor_id).first() + + def get_all_sponsors(self) -> list[Sponsor]: + return self.session.query(Sponsor).all() + + def update_sponsor(self, sponsor: Sponsor): + with self.transaction: + self.session.merge(sponsor) + return sponsor diff --git a/wacruit/src/apps/sponsor/schemas.py b/wacruit/src/apps/sponsor/schemas.py new file mode 100644 index 00000000..47129d0b --- /dev/null +++ b/wacruit/src/apps/sponsor/schemas.py @@ -0,0 +1,35 @@ +from datetime import date + +from pydantic import BaseModel +from pydantic import EmailStr +from pydantic import Field + +from wacruit.src.apps.common.schemas import OrmModel + + +class SponsorCreateRequest(BaseModel): + name: str + amount: int = Field(..., gt=0) + sponsored_date: date + email: EmailStr | None = None + phone_number: str | None = None + + +class SponsorInfoResponse(OrmModel): + id: int + name: str + amount: int = Field(..., gt=0) + sponsored_date: date + email: EmailStr | None = None + phone_number: str | None = None + + +class SponsorBriefResponse(OrmModel): + id: int + name: str + + +class SponsorUpdateRequest(BaseModel): + email: EmailStr | None = None + phone_number: str | None = None + amount: int | None = None diff --git a/wacruit/src/apps/sponsor/services.py b/wacruit/src/apps/sponsor/services.py new file mode 100644 index 00000000..3ca6a4ab --- /dev/null +++ b/wacruit/src/apps/sponsor/services.py @@ -0,0 +1,60 @@ +from typing import Annotated + +from fastapi import Depends + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.sponsor.exceptions import SponsorAlreadyExistsException +from wacruit.src.apps.sponsor.exceptions import SponsorNotFoundException +from wacruit.src.apps.sponsor.models import Sponsor +from wacruit.src.apps.sponsor.repositories import SponsorRepository +from wacruit.src.apps.sponsor.schemas import SponsorBriefResponse +from wacruit.src.apps.sponsor.schemas import SponsorCreateRequest +from wacruit.src.apps.sponsor.schemas import SponsorInfoResponse +from wacruit.src.apps.sponsor.schemas import SponsorUpdateRequest + + +class SponsorService: + def __init__(self, sponsor_repository: Annotated[SponsorRepository, Depends()]): + self.sponsor_repository = sponsor_repository + + def create_sponsor(self, request: SponsorCreateRequest): + if self.sponsor_repository.get_sponsor_by_sponsor_name(request.name): + raise SponsorAlreadyExistsException + sponsor = Sponsor( + name=request.name, + amount=request.amount, + email=request.email, + phone_number=request.phone_number, + sponsored_date=request.sponsored_date, + ) + return self.sponsor_repository.create_sponsor(sponsor) + + def get_sponsor_by_id(self, sponsor_id: str) -> SponsorInfoResponse: + sponsor = self.sponsor_repository.get_sponsor_by_id(sponsor_id) + if not sponsor: + raise SponsorNotFoundException + return SponsorInfoResponse.from_orm(sponsor) + + def get_all_sponsors(self) -> ListResponse[SponsorBriefResponse]: + sponsors = self.sponsor_repository.get_all_sponsors() + return ( + ListResponse[SponsorBriefResponse]( + items=[SponsorBriefResponse.from_orm(sponsor) for sponsor in sponsors] + ) + if sponsors + else ListResponse[SponsorBriefResponse](items=[]) + ) + + def update_sponsor( + self, sponsor_id: str, request: SponsorUpdateRequest + ) -> SponsorInfoResponse: + sponsor = self.sponsor_repository.get_sponsor_by_id(sponsor_id) + if not sponsor: + raise SponsorNotFoundException + + for field, value in request.dict(exclude_unset=True).items(): + setattr(sponsor, field, value) + + self.sponsor_repository.update_sponsor(sponsor) + + return SponsorInfoResponse.from_orm(sponsor) diff --git a/wacruit/src/apps/sponsor/views.py b/wacruit/src/apps/sponsor/views.py new file mode 100644 index 00000000..48a0c818 --- /dev/null +++ b/wacruit/src/apps/sponsor/views.py @@ -0,0 +1,50 @@ +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends +from fastapi import Response + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.sponsor.schemas import SponsorBriefResponse +from wacruit.src.apps.sponsor.schemas import SponsorCreateRequest +from wacruit.src.apps.sponsor.schemas import SponsorInfoResponse +from wacruit.src.apps.sponsor.schemas import SponsorUpdateRequest +from wacruit.src.apps.sponsor.services import SponsorService +from wacruit.src.apps.user.dependencies import AdminUser + +v3_router = APIRouter(prefix="/v3/sponsor", tags=["sponsor"]) + + +@v3_router.post("", status_code=201) +def create_sponsor( + admin_user: AdminUser, + request: SponsorCreateRequest, + sponsor_service: Annotated[SponsorService, Depends()], +): + sponsor_service.create_sponsor(request) + + +@v3_router.get("/{sponsor_id}") +def get_sponsor( + admin_user: AdminUser, + sponsor_id: str, + sponsor_service: Annotated[SponsorService, Depends()], +) -> SponsorInfoResponse: + return sponsor_service.get_sponsor_by_id(sponsor_id) + + +@v3_router.get("") +def get_all_sponsors( + sponsor_service: Annotated[SponsorService, Depends()], +) -> ListResponse[SponsorBriefResponse]: + return sponsor_service.get_all_sponsors() + + +@v3_router.patch("/{sponsor_id}") +def update_sponsor( + admin_user: AdminUser, + sponsor_id: str, + request: SponsorUpdateRequest, + sponsor_service: Annotated[SponsorService, Depends()], +) -> SponsorInfoResponse: + return sponsor_service.update_sponsor(sponsor_id, request) From 466ffdd730022eaf9dd35763c4bffdec5c26ff59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Thu, 17 Jul 2025 00:55:36 +0900 Subject: [PATCH 15/36] =?UTF-8?q?feat:=20question=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * question과 관련된 api를 구현했습니다. --- wacruit/src/apps/faq/__init__.py | 1 + wacruit/src/apps/faq/exceptions.py | 11 +++++ wacruit/src/apps/faq/repositories.py | 41 ++++++++++++++++++ wacruit/src/apps/faq/schemas.py | 20 +++++++++ wacruit/src/apps/faq/services.py | 39 +++++++++++++++++ wacruit/src/apps/faq/views.py | 62 ++++++++++++++++++++++++++++ wacruit/src/apps/router.py | 2 + 7 files changed, 176 insertions(+) create mode 100644 wacruit/src/apps/faq/__init__.py create mode 100644 wacruit/src/apps/faq/exceptions.py create mode 100644 wacruit/src/apps/faq/repositories.py create mode 100644 wacruit/src/apps/faq/schemas.py create mode 100644 wacruit/src/apps/faq/services.py create mode 100644 wacruit/src/apps/faq/views.py diff --git a/wacruit/src/apps/faq/__init__.py b/wacruit/src/apps/faq/__init__.py new file mode 100644 index 00000000..6dc77762 --- /dev/null +++ b/wacruit/src/apps/faq/__init__.py @@ -0,0 +1 @@ +from .views import v3_router as router diff --git a/wacruit/src/apps/faq/exceptions.py b/wacruit/src/apps/faq/exceptions.py new file mode 100644 index 00000000..c12e73a0 --- /dev/null +++ b/wacruit/src/apps/faq/exceptions.py @@ -0,0 +1,11 @@ +from wacruit.src.apps.common.exceptions import WacruitException + + +class QuestionNowFoundException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="존재하지 않는 질문입니다.") + + +class QuestionListEmptyException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="질문 리스트가 비어 있습니다.") diff --git a/wacruit/src/apps/faq/repositories.py b/wacruit/src/apps/faq/repositories.py new file mode 100644 index 00000000..bbacd5e8 --- /dev/null +++ b/wacruit/src/apps/faq/repositories.py @@ -0,0 +1,41 @@ +from fastapi import Depends +from sqlalchemy import delete +from sqlalchemy import select +from sqlalchemy.orm import Session + +from wacruit.src.apps.faq.models import FAQ +from wacruit.src.database.connection import get_db_session +from wacruit.src.database.connection import Transaction + + +class QuestionRepository: + def __init__( + self, + session: Session = Depends(get_db_session), + transaction: Transaction = Depends(), + ) -> None: + self.session = session + self.transaction = transaction + + def get_question_by_id(self, questions_id: int) -> FAQ | None: + return self.session.execute( + select(FAQ).where(FAQ.id == questions_id) + ).scalar_one_or_none() + + def get_questions(self) -> list[FAQ]: + query = select(FAQ) + return list(self.session.execute(query).scalars().all()) + + def create_questions(self, question: FAQ) -> FAQ: + with self.transaction: + self.session.add(question) + return question + + def update_question(self, question: FAQ) -> FAQ: + with self.transaction: + self.session.merge(question) + return question + + def delete_question(self, questions_id: int) -> None: + with self.transaction: + self.session.execute(delete(FAQ).where(FAQ.id == questions_id)) diff --git a/wacruit/src/apps/faq/schemas.py b/wacruit/src/apps/faq/schemas.py new file mode 100644 index 00000000..cd784732 --- /dev/null +++ b/wacruit/src/apps/faq/schemas.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel + + +class CreateQuestionRequest(BaseModel): + question: str + answer: str + + +class UpdateQuestionRequest(BaseModel): + question: str | None = None + answer: str | None = None + + +class QuestionResponse(BaseModel): + id: int + question: str + answer: str + + class Config: + orm_mode = True diff --git a/wacruit/src/apps/faq/services.py b/wacruit/src/apps/faq/services.py new file mode 100644 index 00000000..7cc84bc2 --- /dev/null +++ b/wacruit/src/apps/faq/services.py @@ -0,0 +1,39 @@ +from typing import Annotated + +from fastapi import Depends + +from wacruit.src.apps.faq.exceptions import QuestionListEmptyException +from wacruit.src.apps.faq.exceptions import QuestionNowFoundException +from wacruit.src.apps.faq.models import FAQ +from wacruit.src.apps.faq.repositories import QuestionRepository +from wacruit.src.apps.faq.schemas import UpdateQuestionRequest + + +class QuestionService: + def __init__(self, question_repository: Annotated[QuestionRepository, Depends()]): + self.question_repository = question_repository + + def get_question_by_id(self, questions_id: int) -> FAQ: + question = self.question_repository.get_question_by_id(questions_id) + if question is None: + raise QuestionNowFoundException + return question + + def create_question(self, question: FAQ) -> FAQ: + return self.question_repository.create_questions(question) + + def get_questions(self) -> list[FAQ]: + questions = self.question_repository.get_questions() + if not questions: + raise QuestionListEmptyException + return questions + + def update_question(self, questions_id: int, req: UpdateQuestionRequest) -> FAQ: + question = self.get_question_by_id(questions_id) + for key, value in req.dict(exclude_none=True).items(): + setattr(question, key, value) + return self.question_repository.update_question(question) + + def delete_question(self, questions_id: int) -> None: + self.get_question_by_id(questions_id) + self.question_repository.delete_question(questions_id) diff --git a/wacruit/src/apps/faq/views.py b/wacruit/src/apps/faq/views.py new file mode 100644 index 00000000..470aa152 --- /dev/null +++ b/wacruit/src/apps/faq/views.py @@ -0,0 +1,62 @@ +from http import HTTPStatus +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.faq.models import FAQ +from wacruit.src.apps.faq.schemas import CreateQuestionRequest +from wacruit.src.apps.faq.schemas import QuestionResponse +from wacruit.src.apps.faq.schemas import UpdateQuestionRequest +from wacruit.src.apps.faq.services import QuestionService +from wacruit.src.apps.user.dependencies import AdminUser + +v3_router = APIRouter(prefix="/v3/questions", tags=["questions"]) + + +@v3_router.get(path="", status_code=HTTPStatus.OK) +def get_questions( + question_service: Annotated[QuestionService, Depends()] +) -> ListResponse[QuestionResponse]: + res = question_service.get_questions() + items = [] + + for q in res: + items.append(QuestionResponse.from_orm(q)) + + return ListResponse(items=items) + + +@v3_router.post(path="", status_code=HTTPStatus.CREATED) +def create_question( + admin_user: AdminUser, + request: CreateQuestionRequest, + question_service: Annotated[QuestionService, Depends()], +) -> QuestionResponse: + res = question_service.create_question( + FAQ(question=request.question, answer=request.answer) + ) + + return QuestionResponse.from_orm(res) + + +@v3_router.patch(path="/{questions_id}", status_code=HTTPStatus.OK) +def update_question( + admin_user: AdminUser, + questions_id: int, + request: UpdateQuestionRequest, + question_service: Annotated[QuestionService, Depends()], +) -> QuestionResponse: + res = question_service.update_question(questions_id, request) + + return QuestionResponse.from_orm(res) + + +@v3_router.delete(path="/{questions_id}", status_code=HTTPStatus.NO_CONTENT) +def delete_question( + admin_user: AdminUser, + questions_id: int, + question_service: Annotated[QuestionService, Depends()], +) -> None: + question_service.delete_question(questions_id) diff --git a/wacruit/src/apps/router.py b/wacruit/src/apps/router.py index 5bffd3d4..96ab9cbb 100644 --- a/wacruit/src/apps/router.py +++ b/wacruit/src/apps/router.py @@ -2,6 +2,7 @@ from wacruit.src.apps import announcement from wacruit.src.apps import dummy +from wacruit.src.apps import faq from wacruit.src.apps import history from wacruit.src.apps import member from wacruit.src.apps import portfolio @@ -31,3 +32,4 @@ api_router.include_router(seminar.router) api_router.include_router(history.router) api_router.include_router(review.router) +api_router.include_router(faq.router) From 89f9a93cf2608c137df92cec2bb77af1047f9940 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Fri, 18 Jul 2025 00:59:07 +0900 Subject: [PATCH 16/36] =?UTF-8?q?Feat:=20timeline=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - timeline과 timeline category api를 구현했습니다 - 노션 api 요구사항에 맞게 Timeline 테이블에서 category 열을 없애고 category_id 외래키 열을 추가했습니다 --- wacruit/src/apps/common/enums.py | 6 + wacruit/src/apps/router.py | 2 + wacruit/src/apps/sponsor/services.py | 2 +- wacruit/src/apps/timeline/__init__.py | 1 + wacruit/src/apps/timeline/exceptions.py | 11 ++ wacruit/src/apps/timeline/models.py | 17 ++- wacruit/src/apps/timeline/repositories.py | 76 ++++++++++++ wacruit/src/apps/timeline/schemas.py | 40 ++++++ wacruit/src/apps/timeline/services.py | 115 ++++++++++++++++++ wacruit/src/apps/timeline/views.py | 90 ++++++++++++++ ...c9b5e993e78b_add_timelinecategory_table.py | 52 ++++++++ 11 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 wacruit/src/apps/timeline/__init__.py create mode 100644 wacruit/src/apps/timeline/exceptions.py create mode 100644 wacruit/src/apps/timeline/repositories.py create mode 100644 wacruit/src/apps/timeline/schemas.py create mode 100644 wacruit/src/apps/timeline/services.py create mode 100644 wacruit/src/apps/timeline/views.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_17_1517-c9b5e993e78b_add_timelinecategory_table.py diff --git a/wacruit/src/apps/common/enums.py b/wacruit/src/apps/common/enums.py index aecf2734..356d07c3 100644 --- a/wacruit/src/apps/common/enums.py +++ b/wacruit/src/apps/common/enums.py @@ -109,3 +109,9 @@ class ProjectURLType(Enum): IOS_GITHUB = "IOS_GITHUB" CRAWLER_GITHUB = "CRAWLER_GITHUB" BEHANCE = "BEHANCE" + + +class TimelineGroupType(Enum): + ROOKIES = "ROOKIES" + PROGRAMMERS = "PROGRAMMERS" + DESIGNERS = "DESIGNERS" diff --git a/wacruit/src/apps/router.py b/wacruit/src/apps/router.py index 96ab9cbb..fbad7062 100644 --- a/wacruit/src/apps/router.py +++ b/wacruit/src/apps/router.py @@ -13,6 +13,7 @@ from wacruit.src.apps import review from wacruit.src.apps import seminar from wacruit.src.apps import sponsor +from wacruit.src.apps import timeline from wacruit.src.apps import user api_router = APIRouter(prefix="/api") @@ -33,3 +34,4 @@ api_router.include_router(history.router) api_router.include_router(review.router) api_router.include_router(faq.router) +api_router.include_router(timeline.router) diff --git a/wacruit/src/apps/sponsor/services.py b/wacruit/src/apps/sponsor/services.py index 3ca6a4ab..1f8e18fa 100644 --- a/wacruit/src/apps/sponsor/services.py +++ b/wacruit/src/apps/sponsor/services.py @@ -52,7 +52,7 @@ def update_sponsor( if not sponsor: raise SponsorNotFoundException - for field, value in request.dict(exclude_unset=True).items(): + for field, value in request.dict(exclude_none=True).items(): setattr(sponsor, field, value) self.sponsor_repository.update_sponsor(sponsor) diff --git a/wacruit/src/apps/timeline/__init__.py b/wacruit/src/apps/timeline/__init__.py new file mode 100644 index 00000000..6dc77762 --- /dev/null +++ b/wacruit/src/apps/timeline/__init__.py @@ -0,0 +1 @@ +from .views import v3_router as router diff --git a/wacruit/src/apps/timeline/exceptions.py b/wacruit/src/apps/timeline/exceptions.py new file mode 100644 index 00000000..89728e3b --- /dev/null +++ b/wacruit/src/apps/timeline/exceptions.py @@ -0,0 +1,11 @@ +from wacruit.src.apps.common.exceptions import WacruitException + + +class TimelineCategoryNotFoundException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="타임라인 카테고리를 찾을 수 없습니다.") + + +class TimelineNotFoundException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="타임라인을 찾을 수 없습니다.") diff --git a/wacruit/src/apps/timeline/models.py b/wacruit/src/apps/timeline/models.py index 9442b390..65c061c2 100644 --- a/wacruit/src/apps/timeline/models.py +++ b/wacruit/src/apps/timeline/models.py @@ -1,9 +1,12 @@ from datetime import datetime from sqlalchemy import DateTime +from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship +from wacruit.src.apps.common.enums import TimelineGroupType from wacruit.src.database.base import DeclarativeBase from wacruit.src.database.base import intpk from wacruit.src.database.base import str30 @@ -13,7 +16,19 @@ class Timeline(DeclarativeBase): __tablename__ = "timeline" id: Mapped[intpk] + category_id: Mapped[int] = mapped_column( + ForeignKey("timeline_category.id"), nullable=False + ) + group: Mapped[TimelineGroupType] title: Mapped[str30] - category: Mapped[str30] start_date: Mapped[datetime] = mapped_column(DateTime(timezone=True)) end_date: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + + category: Mapped["TimelineCategory"] = relationship("TimelineCategory") + + +class TimelineCategory(DeclarativeBase): + __tablename__ = "timeline_category" + + id: Mapped[intpk] + title: Mapped[str30] diff --git a/wacruit/src/apps/timeline/repositories.py b/wacruit/src/apps/timeline/repositories.py new file mode 100644 index 00000000..8a7ac3ed --- /dev/null +++ b/wacruit/src/apps/timeline/repositories.py @@ -0,0 +1,76 @@ +from fastapi import Depends +from sqlalchemy import delete +from sqlalchemy.orm import Session + +from wacruit.src.apps.common.enums import TimelineGroupType +from wacruit.src.apps.timeline.models import Timeline +from wacruit.src.apps.timeline.models import TimelineCategory +from wacruit.src.database.connection import get_db_session +from wacruit.src.database.connection import Transaction + + +class TimelineRepository: + def __init__( + self, + session: Session = Depends(get_db_session), + transaction: Transaction = Depends(), + ): + self.session = session + self.transaction = transaction + + def create_category(self, category: TimelineCategory) -> TimelineCategory: + with self.transaction: + self.session.add(category) + return category + + def get_all_categories( + self, group: TimelineGroupType | None = None + ) -> list[TimelineCategory]: + if group is None: + return self.session.query(TimelineCategory).all() + + return ( + self.session.query(TimelineCategory) + .join(Timeline, TimelineCategory.id == Timeline.category_id) + .filter(Timeline.group == group) + .distinct() + .all() + ) + + def get_category_by_id(self, category_id: int) -> TimelineCategory | None: + return self.session.query(TimelineCategory).filter_by(id=category_id).first() + + def update_category(self, category: TimelineCategory) -> TimelineCategory: + with self.transaction: + self.session.merge(category) + return category + + def delete_category(self, category: TimelineCategory): + with self.transaction: + self.session.delete(category) + return category + + def create_timeline(self, timeline: Timeline) -> Timeline: + with self.transaction: + self.session.add(timeline) + return timeline + + def get_timeline_by_id(self, timeline_id: int) -> Timeline | None: + return self.session.query(Timeline).filter_by(id=timeline_id).first() + + def get_all_timelines( + self, group: TimelineGroupType | None = None + ) -> list[Timeline]: + if group is None: + return self.session.query(Timeline).all() + + return self.session.query(Timeline).filter(Timeline.group == group).all() + + def update_timeline(self, timeline: Timeline) -> Timeline: + with self.transaction: + self.session.merge(timeline) + return timeline + + def delete_timeline(self, timeline_id: int): + with self.transaction: + self.session.execute(delete(Timeline).where(Timeline.id == timeline_id)) diff --git a/wacruit/src/apps/timeline/schemas.py b/wacruit/src/apps/timeline/schemas.py new file mode 100644 index 00000000..590fb26d --- /dev/null +++ b/wacruit/src/apps/timeline/schemas.py @@ -0,0 +1,40 @@ +from datetime import datetime + +from pydantic import BaseModel + +from wacruit.src.apps.common.enums import TimelineGroupType +from wacruit.src.apps.common.schemas import OrmModel + + +class TimelineCategoryCreateUpdateRequest(BaseModel): + title: str + + +class TimelineCreateRequest(BaseModel): + title: str + group: TimelineGroupType + category_id: int + start_date: datetime + end_date: datetime + + +class TimelineUpdateRequest(BaseModel): + title: str | None = None + group: TimelineGroupType | None = None + category_id: int | None = None + start_date: datetime | None = None + end_date: datetime | None = None + + +class TimelineCategoryResponse(OrmModel): + id: int + title: str + + +class TimelineResponse(OrmModel): + id: int + title: str + group: TimelineGroupType + category: TimelineCategoryResponse + start_date: datetime + end_date: datetime diff --git a/wacruit/src/apps/timeline/services.py b/wacruit/src/apps/timeline/services.py new file mode 100644 index 00000000..3613902a --- /dev/null +++ b/wacruit/src/apps/timeline/services.py @@ -0,0 +1,115 @@ +from typing import Annotated + +from fastapi import Depends + +from wacruit.src.apps.common.enums import TimelineGroupType +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.timeline.exceptions import TimelineCategoryNotFoundException +from wacruit.src.apps.timeline.exceptions import TimelineNotFoundException +from wacruit.src.apps.timeline.models import Timeline +from wacruit.src.apps.timeline.models import TimelineCategory +from wacruit.src.apps.timeline.repositories import TimelineRepository +from wacruit.src.apps.timeline.schemas import TimelineCategoryCreateUpdateRequest +from wacruit.src.apps.timeline.schemas import TimelineCategoryResponse +from wacruit.src.apps.timeline.schemas import TimelineCreateRequest +from wacruit.src.apps.timeline.schemas import TimelineResponse +from wacruit.src.apps.timeline.schemas import TimelineUpdateRequest + + +class TimelineService: + def __init__( + self, + timeline_repository: TimelineRepository = Depends(), + ): + self.timeline_repository = timeline_repository + + def create_category( + self, request: TimelineCategoryCreateUpdateRequest + ) -> TimelineCategoryResponse: + category = TimelineCategory( + title=request.title, + ) + created_category = self.timeline_repository.create_category(category) + return TimelineCategoryResponse.from_orm(created_category) + + def get_all_categories( + self, group: TimelineGroupType | None = None + ) -> ListResponse[TimelineCategoryResponse]: + categories = self.timeline_repository.get_all_categories(group=group) + return ( + ListResponse( + items=[ + TimelineCategoryResponse.from_orm(category) + for category in categories + ] + ) + if categories + else ListResponse(items=[]) + ) + + def update_category( + self, category_id: int, request: TimelineCategoryCreateUpdateRequest + ) -> TimelineCategoryResponse: + category = self.timeline_repository.get_category_by_id(category_id) + if not category: + raise TimelineCategoryNotFoundException + category.title = request.title + self.timeline_repository.update_category(category) + return TimelineCategoryResponse.from_orm(category) + + def delete_category(self, category_id: int): + category = self.timeline_repository.get_category_by_id(category_id) + if not category: + raise TimelineCategoryNotFoundException + self.timeline_repository.delete_category(category) + + def create_timeline(self, request: TimelineCreateRequest) -> TimelineResponse: + if not self.timeline_repository.get_category_by_id(request.category_id): + raise TimelineCategoryNotFoundException + timeline = Timeline( + title=request.title, + group=request.group, + category_id=request.category_id, + start_date=request.start_date, + end_date=request.end_date, + ) + created_timeline = self.timeline_repository.create_timeline(timeline) + return TimelineResponse.from_orm(created_timeline) + + def get_timeline_by_id(self, timeline_id: int) -> TimelineResponse: + timeline = self.timeline_repository.get_timeline_by_id(timeline_id) + if not timeline: + raise TimelineNotFoundException + return TimelineResponse.from_orm(timeline) + + def get_all_timelines( + self, group: TimelineGroupType | None = None + ) -> ListResponse[TimelineResponse]: + timelines = self.timeline_repository.get_all_timelines(group=group) + return ( + ListResponse( + items=[TimelineResponse.from_orm(timeline) for timeline in timelines] + ) + if timelines + else ListResponse(items=[]) + ) + + def update_timeline( + self, timeline_id: int, request: TimelineUpdateRequest + ) -> TimelineResponse: + timeline = self.timeline_repository.get_timeline_by_id(timeline_id) + if not timeline: + raise TimelineNotFoundException + if request.category_id and not self.timeline_repository.get_category_by_id( + request.category_id + ): + raise TimelineCategoryNotFoundException + for key, value in request.dict(exclude_none=True).items(): + setattr(timeline, key, value) + timeline = self.timeline_repository.update_timeline(timeline) + return TimelineResponse.from_orm(timeline) + + def delete_timeline(self, timeline_id: int): + if not self.timeline_repository.get_timeline_by_id(timeline_id): + raise TimelineNotFoundException + self.timeline_repository.delete_timeline(timeline_id) diff --git a/wacruit/src/apps/timeline/views.py b/wacruit/src/apps/timeline/views.py new file mode 100644 index 00000000..19464932 --- /dev/null +++ b/wacruit/src/apps/timeline/views.py @@ -0,0 +1,90 @@ +from http import HTTPStatus +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends + +from wacruit.src.apps.common.enums import TimelineGroupType +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.timeline.schemas import TimelineCategoryCreateUpdateRequest +from wacruit.src.apps.timeline.schemas import TimelineCategoryResponse +from wacruit.src.apps.timeline.schemas import TimelineCreateRequest +from wacruit.src.apps.timeline.schemas import TimelineResponse +from wacruit.src.apps.timeline.schemas import TimelineUpdateRequest +from wacruit.src.apps.timeline.services import TimelineService +from wacruit.src.apps.user.dependencies import AdminUser + +v3_router = APIRouter(prefix="/v3/timelines", tags=["timelines"]) + + +@v3_router.post("/categories", status_code=HTTPStatus.CREATED) +def create_timeline_category( + admin_user: AdminUser, + request: TimelineCategoryCreateUpdateRequest, + timeline_service: Annotated[TimelineService, Depends()], +) -> TimelineCategoryResponse: + return timeline_service.create_category(request) + + +@v3_router.get("/categories") +def get_all_timeline_categories( + admin_user: AdminUser, + timeline_service: Annotated[TimelineService, Depends()], + group: TimelineGroupType | None = None, +) -> ListResponse[TimelineCategoryResponse]: + return timeline_service.get_all_categories(group=group) + + +@v3_router.patch("/categories/{category_id}") +def update_timeline_category( + admin_user: AdminUser, + category_id: int, + request: TimelineCategoryCreateUpdateRequest, + timeline_service: Annotated[TimelineService, Depends()], +) -> TimelineCategoryResponse: + return timeline_service.update_category(category_id, request) + + +@v3_router.delete("/categories/{category_id}", status_code=HTTPStatus.NO_CONTENT) +def delete_timeline_category( + admin_user: AdminUser, + category_id: int, + timeline_service: Annotated[TimelineService, Depends()], +): + timeline_service.delete_category(category_id) + + +@v3_router.post("", status_code=HTTPStatus.CREATED) +def create_timeline( + admin_user: AdminUser, + request: TimelineCreateRequest, + timeline_service: Annotated[TimelineService, Depends()], +) -> TimelineResponse: + return timeline_service.create_timeline(request) + + +@v3_router.get("") +def get_all_timelines( + timeline_service: Annotated[TimelineService, Depends()], + group: TimelineGroupType | None = None, +) -> ListResponse[TimelineResponse]: + return timeline_service.get_all_timelines(group=group) + + +@v3_router.patch("/{timeline_id}") +def update_timeline( + admin_user: AdminUser, + timeline_id: int, + request: TimelineUpdateRequest, + timeline_service: Annotated[TimelineService, Depends()], +) -> TimelineResponse: + return timeline_service.update_timeline(timeline_id, request) + + +@v3_router.delete("/{timeline_id}", status_code=HTTPStatus.NO_CONTENT) +def delete_timeline( + admin_user: AdminUser, + timeline_id: int, + timeline_service: Annotated[TimelineService, Depends()], +): + timeline_service.delete_timeline(timeline_id) diff --git a/wacruit/src/database/migrations/versions/2025_07_17_1517-c9b5e993e78b_add_timelinecategory_table.py b/wacruit/src/database/migrations/versions/2025_07_17_1517-c9b5e993e78b_add_timelinecategory_table.py new file mode 100644 index 00000000..195e7309 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_17_1517-c9b5e993e78b_add_timelinecategory_table.py @@ -0,0 +1,52 @@ +"""add TimelineCategory table + +Revision ID: c9b5e993e78b +Revises: 8964bb6498ca +Create Date: 2025-07-17 15:17:13.718063 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "c9b5e993e78b" +down_revision = "8964bb6498ca" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "timeline_category", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("title", sa.String(length=30), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + op.add_column("timeline", sa.Column("category_id", sa.Integer(), nullable=False)) + op.add_column( + "timeline", + sa.Column( + "group", + sa.Enum("ROOKIES", "PROGRAMMERS", "DESIGNERS", name="timelinegrouptype"), + nullable=False, + ), + ) + op.create_foreign_key( + None, "timeline", "timeline_category", ["category_id"], ["id"] + ) + op.drop_column("timeline", "category") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "timeline", sa.Column("category", mysql.VARCHAR(length=30), nullable=False) + ) + op.drop_constraint(None, "timeline", type_="foreignkey") + op.drop_column("timeline", "group") + op.drop_column("timeline", "category_id") + op.drop_table("timeline_category") + # ### end Alembic commands ### From 0831af7ff9645aed74c66a855dd75d957c8d2d92 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:56:21 +0900 Subject: [PATCH 17/36] =?UTF-8?q?Fix:=20Project=20api,=20Recruiting=20api?= =?UTF-8?q?=20=EC=88=98=EC=A0=95=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서비스/스터디 목록 불러오는 api를 페이지네이션 가능하게 변경했습니다 - 여러 파일에 오타를 수정했습니다 - 모집 일정 불러오는 api에서 recruiting_type을 query parameter로 변경했습니다 --- wacruit/src/apps/common/enums.py | 6 +++--- wacruit/src/apps/project/repositories.py | 4 ++-- wacruit/src/apps/project/services.py | 6 ++++-- wacruit/src/apps/project/views.py | 9 ++++++--- wacruit/src/apps/recruiting/schemas.py | 2 +- wacruit/src/apps/recruiting/services.py | 5 +---- wacruit/src/apps/recruiting/views_v3.py | 9 +++++---- 7 files changed, 22 insertions(+), 19 deletions(-) diff --git a/wacruit/src/apps/common/enums.py b/wacruit/src/apps/common/enums.py index 356d07c3..81b2d284 100644 --- a/wacruit/src/apps/common/enums.py +++ b/wacruit/src/apps/common/enums.py @@ -112,6 +112,6 @@ class ProjectURLType(Enum): class TimelineGroupType(Enum): - ROOKIES = "ROOKIES" - PROGRAMMERS = "PROGRAMMERS" - DESIGNERS = "DESIGNERS" + ROOKIE = "ROOKIE" + PROGRAMMER = "PROGRAMMER" + DESIGNER = "DESIGNER" diff --git a/wacruit/src/apps/project/repositories.py b/wacruit/src/apps/project/repositories.py index 798111e8..97ffab7c 100644 --- a/wacruit/src/apps/project/repositories.py +++ b/wacruit/src/apps/project/repositories.py @@ -32,8 +32,8 @@ def get_project_by_id(self, project_id: int) -> Project | None: def get_project_by_name(self, name: str) -> Project | None: return self.session.query(Project).filter(Project.name == name).first() - def get_projects(self) -> list[Project]: - return self.session.query(Project).all() + def get_projects(self, offset: int = 0, limit: int = 10) -> list[Project]: + return self.session.query(Project).offset(offset).limit(limit).all() def create_project(self, project: Project) -> Project: with self.transaction: diff --git a/wacruit/src/apps/project/services.py b/wacruit/src/apps/project/services.py index bc291c41..4b3a59b8 100644 --- a/wacruit/src/apps/project/services.py +++ b/wacruit/src/apps/project/services.py @@ -77,8 +77,10 @@ def get_project(self, project_id: int) -> ProjectDetailResponse: raise ProjectNotFoundException return ProjectDetailResponse.from_orm(project) - def list_projects(self) -> ListResponse[ProjectBriefResponse]: - projects = self.project_repository.get_projects() + def list_projects( + self, offset: int = 0, limit: int = 10 + ) -> ListResponse[ProjectBriefResponse]: + projects = self.project_repository.get_projects(offset=offset, limit=limit) items = [] for project in projects: items.append(ProjectBriefResponse.from_orm(project)) diff --git a/wacruit/src/apps/project/views.py b/wacruit/src/apps/project/views.py index 4111ba84..9aa1ba5a 100644 --- a/wacruit/src/apps/project/views.py +++ b/wacruit/src/apps/project/views.py @@ -1,3 +1,4 @@ +from http import HTTPStatus from typing import Annotated from fastapi import APIRouter @@ -14,7 +15,7 @@ v3_router = APIRouter(prefix="/v3/projects", tags=["projects"]) -@v3_router.post("") +@v3_router.post("", status_code=HTTPStatus.CREATED) def create_project( request: ProjectCreateRequest, project_service: Annotated[ProjectService, Depends()], @@ -32,9 +33,11 @@ def get_project( @v3_router.get("") def list_projects( - project_service: Annotated[ProjectService, Depends()] + project_service: Annotated[ProjectService, Depends()], + offset: int = 0, + limit: int = 10, ) -> ListResponse[ProjectBriefResponse]: - return project_service.list_projects() + return project_service.list_projects(offset=offset, limit=limit) @v3_router.patch("/{project_id}") diff --git a/wacruit/src/apps/recruiting/schemas.py b/wacruit/src/apps/recruiting/schemas.py index 61ece9d9..891fd071 100644 --- a/wacruit/src/apps/recruiting/schemas.py +++ b/wacruit/src/apps/recruiting/schemas.py @@ -92,7 +92,7 @@ class RecruitingResultResponse(BaseModel): class RecruitingCreateRequest(BaseModel): name: str - type: str + type: RecruitingType generation: str is_active: bool from_date: datetime diff --git a/wacruit/src/apps/recruiting/services.py b/wacruit/src/apps/recruiting/services.py index ad065b8b..76568b92 100644 --- a/wacruit/src/apps/recruiting/services.py +++ b/wacruit/src/apps/recruiting/services.py @@ -158,12 +158,9 @@ def get_recruiting_infos_by_type( ) def create_recruiting(self, request: RecruitingCreateRequest) -> RecruitingResponse: - if request.type not in RecruitingType.__members__: - raise InvalidRecruitTypeException(request.type) - recruiting = Recruiting( name=request.name, - type=RecruitingType[request.type], + type=request.type, generation=request.generation, is_active=request.is_active, from_date=request.from_date, diff --git a/wacruit/src/apps/recruiting/views_v3.py b/wacruit/src/apps/recruiting/views_v3.py index 733f6a53..031e32a7 100644 --- a/wacruit/src/apps/recruiting/views_v3.py +++ b/wacruit/src/apps/recruiting/views_v3.py @@ -1,3 +1,4 @@ +from http import HTTPStatus from typing import Annotated from fastapi import APIRouter @@ -18,7 +19,7 @@ v3_router = APIRouter(prefix="/v3/recruitings", tags=["recruitings"]) -@v3_router.post("") +@v3_router.post("", status_code=HTTPStatus.CREATED) def create_recruiting( admin_user: AdminUser, recruiting_service: Annotated[RecruitingService, Depends()], @@ -34,8 +35,8 @@ def get_active_recruitings( return recruiting_service.get_active_recruitings() -@v3_router.get("/info/{recruiting_type}") -def get_recruitings_by_type( +@v3_router.get("/info") +def get_recruiting_infos_by_type( recruiting_type: RecruitingType, recruiting_service: Annotated[RecruitingService, Depends()], ) -> ListResponse[RecruitingInfoResponse]: @@ -64,7 +65,7 @@ def get_recruiting_by_id( return recruiting_service.get_recruiting_by_id(recruiting_id) -@v3_router.post("/info") +@v3_router.post("/info", status_code=HTTPStatus.CREATED) def create_recruiting_info( admin_user: AdminUser, recruiting_service: Annotated[RecruitingService, Depends()], From aa96a4b2d716159b66f8a6cb6ac1b4bdc7c1e8dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Fri, 18 Jul 2025 23:33:34 +0900 Subject: [PATCH 18/36] =?UTF-8?q?fix:=20=EB=B9=88=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=98=ED=99=98=20(#88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 리스트가 비어있을 때의 예외 처리를 삭제했습니다. --- wacruit/src/apps/faq/exceptions.py | 5 ----- wacruit/src/apps/faq/services.py | 3 --- wacruit/src/apps/history/exceptions.py | 5 ----- wacruit/src/apps/history/services.py | 3 --- wacruit/src/apps/seminar/exceptions.py | 10 ---------- wacruit/src/apps/seminar/services.py | 6 ------ 6 files changed, 32 deletions(-) diff --git a/wacruit/src/apps/faq/exceptions.py b/wacruit/src/apps/faq/exceptions.py index c12e73a0..7d958701 100644 --- a/wacruit/src/apps/faq/exceptions.py +++ b/wacruit/src/apps/faq/exceptions.py @@ -4,8 +4,3 @@ class QuestionNowFoundException(WacruitException): def __init__(self): super().__init__(status_code=404, detail="존재하지 않는 질문입니다.") - - -class QuestionListEmptyException(WacruitException): - def __init__(self): - super().__init__(status_code=404, detail="질문 리스트가 비어 있습니다.") diff --git a/wacruit/src/apps/faq/services.py b/wacruit/src/apps/faq/services.py index 7cc84bc2..46053525 100644 --- a/wacruit/src/apps/faq/services.py +++ b/wacruit/src/apps/faq/services.py @@ -2,7 +2,6 @@ from fastapi import Depends -from wacruit.src.apps.faq.exceptions import QuestionListEmptyException from wacruit.src.apps.faq.exceptions import QuestionNowFoundException from wacruit.src.apps.faq.models import FAQ from wacruit.src.apps.faq.repositories import QuestionRepository @@ -24,8 +23,6 @@ def create_question(self, question: FAQ) -> FAQ: def get_questions(self) -> list[FAQ]: questions = self.question_repository.get_questions() - if not questions: - raise QuestionListEmptyException return questions def update_question(self, questions_id: int, req: UpdateQuestionRequest) -> FAQ: diff --git a/wacruit/src/apps/history/exceptions.py b/wacruit/src/apps/history/exceptions.py index 27113d20..f317e366 100644 --- a/wacruit/src/apps/history/exceptions.py +++ b/wacruit/src/apps/history/exceptions.py @@ -1,6 +1 @@ from wacruit.src.apps.common.exceptions import WacruitException - - -class HistoryEmptyException(WacruitException): - def __init__(self): - super().__init__(status_code=404, detail="History 테이블이 비어 있습니다.") diff --git a/wacruit/src/apps/history/services.py b/wacruit/src/apps/history/services.py index c81b4d45..4c46d451 100644 --- a/wacruit/src/apps/history/services.py +++ b/wacruit/src/apps/history/services.py @@ -2,7 +2,6 @@ from fastapi import Depends -from wacruit.src.apps.history.exceptions import HistoryEmptyException from wacruit.src.apps.history.models import History from wacruit.src.apps.history.repositories import HistoryRepository from wacruit.src.apps.history.schemas import UpdateHistoryRequest @@ -23,8 +22,6 @@ def get_history(self) -> list[History]: ) ) - if not history_list: - raise HistoryEmptyException return history_list def update_history(self, update_request: UpdateHistoryRequest): diff --git a/wacruit/src/apps/seminar/exceptions.py b/wacruit/src/apps/seminar/exceptions.py index d97a53d1..d746b7cc 100644 --- a/wacruit/src/apps/seminar/exceptions.py +++ b/wacruit/src/apps/seminar/exceptions.py @@ -4,13 +4,3 @@ class SeminarNotFoundException(WacruitException): def __init__(self): super().__init__(status_code=404, detail="존재하지 않는 세미나입니다.") - - -class SeminarListEmptyException(WacruitException): - def __init__(self): - super().__init__(status_code=404, detail="세미나 리스트가 비어있습니다.") - - -class SeminarNotActiveException(WacruitException): - def __init__(self): - super().__init__(status_code=404, detail="활성화된 세미나가 존재하지 않습니다.") diff --git a/wacruit/src/apps/seminar/services.py b/wacruit/src/apps/seminar/services.py index 72afa9b1..3e016edf 100644 --- a/wacruit/src/apps/seminar/services.py +++ b/wacruit/src/apps/seminar/services.py @@ -2,8 +2,6 @@ from fastapi import Depends -from wacruit.src.apps.seminar.exceptions import SeminarListEmptyException -from wacruit.src.apps.seminar.exceptions import SeminarNotActiveException from wacruit.src.apps.seminar.exceptions import SeminarNotFoundException from wacruit.src.apps.seminar.models import Seminar from wacruit.src.apps.seminar.repositories import SeminarRepository @@ -23,14 +21,10 @@ def get_seminar_by_id(self, seminar_id: int) -> Seminar: def get_active_seminar(self) -> list[Seminar]: seminar_list = self.seminar_repository.get_active_seminars() - if not seminar_list: - raise SeminarNotActiveException return seminar_list def get_all_seminars(self) -> list[Seminar]: seminar_list = self.seminar_repository.get_all_seminars() - if not seminar_list: - raise SeminarListEmptyException return seminar_list def create_seminar(self, request: CreateSeminarRequest) -> Seminar: From 217ceab20240e0f99b4d870993546e8b6046e0f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:27:12 +0900 Subject: [PATCH 19/36] feat: preregistration api (#89) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * pre registration api를 구현했습니다. * pre_registration 테이블이 추가가 안되어있어 마이그레이션 파일을 추가했습니다. --- wacruit/src/apps/pre_registration/__init__.py | 1 + .../src/apps/pre_registration/exceptions.py | 16 ++++ .../src/apps/pre_registration/repositories.py | 51 ++++++++++++ wacruit/src/apps/pre_registration/schemas.py | 23 ++++++ wacruit/src/apps/pre_registration/services.py | 77 +++++++++++++++++++ wacruit/src/apps/pre_registration/views.py | 64 +++++++++++++++ wacruit/src/apps/router.py | 2 + wacruit/src/database/__init__.py | 1 + ...9dd2ff558a82_add_pre_registration_table.py | 34 ++++++++ 9 files changed, 269 insertions(+) create mode 100644 wacruit/src/apps/pre_registration/__init__.py create mode 100644 wacruit/src/apps/pre_registration/exceptions.py create mode 100644 wacruit/src/apps/pre_registration/repositories.py create mode 100644 wacruit/src/apps/pre_registration/schemas.py create mode 100644 wacruit/src/apps/pre_registration/services.py create mode 100644 wacruit/src/apps/pre_registration/views.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_18_2104-9dd2ff558a82_add_pre_registration_table.py diff --git a/wacruit/src/apps/pre_registration/__init__.py b/wacruit/src/apps/pre_registration/__init__.py new file mode 100644 index 00000000..6dc77762 --- /dev/null +++ b/wacruit/src/apps/pre_registration/__init__.py @@ -0,0 +1 @@ +from .views import v3_router as router diff --git a/wacruit/src/apps/pre_registration/exceptions.py b/wacruit/src/apps/pre_registration/exceptions.py new file mode 100644 index 00000000..5c9577cb --- /dev/null +++ b/wacruit/src/apps/pre_registration/exceptions.py @@ -0,0 +1,16 @@ +from wacruit.src.apps.common.exceptions import WacruitException + + +class PreRegistNotActiveException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="활성화된 사전 알림이 존재하지 않습니다.") + + +class PreRegistNotExistException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="해당하는 사전 알림이 존재하지 않습니다.") + + +class PreRegistAlreadyExistException(WacruitException): + def __init__(self): + super().__init__(status_code=409, detail="활성화된 사전 알림이 이미 존재합니다.") diff --git a/wacruit/src/apps/pre_registration/repositories.py b/wacruit/src/apps/pre_registration/repositories.py new file mode 100644 index 00000000..91845a51 --- /dev/null +++ b/wacruit/src/apps/pre_registration/repositories.py @@ -0,0 +1,51 @@ +from fastapi import Depends +from sqlalchemy import delete +from sqlalchemy import select +from sqlalchemy import true +from sqlalchemy.orm import Session + +from wacruit.src.apps.pre_registration.models import PreRegistration +from wacruit.src.database.connection import get_db_session +from wacruit.src.database.connection import Transaction + + +class PreRegistrationRepository: + def __init__( + self, + session: Session = Depends(get_db_session), + transaction: Transaction = Depends(), + ) -> None: + self.session = session + self.transaction = transaction + + def get_pre_registration_by_id( + self, pre_registration_id: int + ) -> PreRegistration | None: + query = select(PreRegistration).where(PreRegistration.id == pre_registration_id) + return self.session.execute(query).scalar_one_or_none() + + def get_active_pre_registration(self) -> PreRegistration | None: + query = select(PreRegistration).where(PreRegistration.is_active == true()) + return self.session.execute(query).scalar_one_or_none() + + def get_pre_registration(self) -> list[PreRegistration]: + return list(self.session.execute(select(PreRegistration)).scalars()) + + def create_pre_registration( + self, pre_registration: PreRegistration + ) -> PreRegistration: + with self.transaction: + self.session.add(pre_registration) + return pre_registration + + def update_pre_registration( + self, pre_registration: PreRegistration + ) -> PreRegistration: + with self.transaction: + self.session.merge(pre_registration) + return pre_registration + + def delete_pre_registration(self, pre_registration_id: int) -> None: + self.session.execute( + delete(PreRegistration).where(PreRegistration.id == pre_registration_id) + ) diff --git a/wacruit/src/apps/pre_registration/schemas.py b/wacruit/src/apps/pre_registration/schemas.py new file mode 100644 index 00000000..ba88a2e2 --- /dev/null +++ b/wacruit/src/apps/pre_registration/schemas.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel + + +class CreatePreRegistrationRequest(BaseModel): + url: str + generation: str + is_active: bool + + +class UpdatePreRegistrationRequest(BaseModel): + url: str | None = None + generation: str | None = None + is_active: bool | None = None + + +class PreRegistrationResponse(BaseModel): + id: int + url: str + generation: str + is_active: bool + + class Config: + orm_mode = True diff --git a/wacruit/src/apps/pre_registration/services.py b/wacruit/src/apps/pre_registration/services.py new file mode 100644 index 00000000..3cf48424 --- /dev/null +++ b/wacruit/src/apps/pre_registration/services.py @@ -0,0 +1,77 @@ +from typing import Annotated + +from fastapi import Depends + +from wacruit.src.apps.pre_registration.exceptions import PreRegistAlreadyExistException +from wacruit.src.apps.pre_registration.exceptions import PreRegistNotActiveException +from wacruit.src.apps.pre_registration.exceptions import PreRegistNotExistException +from wacruit.src.apps.pre_registration.models import PreRegistration +from wacruit.src.apps.pre_registration.repositories import PreRegistrationRepository +from wacruit.src.apps.pre_registration.schemas import CreatePreRegistrationRequest +from wacruit.src.apps.pre_registration.schemas import UpdatePreRegistrationRequest + + +class PreRegistrationService: + def __init__( + self, + pre_registration_repository: Annotated[PreRegistrationRepository, Depends()], + ): + self.pre_registration_repository = pre_registration_repository + + def check_active_pre_registration(self) -> bool: + pre_registration = ( + self.pre_registration_repository.get_active_pre_registration() + ) + if pre_registration is None: + return False + return True + + def get_active_pre_registration(self) -> PreRegistration: + pre_registration = ( + self.pre_registration_repository.get_active_pre_registration() + ) + if pre_registration is None: + raise PreRegistNotActiveException + return pre_registration + + def get_pre_registration(self) -> list[PreRegistration]: + pre_registration_list = self.pre_registration_repository.get_pre_registration() + return pre_registration_list + + def create_pre_registration( + self, req: CreatePreRegistrationRequest + ) -> PreRegistration: + if req.is_active and (self.check_active_pre_registration()): + raise PreRegistAlreadyExistException + to_create = PreRegistration( + url=req.url, generation=req.generation, is_active=req.is_active + ) + pre_registration = self.pre_registration_repository.create_pre_registration( + to_create + ) + return pre_registration + + def update_pre_registration( + self, pre_registration_id: int, req: UpdatePreRegistrationRequest + ) -> PreRegistration: + pre_registration = self.pre_registration_repository.get_pre_registration_by_id( + pre_registration_id + ) + if pre_registration is None: + raise PreRegistNotExistException + if req.is_active and (self.check_active_pre_registration()): + raise PreRegistAlreadyExistException + + for key, value in req.dict(exclude_none=True).items(): + setattr(pre_registration, key, value) + return self.pre_registration_repository.update_pre_registration( + pre_registration + ) + + def delete_pre_registration(self, pre_registration_id: int) -> None: + pre_registration = self.pre_registration_repository.get_pre_registration_by_id( + pre_registration_id + ) + if pre_registration is None: + raise PreRegistNotExistException + self.pre_registration_repository.delete_pre_registration(pre_registration_id) diff --git a/wacruit/src/apps/pre_registration/views.py b/wacruit/src/apps/pre_registration/views.py new file mode 100644 index 00000000..f05b27c3 --- /dev/null +++ b/wacruit/src/apps/pre_registration/views.py @@ -0,0 +1,64 @@ +from http import HTTPStatus +from typing import Annotated + +from fastapi import APIRouter +from fastapi import Depends + +from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.pre_registration.models import PreRegistration +from wacruit.src.apps.pre_registration.schemas import CreatePreRegistrationRequest +from wacruit.src.apps.pre_registration.schemas import PreRegistrationResponse +from wacruit.src.apps.pre_registration.schemas import UpdatePreRegistrationRequest +from wacruit.src.apps.pre_registration.services import PreRegistrationService +from wacruit.src.apps.user.dependencies import AdminUser + +v3_router = APIRouter(prefix="/v3/pre-registration", tags=["preregistration"]) + + +@v3_router.get("/active", status_code=HTTPStatus.OK) +def get_active_pre_registration( + pre_registration_service: Annotated[PreRegistrationService, Depends()] +) -> PreRegistrationResponse: + pre_registration = pre_registration_service.get_active_pre_registration() + return PreRegistrationResponse.from_orm(pre_registration) + + +@v3_router.get("", status_code=HTTPStatus.OK) +def get_pre_registration( + admin_user: AdminUser, + pre_registration_service: Annotated[PreRegistrationService, Depends()], +) -> ListResponse[PreRegistrationResponse]: + pre_registration_list = pre_registration_service.get_pre_registration() + return ListResponse( + items=[PreRegistrationResponse.from_orm(item) for item in pre_registration_list] + ) + + +@v3_router.post("", status_code=HTTPStatus.CREATED) +def create_pre_registration( + admin_user: AdminUser, + request: CreatePreRegistrationRequest, + pre_registration_service: Annotated[PreRegistrationService, Depends()], +) -> PreRegistrationResponse: + res = pre_registration_service.create_pre_registration(request) + return PreRegistrationResponse.from_orm(res) + + +@v3_router.patch("/{pre_registration_id}", status_code=HTTPStatus.OK) +def update_pre_registration( + admin_user: AdminUser, + request: UpdatePreRegistrationRequest, + pre_registration_id: int, + pre_registration_service: Annotated[PreRegistrationService, Depends()], +) -> PreRegistrationResponse: + res = pre_registration_service.update_pre_registration(pre_registration_id, request) + return PreRegistrationResponse.from_orm(res) + + +@v3_router.delete("/{pre_registration_id}", status_code=HTTPStatus.NO_CONTENT) +def delete_pre_registration( + admin_user: AdminUser, + pre_registration_id: int, + pre_registration_service: Annotated[PreRegistrationService, Depends()], +) -> None: + pre_registration_service.delete_pre_registration(pre_registration_id) diff --git a/wacruit/src/apps/router.py b/wacruit/src/apps/router.py index fbad7062..eeb391df 100644 --- a/wacruit/src/apps/router.py +++ b/wacruit/src/apps/router.py @@ -6,6 +6,7 @@ from wacruit.src.apps import history from wacruit.src.apps import member from wacruit.src.apps import portfolio +from wacruit.src.apps import pre_registration from wacruit.src.apps import problem from wacruit.src.apps import project from wacruit.src.apps import recruiting @@ -35,3 +36,4 @@ api_router.include_router(review.router) api_router.include_router(faq.router) api_router.include_router(timeline.router) +api_router.include_router(pre_registration.router) diff --git a/wacruit/src/database/__init__.py b/wacruit/src/database/__init__.py index 554a4bc9..5794a84c 100644 --- a/wacruit/src/database/__init__.py +++ b/wacruit/src/database/__init__.py @@ -13,3 +13,4 @@ import wacruit.src.apps.faq.models # isort: skip import wacruit.src.apps.history.models # isort: skip import wacruit.src.apps.timeline.models # isort: skip +import wacruit.src.apps.pre_registration.models # isort: skip diff --git a/wacruit/src/database/migrations/versions/2025_07_18_2104-9dd2ff558a82_add_pre_registration_table.py b/wacruit/src/database/migrations/versions/2025_07_18_2104-9dd2ff558a82_add_pre_registration_table.py new file mode 100644 index 00000000..4c566d16 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_18_2104-9dd2ff558a82_add_pre_registration_table.py @@ -0,0 +1,34 @@ +"""add pre_registration table + +Revision ID: 9dd2ff558a82 +Revises: c9b5e993e78b +Create Date: 2025-07-18 21:04:38.078619 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "9dd2ff558a82" +down_revision = "c9b5e993e78b" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "pre_registration", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("url", sa.String(length=255), nullable=False), + sa.Column("generation", sa.String(length=30), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("pre_registration") + # ### end Alembic commands ### From eab96b61695cc6ab19b6cc72b28acea9fc707b3d Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Sun, 20 Jul 2025 13:35:42 +0900 Subject: [PATCH 20/36] Fix/project image upload (#90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로젝트 이미지 업로드를 위한 api를 구현했습니다 - 프로젝트 이미지가 저장되는 버킷은 기존의 포트폴리오가 저장되는 버킷인 wacruit-protfolio-dev 또는 wacruit-protfolio-prod입니다. - 프로젝트 이미지의 key의 형식은 PROJECT/{project_id}/{file_name}입니다. - 프로젝트 이미지를 업로드할 때는 프로젝트가 먼저 생성되어 있어야 합니다 - api의 세부적인 내용은 노션을 참조해주세요 - project_image 테이블에서 url 대신 s3 객체 키를 저장하도록 수정하였습니다. --- wacruit/src/apps/member/views.py | 6 +- wacruit/src/apps/project/exceptions.py | 5 + wacruit/src/apps/project/models.py | 11 +- wacruit/src/apps/project/repositories.py | 26 ++- wacruit/src/apps/project/schemas.py | 47 ++--- wacruit/src/apps/project/services.py | 174 ++++++++++++++++-- wacruit/src/apps/project/views.py | 55 ++---- wacruit/src/apps/review/views.py | 4 +- ...07_19_2341-a6ed99114b92_fix_project_url.py | 68 +++++++ ...0019-599c54cee280_add_is_uploaded_field.py | 29 +++ 10 files changed, 332 insertions(+), 93 deletions(-) create mode 100644 wacruit/src/database/migrations/versions/2025_07_19_2341-a6ed99114b92_fix_project_url.py create mode 100644 wacruit/src/database/migrations/versions/2025_07_20_0019-599c54cee280_add_is_uploaded_field.py diff --git a/wacruit/src/apps/member/views.py b/wacruit/src/apps/member/views.py index 4ef65a24..bf05b4c0 100644 --- a/wacruit/src/apps/member/views.py +++ b/wacruit/src/apps/member/views.py @@ -1,7 +1,9 @@ +from http import HTTPStatus from typing import Annotated from fastapi import APIRouter from fastapi import Depends +from fastapi import Response from fastapi import Security from fastapi.security import APIKeyHeader @@ -17,13 +19,13 @@ v3_router = APIRouter(prefix="/v3/members", tags=["members"]) -@v3_router.post("") +@v3_router.post("", status_code=HTTPStatus.CREATED) def create_member( admin_user: AdminUser, request: MemberCreateRequest, member_service: Annotated[MemberService, Depends()], ): - return member_service.create_member(request) + member_service.create_member(request) @v3_router.get("/{member_id}") diff --git a/wacruit/src/apps/project/exceptions.py b/wacruit/src/apps/project/exceptions.py index f8811aa5..74529d31 100644 --- a/wacruit/src/apps/project/exceptions.py +++ b/wacruit/src/apps/project/exceptions.py @@ -9,3 +9,8 @@ def __init__(self): class ProjectAlreadyExistsException(WacruitException): def __init__(self): super().__init__(status_code=409, detail="이미 존재하는 프로젝트입니다.") + + +class ProjectImageNotFoundException(WacruitException): + def __init__(self): + super().__init__(status_code=404, detail="존재하지 않는 프로젝트 이미지입니다.") diff --git a/wacruit/src/apps/project/models.py b/wacruit/src/apps/project/models.py index c1ce764c..22a84101 100644 --- a/wacruit/src/apps/project/models.py +++ b/wacruit/src/apps/project/models.py @@ -32,7 +32,7 @@ class Project(DeclarativeBase): urls: Mapped[list["ProjectURL"] | None] = relationship( back_populates="source_project" ) - image_urls: Mapped[list["ProjectImageURL"] | None] = relationship( + images: Mapped[list["ProjectImage"] | None] = relationship( back_populates="source_project" ) @@ -48,11 +48,12 @@ class ProjectURL(DeclarativeBase): source_project: Mapped["Project"] = relationship(back_populates="urls") -class ProjectImageURL(DeclarativeBase): - __tablename__ = "project_image_url" +class ProjectImage(DeclarativeBase): + __tablename__ = "project_image" id: Mapped[intpk] project_id: Mapped[int] = mapped_column(ForeignKey("project.id")) - url: Mapped[str255] + object_key: Mapped[str255] + is_uploaded: Mapped[bool] = mapped_column(default=False) - source_project: Mapped["Project"] = relationship(back_populates="image_urls") + source_project: Mapped["Project"] = relationship(back_populates="images") diff --git a/wacruit/src/apps/project/repositories.py b/wacruit/src/apps/project/repositories.py index 97ffab7c..a32e2650 100644 --- a/wacruit/src/apps/project/repositories.py +++ b/wacruit/src/apps/project/repositories.py @@ -1,9 +1,10 @@ from fastapi import Depends +from sqlalchemy import update from sqlalchemy.orm import joinedload from sqlalchemy.orm import Session from wacruit.src.apps.project.models import Project -from wacruit.src.apps.project.models import ProjectImageURL +from wacruit.src.apps.project.models import ProjectImage from wacruit.src.apps.project.models import ProjectURL from wacruit.src.database.connection import get_db_session from wacruit.src.database.connection import Transaction @@ -23,7 +24,7 @@ def get_project_by_id(self, project_id: int) -> Project | None: self.session.query(Project) .options( joinedload(Project.urls), - joinedload(Project.image_urls), + joinedload(Project.images), ) .filter(Project.id == project_id) .first() @@ -45,3 +46,24 @@ def update_project(self, project: Project) -> Project: self.session.merge(project) return project + + def get_project_image_by_id(self, image_id: int) -> ProjectImage | None: + return ( + self.session.query(ProjectImage).filter(ProjectImage.id == image_id).first() + ) + + def get_project_images_by_project_id(self, project_id: int) -> list[ProjectImage]: + return ( + self.session.query(ProjectImage) + .filter(ProjectImage.project_id == project_id) + .all() + ) + + def update_project_image(self, project_image_id: int) -> None: + with self.transaction: + query = ( + update(ProjectImage) + .where(ProjectImage.id == project_image_id) + .values(is_uploaded=True) + ) + self.session.execute(query) diff --git a/wacruit/src/apps/project/schemas.py b/wacruit/src/apps/project/schemas.py index 6395218a..38a8af09 100644 --- a/wacruit/src/apps/project/schemas.py +++ b/wacruit/src/apps/project/schemas.py @@ -20,7 +20,6 @@ class ProjectCreateRequest(BaseModel): thumbnail_url: str | None = Field(None, max_length=255) project_type: ProjectType is_active: bool - images: list[str] | None urls: list[ProjectLinkDto] | None @@ -31,10 +30,23 @@ class ProjectUpdateRequest(BaseModel): thumbnail_url: str | None = Field(None, max_length=255) project_type: ProjectType | None is_active: bool | None - images: list[str] | None urls: list[ProjectLinkDto] | None +class ProjectImageResponse(OrmModel): + id: int + project_id: int + object_key: str + is_uploaded: bool + + +class PresignedUrlWithIdResponse(BaseModel): + object_name: str + presigned_url: str + fields: dict[str, str] = Field(default={}) + project_image_id: int + + class ProjectDetailResponse(OrmModel): id: int name: str @@ -43,33 +55,9 @@ class ProjectDetailResponse(OrmModel): thumbnail_url: str | None project_type: ProjectType is_active: bool - images: list[str] | None + images: list[PresignedUrlWithIdResponse] | None urls: list[ProjectLinkDto] | None - @classmethod - def from_orm(cls, obj): - urls = None - if obj.urls: - urls = [ - ProjectLinkDto(url_type=url.url_type, url=url.url) for url in obj.urls - ] - - images = None - if obj.image_urls: - images = [img.url for img in obj.image_urls] - - return cls( - id=obj.id, - name=obj.name, - summary=obj.summary, - introduction=obj.introduction, - thumbnail_url=obj.thumbnail_url, - project_type=obj.project_type.name, - is_active=obj.is_active, - images=images, - urls=urls, - ) - class ProjectBriefResponse(OrmModel): id: int @@ -91,5 +79,10 @@ def from_orm(cls, obj): ) +class ProjectImageUploadRequest(BaseModel): + project_id: int + file_name: str + + class ProjectListResponse(OrmModel): projects: list[ProjectBriefResponse] diff --git a/wacruit/src/apps/project/services.py b/wacruit/src/apps/project/services.py index 4b3a59b8..bb8ea180 100644 --- a/wacruit/src/apps/project/services.py +++ b/wacruit/src/apps/project/services.py @@ -2,24 +2,33 @@ from fastapi import Depends -from wacruit.src.apps.common.enums import ProjectType -from wacruit.src.apps.common.exceptions import InvalidProjectTypeException from wacruit.src.apps.common.schemas import ListResponse -from wacruit.src.apps.member.exceptions import MemberAlreadyExistsException -from wacruit.src.apps.member.exceptions import MemberNotFoundException from wacruit.src.apps.member.repositories import MemberRepository +from wacruit.src.apps.portfolio.file.aws.config import s3_config +from wacruit.src.apps.portfolio.file.aws.s3.client import S3Client +from wacruit.src.apps.portfolio.file.aws.s3.method import S3PresignedUrlMethod +from wacruit.src.apps.portfolio.file.aws.s3.utils import generate_presigned_post_url +from wacruit.src.apps.portfolio.file.aws.s3.utils import generate_presigned_url from wacruit.src.apps.project.exceptions import ProjectAlreadyExistsException +from wacruit.src.apps.project.exceptions import ProjectImageNotFoundException from wacruit.src.apps.project.exceptions import ProjectNotFoundException from wacruit.src.apps.project.models import Project -from wacruit.src.apps.project.models import ProjectImageURL +from wacruit.src.apps.project.models import ProjectImage from wacruit.src.apps.project.models import ProjectURL from wacruit.src.apps.project.repositories import ProjectRepository +from wacruit.src.apps.project.schemas import PresignedUrlWithIdResponse from wacruit.src.apps.project.schemas import ProjectBriefResponse from wacruit.src.apps.project.schemas import ProjectCreateRequest from wacruit.src.apps.project.schemas import ProjectDetailResponse +from wacruit.src.apps.project.schemas import ProjectImageResponse from wacruit.src.apps.project.schemas import ProjectLinkDto from wacruit.src.apps.project.schemas import ProjectUpdateRequest +_1_MIN = 60 +_10_MIN = 10 * _1_MIN +_1_MB = 1024 * 1024 +_50_MB = 50 * _1_MB + class ProjectService: def __init__( @@ -27,6 +36,8 @@ def __init__( member_repository: MemberRepository = Depends(), project_repository: ProjectRepository = Depends(), ) -> None: + self._s3_config = s3_config + self._s3_client = S3Client(region_name=self._s3_config.bucket_region) self.member_repository = member_repository self.project_repository = project_repository @@ -46,16 +57,6 @@ def create_project(self, request: ProjectCreateRequest): # 프로젝트를 먼저 생성하여 project_id를 얻음 created_project = self.project_repository.create_project(project) - # 프로젝트가 생성된 후 이미지와 URL 추가 - if request.images: - if created_project.image_urls is None: - created_project.image_urls = [] - for image_url in request.images: - image_obj = ProjectImageURL( - project_id=created_project.id, url=image_url - ) - created_project.image_urls.append(image_obj) - if request.urls: if created_project.urls is None: created_project.urls = [] @@ -67,15 +68,43 @@ def create_project(self, request: ProjectCreateRequest): ) created_project.urls.append(url_obj) - # 이미지와 URL이 추가된 프로젝트를 업데이트 - if request.images or request.urls: + # URL이 추가된 프로젝트를 업데이트 + if request.urls: created_project = self.project_repository.update_project(created_project) def get_project(self, project_id: int) -> ProjectDetailResponse: project = self.project_repository.get_project_by_id(project_id) if not project: raise ProjectNotFoundException - return ProjectDetailResponse.from_orm(project) + images = [] + if project.images is not None: + for image in project.images: + if image.is_uploaded: + presigned_url = self.generate_presigned_url_for_get_image(image.id) + images.append( + PresignedUrlWithIdResponse( + object_name=image.object_key, + presigned_url=presigned_url, + project_image_id=image.id, + ) + ) + + return ProjectDetailResponse( + id=project.id, + name=project.name, + summary=project.summary, + introduction=project.introduction, + thumbnail_url=project.thumbnail_url, + project_type=project.project_type, + is_active=project.is_active, + images=images, + urls=[ + ProjectLinkDto(url_type=url.url_type, url=url.url) + for url in project.urls + ] + if project.urls + else None, + ) def list_projects( self, offset: int = 0, limit: int = 10 @@ -92,9 +121,114 @@ def update_project( project = self.project_repository.get_project_by_id(project_id) if not project: raise ProjectNotFoundException - for key, value in request.dict(exclude_none=True).items(): + + # urls는 별도로 처리 + request_dict = request.dict(exclude_none=True) + urls_data = request_dict.pop("urls", None) + + for key, value in request_dict.items(): setattr(project, key, value) + + if urls_data is not None: + if project.urls is None: + project.urls = [] + for url_dto in urls_data: + url_obj = ProjectURL( + project_id=project.id, + url_type=url_dto["url_type"], + url=url_dto["url"], + ) + project.urls.append(url_obj) + updated_project = self.project_repository.update_project(project) if not updated_project: raise ProjectNotFoundException - return ProjectDetailResponse.from_orm(updated_project) + images = [] + if updated_project.images is not None: + for image in updated_project.images: + if image.is_uploaded: + presigned_url = self.generate_presigned_url_for_get_image(image.id) + images.append( + PresignedUrlWithIdResponse( + object_name=image.object_key, + presigned_url=presigned_url, + project_image_id=image.id, + ) + ) + + return ProjectDetailResponse( + id=updated_project.id, + name=updated_project.name, + summary=updated_project.summary, + introduction=updated_project.introduction, + thumbnail_url=updated_project.thumbnail_url, + project_type=updated_project.project_type, + is_active=updated_project.is_active, + images=images, + urls=[ + ProjectLinkDto(url_type=url.url_type, url=url.url) + for url in updated_project.urls + ] + if updated_project.urls + else None, + ) + + def get_project_object_name(self, project_id: int, file_name: str) -> str: + return f"PROJECT/{project_id}/{file_name}" + + def generate_presigned_url_for_post_image( + self, project_id: int, file_name: str + ) -> PresignedUrlWithIdResponse: + project = self.project_repository.get_project_by_id(project_id) + if not project: + raise ProjectNotFoundException + object_name = self.get_project_object_name(project_id, file_name) + url, fields = generate_presigned_post_url( + s3_client=self._s3_client.client, + s3_bucket=self._s3_config.bucket_name, + s3_object=object_name, + expires_in=_10_MIN, + conditions=[ + ["content-length-range", 0, _50_MB], + ], + ) + project_image = ProjectImage( + project_id=project_id, + object_key=object_name, + ) + if project.images is None: + project.images = [] + project.images.append(project_image) + self.project_repository.update_project(project) + return PresignedUrlWithIdResponse( + object_name=object_name, + presigned_url=url, + fields=fields, + project_image_id=project_image.id, + ) + + def generate_presigned_url_for_get_image(self, file_id: int) -> str: + project_image = self.project_repository.get_project_image_by_id(file_id) + if not project_image or not project_image.is_uploaded: + raise ProjectImageNotFoundException + object_name = project_image.object_key + url = generate_presigned_url( + s3_client=self._s3_client.client, + client_method=S3PresignedUrlMethod.GET, + method_parameters={ + "Bucket": self._s3_config.bucket_name, + "Key": object_name, + }, + expires_in=_10_MIN, + ) + return url + + def register_project_image_info_in_db(self, file_id: int) -> ProjectImageResponse: + project_image = self.project_repository.get_project_image_by_id(file_id) + if not project_image: + raise ProjectImageNotFoundException + self.project_repository.update_project_image( + project_image_id=file_id, + ) + project_image = self.project_repository.get_project_image_by_id(file_id) + return ProjectImageResponse.from_orm(project_image) diff --git a/wacruit/src/apps/project/views.py b/wacruit/src/apps/project/views.py index 9aa1ba5a..ed3dbec7 100644 --- a/wacruit/src/apps/project/views.py +++ b/wacruit/src/apps/project/views.py @@ -5,9 +5,12 @@ from fastapi import Depends from wacruit.src.apps.common.schemas import ListResponse +from wacruit.src.apps.project.schemas import PresignedUrlWithIdResponse from wacruit.src.apps.project.schemas import ProjectBriefResponse from wacruit.src.apps.project.schemas import ProjectCreateRequest from wacruit.src.apps.project.schemas import ProjectDetailResponse +from wacruit.src.apps.project.schemas import ProjectImageResponse +from wacruit.src.apps.project.schemas import ProjectImageUploadRequest from wacruit.src.apps.project.schemas import ProjectUpdateRequest from wacruit.src.apps.project.services import ProjectService from wacruit.src.apps.user.dependencies import AdminUser @@ -21,7 +24,7 @@ def create_project( project_service: Annotated[ProjectService, Depends()], admin_user: AdminUser, ): - return project_service.create_project(request) + project_service.create_project(request) @v3_router.get("/{project_id}") @@ -50,39 +53,21 @@ def update_project( return project_service.update_project(project_id, request) -# @v3_router.post("/{project_id}/members") -# def add_project_member( -# admin_user: AdminUser, -# project_id: int, -# request: ProjectMemberCreateRequest, -# project_service: Annotated[ProjectService, Depends()], -# ): -# return project_service.add_project_member(project_id, request) - - -# @v3_router.get("/{project_id}/members") -# def list_project_members( -# project_id: int, project_service: Annotated[ProjectService, Depends()] -# ) -> ListResponse[ProjectMemberResponse]: -# return project_service.list_project_members(project_id) - - -# @v3_router.patch("/{project_id}/members/{member_id}") -# def update_project_member( -# admin_user: AdminUser, -# project_id: int, -# member_id: int, -# request: ProjectMemberUpdateRequest, -# project_service: Annotated[ProjectService, Depends()], -# ) -> ListResponse[ProjectMemberResponse]: -# return project_service.update_project_member(project_id, member_id, request) +@v3_router.post("/image/upload") +def get_upload_project_image_url( + admin_user: AdminUser, + request: ProjectImageUploadRequest, + project_service: Annotated[ProjectService, Depends()], +) -> PresignedUrlWithIdResponse: + return project_service.generate_presigned_url_for_post_image( + project_id=request.project_id, file_name=request.file_name + ) -# @v3_router.delete("/{project_id}/members/{member_id}") -# def delete_project_member( -# admin_user: AdminUser, -# project_id: int, -# member_id: int, -# project_service: Annotated[ProjectService, Depends()], -# ) -> ListResponse[ProjectMemberResponse]: -# return project_service.delete_project_member(project_id, member_id) +@v3_router.get("/image/check-upload-completed/{file_id}") +def check_upload_project_image_completed( + admin_user: AdminUser, + file_id: int, + project_service: Annotated[ProjectService, Depends()], +) -> ProjectImageResponse: + return project_service.register_project_image_info_in_db(file_id=file_id) diff --git a/wacruit/src/apps/review/views.py b/wacruit/src/apps/review/views.py index 123ee6cb..90e7007d 100644 --- a/wacruit/src/apps/review/views.py +++ b/wacruit/src/apps/review/views.py @@ -19,7 +19,7 @@ def create_review( request: ReviewCreateRequest, review_service: Annotated[ReviewService, Depends()], ): - return review_service.create_review(request) + review_service.create_review(request) @v3_router.get("/{review_id}") @@ -55,4 +55,4 @@ def delete_review( review_id: int, review_service: Annotated[ReviewService, Depends()], ): - return review_service.delete_review(review_id) + review_service.delete_review(review_id) diff --git a/wacruit/src/database/migrations/versions/2025_07_19_2341-a6ed99114b92_fix_project_url.py b/wacruit/src/database/migrations/versions/2025_07_19_2341-a6ed99114b92_fix_project_url.py new file mode 100644 index 00000000..7d40f097 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_19_2341-a6ed99114b92_fix_project_url.py @@ -0,0 +1,68 @@ +"""fix project url + +Revision ID: a6ed99114b92 +Revises: 9dd2ff558a82 +Create Date: 2025-07-19 23:41:02.400446 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "a6ed99114b92" +down_revision = "9dd2ff558a82" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "project_image", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("project_id", sa.Integer(), nullable=False), + sa.Column("object_key", sa.String(length=255), nullable=False), + sa.ForeignKeyConstraint( + ["project_id"], + ["project.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.drop_table("project_image_url") + op.alter_column( + "timeline", + "group", + existing_type=mysql.ENUM("ROOKIES", "PROGRAMMERS", "DESIGNERS"), + type_=sa.Enum("ROOKIE", "PROGRAMMER", "DESIGNER", name="timelinegrouptype"), + existing_nullable=False, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "timeline", + "group", + existing_type=sa.Enum( + "ROOKIE", "PROGRAMMER", "DESIGNER", name="timelinegrouptype" + ), + type_=mysql.ENUM("ROOKIES", "PROGRAMMERS", "DESIGNERS"), + existing_nullable=False, + ) + op.create_table( + "project_image_url", + sa.Column("id", mysql.INTEGER(), autoincrement=True, nullable=False), + sa.Column("project_id", mysql.INTEGER(), autoincrement=False, nullable=False), + sa.Column("url", mysql.VARCHAR(length=255), nullable=False), + sa.ForeignKeyConstraint( + ["project_id"], ["project.id"], name="project_image_url_ibfk_1" + ), + sa.PrimaryKeyConstraint("id"), + mysql_collate="utf8mb4_0900_ai_ci", + mysql_default_charset="utf8mb4", + mysql_engine="InnoDB", + ) + op.drop_table("project_image") + # ### end Alembic commands ### diff --git a/wacruit/src/database/migrations/versions/2025_07_20_0019-599c54cee280_add_is_uploaded_field.py b/wacruit/src/database/migrations/versions/2025_07_20_0019-599c54cee280_add_is_uploaded_field.py new file mode 100644 index 00000000..fb3536e9 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_20_0019-599c54cee280_add_is_uploaded_field.py @@ -0,0 +1,29 @@ +"""add is_uploaded field + +Revision ID: 599c54cee280 +Revises: a6ed99114b92 +Create Date: 2025-07-20 00:19:05.269601 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "599c54cee280" +down_revision = "a6ed99114b92" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "project_image", sa.Column("is_uploaded", sa.Boolean(), nullable=False) + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("project_image", "is_uploaded") + # ### end Alembic commands ### From e57fc0b055a1a507f62736013ae243ebe1388f02 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:14:28 +0900 Subject: [PATCH 21/36] =?UTF-8?q?Chore:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 어드민 페이지에 새로 추가된 api들이 보일 수 있도록 수정하였습니다 --- wacruit/src/admin/formatters.py | 12 ++ wacruit/src/admin/views.py | 215 ++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) diff --git a/wacruit/src/admin/formatters.py b/wacruit/src/admin/formatters.py index 04f1e5a9..3113e0f3 100644 --- a/wacruit/src/admin/formatters.py +++ b/wacruit/src/admin/formatters.py @@ -26,3 +26,15 @@ def formatter(model: type[DeclarativeBase], attribute: str): ) return formatter + + +def member_formatter(model: type[DeclarativeBase], attribute: str) -> str | None: + member = getattr(model, "member") + return member and f"{member.last_name} {member.first_name}" + + +def timeline_category_formatter( + model: type[DeclarativeBase], attribute: str +) -> str | None: + category = getattr(model, "category") + return category and category.title diff --git a/wacruit/src/admin/views.py b/wacruit/src/admin/views.py index 16a2b7dc..8fbe62d0 100644 --- a/wacruit/src/admin/views.py +++ b/wacruit/src/admin/views.py @@ -3,17 +3,30 @@ from sqladmin import ModelView from sqlalchemy import Column +from wacruit.src.admin.formatters import member_formatter from wacruit.src.admin.formatters import recruiting_formatter from wacruit.src.admin.formatters import shorten_column +from wacruit.src.admin.formatters import timeline_category_formatter from wacruit.src.admin.formatters import user_formatter from wacruit.src.apps.announcement.models import Announcement +from wacruit.src.apps.faq.models import FAQ +from wacruit.src.apps.history.models import History +from wacruit.src.apps.member.models import Member +from wacruit.src.apps.pre_registration.models import PreRegistration from wacruit.src.apps.problem.models import CodeSubmission from wacruit.src.apps.problem.models import Problem from wacruit.src.apps.problem.models import Testcase +from wacruit.src.apps.project.models import Project from wacruit.src.apps.recruiting.models import Recruiting from wacruit.src.apps.recruiting.models import RecruitingApplication +from wacruit.src.apps.recruiting_info.models import RecruitingInfo from wacruit.src.apps.resume.models import ResumeQuestion from wacruit.src.apps.resume.models import ResumeSubmission +from wacruit.src.apps.review.models import Review +from wacruit.src.apps.seminar.models import Seminar +from wacruit.src.apps.sponsor.models import Sponsor +from wacruit.src.apps.timeline.models import Timeline +from wacruit.src.apps.timeline.models import TimelineCategory from wacruit.src.apps.user.models import User @@ -84,6 +97,7 @@ class RecruitingAdmin(ModelView, model=Recruiting): Recruiting.to_date, Recruiting.description, Recruiting.short_description, + Recruiting.recruiting_info, ] column_formatters = { @@ -284,6 +298,196 @@ class RecruitingApplicationAdmin(ModelView, model=RecruitingApplication): column_sortable_list = RecruitingApplication.__table__.columns.keys() +class ProjectAdmin(ModelView, model=Project): + column_list = [ + Project.id, + Project.name, + Project.summary, + Project.introduction, + Project.thumbnail_url, + Project.project_type, + Project.is_active, + ] + + form_excluded_columns = [ + Project.images, + Project.urls, + ] + + column_searchable_list = [ + Project.name, + Project.summary, + Project.introduction, + ] + + column_sortable_list = Project.__table__.columns.keys() + + +class MemberAdmin(ModelView, model=Member): + column_list = [ + Member.id, + Member.first_name, + Member.last_name, + Member.introduction, + Member.department, + Member.college, + Member.phone_number, + Member.github_id, + Member.generation, + Member.is_active, + Member.created_at, + Member.position, + ] + + column_searchable_list = [Member.first_name, Member.last_name] + + column_sortable_list = Member.__table__.columns.keys() + + +class ReviewAdmin(ModelView, model=Review): + column_list = [ + Review.id, + Review.member, + Review.title, + Review.content, + ] + + column_formatters = { + Review.member: member_formatter, # type: ignore + } + + column_searchable_list = [ + Review.title, + ] + + column_sortable_list = Review.__table__.columns.keys() + + +class SeminarAdmin(ModelView, model=Seminar): + column_list = [ + Seminar.id, + Seminar.seminar_type, + Seminar.curriculum_info, + Seminar.prerequisite_info, + Seminar.is_active, + ] + + column_searchable_list = [Seminar.seminar_type] + + column_sortable_list = Seminar.__table__.columns.keys() + + +class PreRegistrationAdmin(ModelView, model=PreRegistration): + column_list = [ + PreRegistration.id, + PreRegistration.url, + PreRegistration.generation, + PreRegistration.is_active, + ] + column_searchable_list = [ + PreRegistration.url, + PreRegistration.generation, + ] + + column_sortable_list = PreRegistration.__table__.columns.keys() + + +class SponsorAdmin(ModelView, model=Sponsor): + column_list = [ + Sponsor.id, + Sponsor.name, + Sponsor.amount, + Sponsor.email, + Sponsor.phone_number, + Sponsor.sponsored_date, + ] + + column_searchable_list = [ + Sponsor.name, + ] + + column_sortable_list = Sponsor.__table__.columns.keys() + + +class TimelineAdmin(ModelView, model=Timeline): + column_list = [ + Timeline.id, + Timeline.title, + Timeline.category, + Timeline.group, + Timeline.start_date, + Timeline.end_date, + ] + column_formatters = { + Timeline.category: timeline_category_formatter, # type: ignore + } + column_searchable_list = [ + Timeline.title, + ] + + column_sortable_list = Timeline.__table__.columns.keys() + + +class TimelineCategoryAdmin(ModelView, model=TimelineCategory): + column_list = [ + TimelineCategory.id, + TimelineCategory.title, + ] + + column_searchable_list = [ + TimelineCategory.title, + ] + + column_sortable_list = TimelineCategory.__table__.columns.keys() + + +class FAQAdmin(ModelView, model=FAQ): + column_list = [ + FAQ.id, + FAQ.question, + FAQ.answer, + ] + + column_searchable_list = [ + FAQ.question, + ] + + column_sortable_list = FAQ.__table__.columns.keys() + + +class RecruitingInfoAdmin(ModelView, model=RecruitingInfo): + column_list = [ + RecruitingInfo.id, + RecruitingInfo.recruiting, + RecruitingInfo.info_num, + ] + + column_formatters = { + RecruitingInfo.recruiting: recruiting_formatter # type: ignore + } + + column_searchable_list = [ + "recruiting.name", + RecruitingInfo.info_num, + ] + + column_sortable_list = RecruitingInfo.__table__.columns.keys() + + +class HistoryAdmin(ModelView, model=History): + column_list = [ + History.id, + History.history_key, + History.history_value, + ] + + column_searchable_list = [ + History.history_key, + ] + + column_sortable_list = History.__table__.columns.keys() + + admin_views = [ UserAdmin, RecruitingAdmin, @@ -294,4 +498,15 @@ class RecruitingApplicationAdmin(ModelView, model=RecruitingApplication): ResumeQuestionAdmin, ResumeSubmissionAdmin, RecruitingApplicationAdmin, + ProjectAdmin, + MemberAdmin, + ReviewAdmin, + SeminarAdmin, + PreRegistrationAdmin, + SponsorAdmin, + TimelineAdmin, + TimelineCategoryAdmin, + FAQAdmin, + RecruitingInfoAdmin, + HistoryAdmin, ] From 49eef86d382a1607e2f57607abc2761e25726a34 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Mon, 21 Jul 2025 22:00:52 +0900 Subject: [PATCH 22/36] =?UTF-8?q?Hotfix:=20history=20response=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HistoryResponse를 수정했습니다. - Project introduction과 Review content의 길이가 짧은 것 같아 수정했습니다 --- wacruit/src/apps/history/schemas.py | 8 ++- wacruit/src/apps/history/views.py | 15 ++-- wacruit/src/apps/project/models.py | 6 +- wacruit/src/apps/review/models.py | 3 +- wacruit/src/database/base.py | 2 + ...77282a1_fix_project_introduction_length.py | 68 +++++++++++++++++++ 6 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 wacruit/src/database/migrations/versions/2025_07_21_1804-b306477282a1_fix_project_introduction_length.py diff --git a/wacruit/src/apps/history/schemas.py b/wacruit/src/apps/history/schemas.py index f829fc94..68daf0e1 100644 --- a/wacruit/src/apps/history/schemas.py +++ b/wacruit/src/apps/history/schemas.py @@ -1,10 +1,14 @@ from pydantic import BaseModel +from wacruit.src.apps.common.schemas import OrmModel + class UpdateHistoryRequest(BaseModel): class Config: extra = "allow" -class HistoryResponse(BaseModel): - __root__: dict[str, str] +class HistoryResponse(OrmModel): + id: int + history_key: str + history_value: str diff --git a/wacruit/src/apps/history/views.py b/wacruit/src/apps/history/views.py index 4f00392d..b1c39667 100644 --- a/wacruit/src/apps/history/views.py +++ b/wacruit/src/apps/history/views.py @@ -17,19 +17,16 @@ def update_history( history_service: Annotated[HistoryService, Depends()], update_request: UpdateHistoryRequest, ): - updated_history = history_service.update_history(update_request) - response_dict = {} - for h in updated_history: - response_dict[h.history_key] = h.history_value + updated_history_list = history_service.update_history(update_request) - return HistoryResponse(__root__=response_dict) + return [ + HistoryResponse.from_orm(updated_history) + for updated_history in updated_history_list + ] @v3_router.get("") def get_history(history_service: Annotated[HistoryService, Depends()]): history_list = history_service.get_history() - response_dict = {} - for h in history_list: - response_dict[h.history_key] = h.history_value - return HistoryResponse(__root__=response_dict) + return [HistoryResponse.from_orm(history) for history in history_list] diff --git a/wacruit/src/apps/project/models.py b/wacruit/src/apps/project/models.py index 22a84101..fcf94e03 100644 --- a/wacruit/src/apps/project/models.py +++ b/wacruit/src/apps/project/models.py @@ -13,7 +13,9 @@ from wacruit.src.database.base import intpk from wacruit.src.database.base import str30 from wacruit.src.database.base import str50 +from wacruit.src.database.base import str100 from wacruit.src.database.base import str255 +from wacruit.src.database.base import str1500 if TYPE_CHECKING: from wacruit.src.apps.member.models import Member @@ -24,8 +26,8 @@ class Project(DeclarativeBase): id: Mapped[intpk] name: Mapped[str30] - summary: Mapped[str50 | None] - introduction: Mapped[str255 | None] + summary: Mapped[str100 | None] + introduction: Mapped[str1500 | None] thumbnail_url: Mapped[str255 | None] project_type: Mapped[ProjectType] is_active: Mapped[bool] = mapped_column(default=True) diff --git a/wacruit/src/apps/review/models.py b/wacruit/src/apps/review/models.py index 168bcbff..f4bef5c5 100644 --- a/wacruit/src/apps/review/models.py +++ b/wacruit/src/apps/review/models.py @@ -9,6 +9,7 @@ from wacruit.src.database.base import intpk from wacruit.src.database.base import str30 from wacruit.src.database.base import str255 +from wacruit.src.database.base import str1500 if TYPE_CHECKING: from wacruit.src.apps.member.models import Member @@ -19,7 +20,7 @@ class Review(DeclarativeBase): id: Mapped[intpk] title: Mapped[str30] - content: Mapped[str255] + content: Mapped[str1500] member_id: Mapped[int] = mapped_column(ForeignKey("member.id")) member: Mapped["Member"] = relationship(back_populates="review") diff --git a/wacruit/src/database/base.py b/wacruit/src/database/base.py index d848cb22..c5446f17 100644 --- a/wacruit/src/database/base.py +++ b/wacruit/src/database/base.py @@ -10,4 +10,6 @@ intpk = Annotated[int, mapped_column(primary_key=True, autoincrement=True)] str30 = Annotated[str, mapped_column(String(30))] str50 = Annotated[str, mapped_column(String(50))] +str100 = Annotated[str, mapped_column(String(100))] str255 = Annotated[str, mapped_column(String(255))] +str1500 = Annotated[str, mapped_column(String(1500))] diff --git a/wacruit/src/database/migrations/versions/2025_07_21_1804-b306477282a1_fix_project_introduction_length.py b/wacruit/src/database/migrations/versions/2025_07_21_1804-b306477282a1_fix_project_introduction_length.py new file mode 100644 index 00000000..63337b04 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_07_21_1804-b306477282a1_fix_project_introduction_length.py @@ -0,0 +1,68 @@ +"""fix project introduction length + +Revision ID: b306477282a1 +Revises: 599c54cee280 +Create Date: 2025-07-21 18:04:36.099890 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "b306477282a1" +down_revision = "599c54cee280" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "project", + "summary", + existing_type=mysql.VARCHAR(length=50), + type_=sa.String(length=100), + existing_nullable=True, + ) + op.alter_column( + "project", + "introduction", + existing_type=mysql.VARCHAR(length=255), + type_=sa.String(length=1500), + existing_nullable=True, + ) + op.alter_column( + "review", + "content", + existing_type=mysql.VARCHAR(length=255), + type_=sa.String(length=1500), + existing_nullable=False, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "review", + "content", + existing_type=sa.String(length=1500), + type_=mysql.VARCHAR(length=255), + existing_nullable=False, + ) + op.alter_column( + "project", + "introduction", + existing_type=sa.String(length=1500), + type_=mysql.VARCHAR(length=255), + existing_nullable=True, + ) + op.alter_column( + "project", + "summary", + existing_type=sa.String(length=100), + type_=mysql.VARCHAR(length=50), + existing_nullable=True, + ) + # ### end Alembic commands ### From 85fff390ec651c1395ff5c1405c335a6d385784f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Mon, 21 Jul 2025 23:43:31 +0900 Subject: [PATCH 23/36] =?UTF-8?q?fix:=20=EB=9D=BC=EC=9A=B0=ED=84=B0=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EB=93=B1=EB=A1=9D=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#93)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 라우터가 중복으로 등록되어 생기는 경고 메세지가 나오지 않도록 수정했습니다 --- wacruit/src/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/wacruit/src/main.py b/wacruit/src/main.py index 61f7430d..69000929 100644 --- a/wacruit/src/main.py +++ b/wacruit/src/main.py @@ -1,11 +1,9 @@ import uvicorn from wacruit.src.app import create_app -from wacruit.src.apps.router import api_router # Entrypoint for Docker container. app = create_app() -app.include_router(api_router) def main(): From c67dcd1ada618a80cceff7c3ac6c0ee8479636f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:38:36 +0900 Subject: [PATCH 24/36] fix: recruiting enum type (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Recruiting type을 prod와 맞춰주었습니다. --- wacruit/src/apps/common/enums.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wacruit/src/apps/common/enums.py b/wacruit/src/apps/common/enums.py index 81b2d284..6bcf6d6c 100644 --- a/wacruit/src/apps/common/enums.py +++ b/wacruit/src/apps/common/enums.py @@ -64,9 +64,9 @@ class CodeSubmissionResultStatus(Enum): class RecruitingType(Enum): - ROOKIE = "ROOKIE" - DESIGNER = "DESIGNER" - PROGRAMMER = "PROGRAMMER" + ROOKIE = 1 + DESIGNER = 2 + PROGRAMMER = 3 class RecruitingApplicationStatus(Enum): From 60ff29d7c4fafbe256398176d843955e4e4ea485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:00:23 +0900 Subject: [PATCH 25/36] =?UTF-8?q?fix:=20testcase=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * stdin, expected_output을 MediumText로 수정했습니다. --------- Co-authored-by: deveroskp --- .github/workflows/test.yaml | 6 +- poetry.lock | 275 ++++++++++++++---- pyproject.toml | 1 + wacruit/src/apps/problem/models.py | 5 +- ...-4869ce051b80_change_text_to_mediumtext.py | 54 ++++ 5 files changed, 273 insertions(+), 68 deletions(-) create mode 100644 wacruit/src/database/migrations/versions/2025_08_04_0020-4869ce051b80_change_text_to_mediumtext.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7431bb8f..d3440528 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -30,13 +30,13 @@ jobs: - name: change mysql charset run: mysql -h 127.0.0.1 -P 3307 -u root -p'root-password' -e "ALTER DATABASE testdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci" - - name: Install poetry - run: pipx install poetry - - uses: actions/setup-python@v4 with: python-version: 3.11 + - name: Install poetry + run: pipx install poetry --python python3.11 + - name: Set poetry environment & install dependencies run: | poetry config virtualenvs.in-project true diff --git a/poetry.lock b/poetry.lock index e8687640..ff7499ce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "alembic" @@ -6,6 +6,7 @@ version = "1.11.1" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "alembic-1.11.1-py3-none-any.whl", hash = "sha256:dc871798a601fab38332e38d6ddb38d5e734f60034baeb8e2db5b642fccd8ab8"}, {file = "alembic-1.11.1.tar.gz", hash = "sha256:6a810a6b012c88b33458fceb869aef09ac75d6ace5291915ba7fae44de372c01"}, @@ -25,6 +26,7 @@ version = "3.7.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0"}, {file = "anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce"}, @@ -36,7 +38,7 @@ sniffio = ">=1.1" [package.extras] doc = ["Sphinx (>=6.1.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4) ; python_version < \"3.8\"", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17) ; python_version < \"3.12\" and platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] trio = ["trio (<0.22)"] [[package]] @@ -45,6 +47,7 @@ version = "2.15.5" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.7.2" +groups = ["dev"] files = [ {file = "astroid-2.15.5-py3-none-any.whl", hash = "sha256:078e5212f9885fa85fbb0cf0101978a336190aadea6e13305409d099f71b2324"}, {file = "astroid-2.15.5.tar.gz", hash = "sha256:1039262575027b441137ab4a62a793a9b43defb42c32d5670f38686207cd780f"}, @@ -60,6 +63,7 @@ version = "2.2.1" description = "Annotate AST trees with source code positions" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, @@ -77,6 +81,7 @@ version = "23.1.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, @@ -87,7 +92,7 @@ cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] dev = ["attrs[docs,tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-no-zope = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.1.1) ; platform_python_implementation == \"CPython\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version < \"3.11\"", "pytest-xdist[psutil]"] [[package]] name = "aws-sam-translator" @@ -95,6 +100,7 @@ version = "1.79.0" description = "AWS SAM Translator is a library that transform SAM templates into AWS CloudFormation templates" optional = false python-versions = ">=3.7, <=4.0, !=4.0" +groups = ["main"] files = [ {file = "aws-sam-translator-1.79.0.tar.gz", hash = "sha256:990f3043d00b6fd801b38ad780ecd058c315b7581b2e43fc013c9b6253f876e8"}, {file = "aws_sam_translator-1.79.0-py3-none-any.whl", hash = "sha256:6f0c6bbcebcc98c4f98dc238352a06b0befe7fb72535cca75712fffff4873875"}, @@ -107,7 +113,7 @@ pydantic = ">=1.8,<3" typing-extensions = ">=4.4,<5" [package.extras] -dev = ["black (==23.3.0)", "boto3 (>=1.23,<2)", "boto3-stubs[appconfig,serverlessrepo] (>=1.19.5,<2.dev0)", "coverage (>=5.3,<8)", "dateparser (>=1.1,<2.0)", "importlib-metadata", "mypy (>=1.3.0,<1.4.0)", "parameterized (>=0.7,<1.0)", "pytest (>=6.2,<8)", "pytest-cov (>=2.10,<5)", "pytest-env (>=0.6,<1)", "pytest-rerunfailures (>=9.1,<12)", "pytest-xdist (>=2.5,<4)", "pyyaml (>=6.0,<7.0)", "requests (>=2.28,<3.0)", "ruamel.yaml (==0.17.21)", "ruff (==0.0.284)", "tenacity (>=8.0,<9.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)"] +dev = ["black (==23.3.0)", "boto3 (>=1.23,<2)", "boto3-stubs[appconfig,serverlessrepo] (>=1.19.5,<2.dev0)", "coverage (>=5.3,<8)", "dateparser (>=1.1,<2.0)", "importlib-metadata ; python_version < \"3.8\"", "mypy (>=1.3.0,<1.4.0)", "parameterized (>=0.7,<1.0)", "pytest (>=6.2,<8)", "pytest-cov (>=2.10,<5)", "pytest-env (>=0.6,<1)", "pytest-rerunfailures (>=9.1,<12)", "pytest-xdist (>=2.5,<4)", "pyyaml (>=6.0,<7.0)", "requests (>=2.28,<3.0)", "ruamel.yaml (==0.17.21)", "ruff (==0.0.284)", "tenacity (>=8.0,<9.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)"] [[package]] name = "aws-secretsmanager-caching" @@ -115,6 +121,7 @@ version = "1.1.1.5" description = "Client-side AWS Secrets Manager caching library" optional = false python-versions = ">3.5" +groups = ["main"] files = [ {file = "aws_secretsmanager_caching-1.1.1.5-py3-none-any.whl", hash = "sha256:6afb0233b6ae0183b518138e79b3a53f26432f3a71b03df99801e02e2456adc0"}, {file = "aws_secretsmanager_caching-1.1.1.5.tar.gz", hash = "sha256:5cee2762bb89b72f3e5123feee8e45fbe44ffe163bfca08b28f27b2e2b7772e1"}, @@ -129,6 +136,7 @@ version = "2.12.1" description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "aws-xray-sdk-2.12.1.tar.gz", hash = "sha256:0bbfdbc773cfef4061062ac940b85e408297a2242f120bcdfee2593209b1e432"}, {file = "aws_xray_sdk-2.12.1-py2.py3-none-any.whl", hash = "sha256:f6803832dc08d18cc265e2327a69bfa9ee41c121fac195edc9745d04b7a566c3"}, @@ -144,6 +152,7 @@ version = "4.0.1" description = "Modern password hashing for your software and your servers" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, @@ -178,6 +187,7 @@ version = "23.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, @@ -225,6 +235,7 @@ version = "1.28.3" description = "The AWS SDK for Python" optional = false python-versions = ">= 3.7" +groups = ["main"] files = [ {file = "boto3-1.28.3-py3-none-any.whl", hash = "sha256:34f07b3fc2503240c0f58114cf3afea91295c7996d084991b78a278a56157c48"}, {file = "boto3-1.28.3.tar.gz", hash = "sha256:610369a7f984b58973c097ea649ec81976c04565d39a2d6d3edc280d23b0cb87"}, @@ -244,6 +255,7 @@ version = "1.31.3" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">= 3.7" +groups = ["main"] files = [ {file = "botocore-1.31.3-py3-none-any.whl", hash = "sha256:2aa27c75e62bfcf7900cb85283228fcb7df0fa1bdc2ae76dcee90788dd444934"}, {file = "botocore-1.31.3.tar.gz", hash = "sha256:744ce853cadc7ae87ba42ef6828194ddec97d606dd4d08b4dfe3d96d5001eb0c"}, @@ -263,6 +275,7 @@ version = "2023.5.7" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, @@ -274,6 +287,7 @@ version = "1.16.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, @@ -338,6 +352,7 @@ version = "3.3.1" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.6.1" +groups = ["dev"] files = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, @@ -349,6 +364,7 @@ version = "0.83.1" description = "Checks CloudFormation templates for practices and behaviour that could potentially be improved" optional = false python-versions = ">=3.7, <=4.0, !=4.0" +groups = ["main"] files = [ {file = "cfn-lint-0.83.1.tar.gz", hash = "sha256:9629138f98d83898c7ffc63f67a3106af67f267d6cee7943253f1f813655293d"}, {file = "cfn_lint-0.83.1-py3-none-any.whl", hash = "sha256:e801e9ca384f8c0fbbcd3e342cad5b038d6ca60214b81e70e2710b4dc3339567"}, @@ -372,6 +388,7 @@ version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" +groups = ["main"] files = [ {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, @@ -471,6 +488,7 @@ version = "8.1.3" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -485,10 +503,12 @@ 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" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "cryptography" @@ -496,6 +516,7 @@ version = "41.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, @@ -541,6 +562,7 @@ version = "0.11.0" description = "Python's missing debug print command, and more." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "devtools-0.11.0-py3-none-any.whl", hash = "sha256:22a3b65b58713ccd7e8586feffdefaad57bbdbc6ff76bd68ecf3150a00cf41e3"}, {file = "devtools-0.11.0.tar.gz", hash = "sha256:4a6d987dfab0c7513a514e7faf0a71296d220423470204e76e0752d146dfad62"}, @@ -559,6 +581,7 @@ version = "0.3.6" description = "serialize all of python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "dill-0.3.6-py3-none-any.whl", hash = "sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0"}, {file = "dill-0.3.6.tar.gz", hash = "sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"}, @@ -573,6 +596,7 @@ version = "0.3.6" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, @@ -584,6 +608,7 @@ version = "2.3.0" description = "DNS toolkit" optional = false python-versions = ">=3.7,<4.0" +groups = ["main"] files = [ {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"}, {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"}, @@ -592,7 +617,7 @@ files = [ [package.extras] curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] dnssec = ["cryptography (>=2.6,<40.0)"] -doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] +doh = ["h2 (>=4.1.0) ; python_full_version >= \"3.6.2\"", "httpx (>=0.21.1) ; python_full_version >= \"3.6.2\"", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"] doq = ["aioquic (>=0.9.20)"] idna = ["idna (>=2.1,<4.0)"] trio = ["trio (>=0.14,<0.23)"] @@ -604,6 +629,7 @@ version = "6.1.3" description = "A Python library for the Docker Engine API." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "docker-6.1.3-py3-none-any.whl", hash = "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9"}, {file = "docker-6.1.3.tar.gz", hash = "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20"}, @@ -625,6 +651,7 @@ version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, @@ -643,6 +670,7 @@ version = "2.0.0.post2" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "email_validator-2.0.0.post2-py3-none-any.whl", hash = "sha256:2466ba57cda361fb7309fd3d5a225723c788ca4bbad32a0ebd5373b99730285c"}, {file = "email_validator-2.0.0.post2.tar.gz", hash = "sha256:1ff6e86044200c56ae23595695c54e9614f4a9551e0e393614f764860b3d7900"}, @@ -658,13 +686,14 @@ version = "1.2.0" description = "Get the currently executing AST node of a frame, and other information" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, ] [package.extras] -tests = ["asttokens", "littleutils", "pytest", "rich"] +tests = ["asttokens", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] [[package]] name = "fastapi" @@ -672,6 +701,7 @@ version = "0.95.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "fastapi-0.95.2-py3-none-any.whl", hash = "sha256:d374dbc4ef2ad9b803899bd3360d34c534adc574546e25314ab72c0c4411749f"}, {file = "fastapi-0.95.2.tar.gz", hash = "sha256:4d9d3e8c71c73f11874bcf5e33626258d143252e329a01002f767306c64fb982"}, @@ -693,6 +723,7 @@ version = "3.12.2" description = "A platform independent file lock." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, @@ -708,6 +739,7 @@ version = "3.2.3" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false python-versions = ">=3.6,<4" +groups = ["main"] files = [ {file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"}, {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, @@ -719,12 +751,15 @@ version = "2.0.2" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +groups = ["main"] +markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\"" files = [ {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, + {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, @@ -733,6 +768,7 @@ files = [ {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, + {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, @@ -762,6 +798,7 @@ files = [ {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, + {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, @@ -770,6 +807,7 @@ files = [ {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, + {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, @@ -783,7 +821,7 @@ files = [ ] [package.extras] -docs = ["Sphinx", "docutils (<0.18)"] +docs = ["Sphinx", "docutils (<0.18) ; python_version < \"3\""] test = ["objgraph", "psutil"] [[package]] @@ -792,6 +830,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -803,6 +842,7 @@ version = "0.17.2" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "httpcore-0.17.2-py3-none-any.whl", hash = "sha256:5581b9c12379c4288fe70f43c710d16060c10080617001e6b22a3b6dbcbefd36"}, {file = "httpcore-0.17.2.tar.gz", hash = "sha256:125f8375ab60036db632f34f4b627a9ad085048eef7cb7d2616fea0f739f98af"}, @@ -824,6 +864,7 @@ version = "0.24.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, @@ -836,7 +877,7 @@ idna = "*" sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -847,6 +888,7 @@ version = "2.5.24" description = "File identification library for Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, @@ -861,6 +903,7 @@ version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, @@ -872,6 +915,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -883,6 +927,7 @@ version = "5.12.0" description = "A Python utility / library to sort Python imports." optional = false python-versions = ">=3.8.0" +groups = ["dev"] files = [ {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, @@ -900,6 +945,7 @@ version = "2.1.2" description = "Safely pass data to untrusted environments and back." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, @@ -911,6 +957,7 @@ version = "3.1.2" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, @@ -928,6 +975,7 @@ version = "1.0.1" description = "JSON Matching Expressions" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, @@ -939,6 +987,7 @@ version = "1.2.3" description = "Generate source code for Python classes from a JSON schema." optional = false python-versions = ">= 2.7" +groups = ["main"] files = [ {file = "jschema_to_python-1.2.3-py3-none-any.whl", hash = "sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05"}, {file = "jschema_to_python-1.2.3.tar.gz", hash = "sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91"}, @@ -955,6 +1004,7 @@ version = "2.0.0" description = "Diff JSON and JSON-like structures in Python" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "jsondiff-2.0.0-py3-none-any.whl", hash = "sha256:689841d66273fc88fc79f7d33f4c074774f4f214b6466e3aff0e5adaf889d1e0"}, {file = "jsondiff-2.0.0.tar.gz", hash = "sha256:2795844ef075ec8a2b8d385c4d59f5ea48b08e7180fce3cb2787be0db00b1fb4"}, @@ -966,6 +1016,7 @@ version = "1.33" description = "Apply JSON-Patches (RFC 6902)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +groups = ["main"] files = [ {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, @@ -980,6 +1031,7 @@ version = "3.0.2" description = "Python library for serializing any arbitrary object graph into JSON" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jsonpickle-3.0.2-py3-none-any.whl", hash = "sha256:4a8442d97ca3f77978afa58068768dba7bff2dbabe79a9647bc3cdafd4ef019f"}, {file = "jsonpickle-3.0.2.tar.gz", hash = "sha256:e37abba4bfb3ca4a4647d28bb9f4706436f7b46c8a8333b4a718abafa8e46b37"}, @@ -996,6 +1048,7 @@ version = "2.4" description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +groups = ["main"] files = [ {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, @@ -1007,6 +1060,7 @@ version = "4.19.2" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "jsonschema-4.19.2-py3-none-any.whl", hash = "sha256:eee9e502c788e89cb166d4d37f43084e3b64ab405c795c03d343a4dbc2c810fc"}, {file = "jsonschema-4.19.2.tar.gz", hash = "sha256:c9ff4d7447eed9592c23a12ccee508baf0dd0d59650615e847feb6cdca74f392"}, @@ -1028,6 +1082,7 @@ version = "0.3.1" description = "JSONSchema Spec with object-oriented paths" optional = false python-versions = ">=3.8.0,<4.0.0" +groups = ["main"] files = [ {file = "jsonschema_path-0.3.1-py3-none-any.whl", hash = "sha256:06f01b1848a28963f49a17730e11204d252aa6ff5db4ef84ec77e5ac93cfa831"}, {file = "jsonschema_path-0.3.1.tar.gz", hash = "sha256:07ea584b5c9b41a614b4d011c5575955676f48d0abbfd93d9ea8e933018d716d"}, @@ -1045,6 +1100,7 @@ version = "2023.7.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, @@ -1059,6 +1115,7 @@ version = "1.9" description = "Creates JUnit XML test result documents that can be read by tools such as Jenkins" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "junit-xml-1.9.tar.gz", hash = "sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f"}, {file = "junit_xml-1.9-py2.py3-none-any.whl", hash = "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732"}, @@ -1073,6 +1130,7 @@ version = "1.9.0" description = "A fast and thorough lazy object proxy." optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, @@ -1118,6 +1176,7 @@ version = "0.7.0" description = "Python logging made (stupidly) simple" optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "loguru-0.7.0-py3-none-any.whl", hash = "sha256:b93aa30099fa6860d4727f1b81f8718e965bb96253fa190fab2077aaad6d15d3"}, {file = "loguru-0.7.0.tar.gz", hash = "sha256:1612053ced6ae84d7959dd7d5e431a0532642237ec21f7fd83ac73fe539e03e1"}, @@ -1128,7 +1187,7 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] -dev = ["Sphinx (==5.3.0)", "colorama (==0.4.5)", "colorama (==0.4.6)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v0.990)", "pre-commit (==3.2.1)", "pytest (==6.1.2)", "pytest (==7.2.1)", "pytest-cov (==2.12.1)", "pytest-cov (==4.0.0)", "pytest-mypy-plugins (==1.10.1)", "pytest-mypy-plugins (==1.9.3)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.2.0)", "tox (==3.27.1)", "tox (==4.4.6)"] +dev = ["Sphinx (==5.3.0) ; python_version >= \"3.8\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.2.2) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v0.990) ; python_version >= \"3.7\"", "pre-commit (==3.2.1) ; python_version >= \"3.8\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==7.2.1) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==4.0.0) ; python_version >= \"3.8\"", "pytest-mypy-plugins (==1.10.1) ; python_version >= \"3.8\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "sphinx-autobuild (==2021.3.14) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==1.2.0) ; python_version >= \"3.8\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.4.6) ; python_version >= \"3.8\""] [[package]] name = "mako" @@ -1136,6 +1195,7 @@ version = "1.2.4" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, @@ -1155,6 +1215,7 @@ version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, @@ -1176,6 +1237,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1214,6 +1285,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -1225,6 +1297,7 @@ version = "4.2.7" description = "" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "moto-4.2.7-py2.py3-none-any.whl", hash = "sha256:3e0ef388900448485cd6eff18e9f7fcaa6cf4560b6fb536ba2e2e1278a5ecc59"}, {file = "moto-4.2.7.tar.gz", hash = "sha256:1298006aaa6996b886658eb194cac0e3a5679c9fcce6cb13e741ccc5a7247abb"}, @@ -1288,6 +1361,7 @@ version = "1.3.0" description = "Python library for arbitrary-precision floating-point arithmetic" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, @@ -1296,7 +1370,7 @@ files = [ [package.extras] develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4)"] +gmpy = ["gmpy2 (>=2.1.0a4) ; platform_python_implementation != \"PyPy\""] tests = ["pytest (>=4.6)"] [[package]] @@ -1305,6 +1379,7 @@ version = "0.2.4" description = "Parser for multipart/form-data." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "multipart-0.2.4-py3-none-any.whl", hash = "sha256:5aec990820b8a9e94f9c164fbeb58cf118cfbde2854865b67a9a730edd1fb9d1"}, {file = "multipart-0.2.4.tar.gz", hash = "sha256:06ba205360bc7096fefe618e4f1e9b2cdb890b4f2157053a81f386912a2522cb"}, @@ -1316,6 +1391,7 @@ version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, @@ -1327,6 +1403,7 @@ version = "2.2.4" description = "Python interface to MySQL" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "mysqlclient-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac44777eab0a66c14cb0d38965572f762e193ec2e5c0723bcd11319cc5b693c5"}, {file = "mysqlclient-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:329e4eec086a2336fe3541f1ce095d87a6f169d1cc8ba7b04ac68bcb234c9711"}, @@ -1345,6 +1422,7 @@ version = "3.2.1" description = "Python package for creating and manipulating graphs and networks" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, @@ -1363,6 +1441,7 @@ 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.*" +groups = ["dev"] files = [ {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, @@ -1377,6 +1456,7 @@ version = "0.6.2" description = "OpenAPI schema validation for Python" optional = false python-versions = ">=3.8.0,<4.0.0" +groups = ["main"] files = [ {file = "openapi_schema_validator-0.6.2-py3-none-any.whl", hash = "sha256:c4887c1347c669eb7cded9090f4438b710845cd0f90d1fb9e1b3303fb37339f8"}, {file = "openapi_schema_validator-0.6.2.tar.gz", hash = "sha256:11a95c9c9017912964e3e5f2545a5b11c3814880681fcacfb73b1759bb4f2804"}, @@ -1393,6 +1473,7 @@ version = "0.7.1" description = "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" optional = false python-versions = ">=3.8.0,<4.0.0" +groups = ["main"] files = [ {file = "openapi_spec_validator-0.7.1-py3-none-any.whl", hash = "sha256:3c81825043f24ccbcd2f4b149b11e8231abce5ba84f37065e14ec947d8f4e959"}, {file = "openapi_spec_validator-0.7.1.tar.gz", hash = "sha256:8577b85a8268685da6f8aa30990b83b7960d4d1117e901d451b5d572605e5ec7"}, @@ -1410,6 +1491,7 @@ version = "23.1" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, @@ -1421,6 +1503,7 @@ version = "0.4.3" description = "Object-oriented paths" optional = false python-versions = ">=3.7.0,<4.0.0" +groups = ["main"] files = [ {file = "pathable-0.4.3-py3-none-any.whl", hash = "sha256:cdd7b1f9d7d5c8b8d3315dbf5a86b2596053ae845f056f57d97c0eefff84da14"}, {file = "pathable-0.4.3.tar.gz", hash = "sha256:5c869d315be50776cc8a993f3af43e0c60dc01506b399643f919034ebf4cdcab"}, @@ -1432,6 +1515,7 @@ version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, @@ -1443,6 +1527,7 @@ version = "5.11.1" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" +groups = ["main"] files = [ {file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"}, {file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"}, @@ -1454,6 +1539,7 @@ version = "3.6.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "platformdirs-3.6.0-py3-none-any.whl", hash = "sha256:ffa199e3fbab8365778c4a10e1fbf1b9cd50707de826eb304b50e57ec0cc8d38"}, {file = "platformdirs-3.6.0.tar.gz", hash = "sha256:57e28820ca8094678b807ff529196506d7a21e17156cb1cddb3e74cebce54640"}, @@ -1469,6 +1555,7 @@ version = "1.0.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, @@ -1484,6 +1571,7 @@ version = "3.3.3" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, @@ -1502,6 +1590,7 @@ version = "0.4.1" description = "Pure Python PartiQL Parser" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "py-partiql-parser-0.4.1.tar.gz", hash = "sha256:e0640ee913812bbf5cd126accc0b09eebd0e68df769a9bfbaef9a5e74a0c6d55"}, {file = "py_partiql_parser-0.4.1-py3-none-any.whl", hash = "sha256:6357ec3215f4ceabe4aa1926e4a0f808b9ebc9f7fd438e7f22dbdc3d6efb2eae"}, @@ -1516,6 +1605,7 @@ version = "0.5.0" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] files = [ {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, @@ -1527,6 +1617,7 @@ version = "2.21" description = "C parser in Python" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -1538,6 +1629,7 @@ version = "1.10.9" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pydantic-1.10.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e692dec4a40bfb40ca530e07805b1208c1de071a18d26af4a2a0d79015b352ca"}, {file = "pydantic-1.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c52eb595db83e189419bf337b59154bdcca642ee4b2a09e5d7797e41ace783f"}, @@ -1590,6 +1682,7 @@ version = "2.8.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, @@ -1607,6 +1700,7 @@ version = "2.17.4" description = "python code static checker" optional = false python-versions = ">=3.7.2" +groups = ["dev"] files = [ {file = "pylint-2.17.4-py3-none-any.whl", hash = "sha256:7a1145fb08c251bdb5cca11739722ce64a63db479283d10ce718b2460e54123c"}, {file = "pylint-2.17.4.tar.gz", hash = "sha256:5dcf1d9e19f41f38e4e85d10f511e5b9c35e1aa74251bf95cdd8cb23584e2db1"}, @@ -1631,6 +1725,7 @@ version = "3.1.1" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" +groups = ["main"] files = [ {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, @@ -1645,6 +1740,7 @@ version = "1.1.314" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pyright-1.1.314-py3-none-any.whl", hash = "sha256:5008a2e04b71e35c5f1b78b16adae9d012601197442ae6c798e9bb3456d1eecb"}, {file = "pyright-1.1.314.tar.gz", hash = "sha256:bd104c206fe40eaf5f836efa9027f07cc0efcbc452e6d22dfae36759c5fd28b3"}, @@ -1663,6 +1759,7 @@ version = "7.3.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "pytest-7.3.2-py3-none-any.whl", hash = "sha256:cdcbd012c9312258922f8cd3f1b62a6580fdced17db6014896053d47cddf9295"}, {file = "pytest-7.3.2.tar.gz", hash = "sha256:ee990a3cc55ba808b80795a79944756f315c67c12b56abd3ac993a7b8c17030b"}, @@ -1683,6 +1780,7 @@ version = "3.8.2" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pytest-mock-3.8.2.tar.gz", hash = "sha256:77f03f4554392558700295e05aed0b1096a20d4a60a4f3ddcde58b0c31c8fca2"}, {file = "pytest_mock-3.8.2-py3-none-any.whl", hash = "sha256:8a9e226d6c0ef09fcf20c94eb3405c388af438a90f3e39687f84166da82d5948"}, @@ -1700,6 +1798,7 @@ version = "2.8.2" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -1714,6 +1813,7 @@ version = "1.0.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, @@ -1728,6 +1828,7 @@ version = "3.3.0" description = "JOSE implementation in Python" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, @@ -1750,6 +1851,7 @@ version = "0.0.6" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"}, {file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"}, @@ -1764,6 +1866,8 @@ version = "306" description = "Python for Window Extensions" optional = false python-versions = "*" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, @@ -1783,51 +1887,65 @@ files = [ [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" -files = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] @@ -1836,6 +1954,7 @@ version = "0.30.2" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "referencing-0.30.2-py3-none-any.whl", hash = "sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf"}, {file = "referencing-0.30.2.tar.gz", hash = "sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0"}, @@ -1851,6 +1970,7 @@ version = "2023.10.3" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "regex-2023.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc"}, {file = "regex-2023.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915"}, @@ -1948,6 +2068,7 @@ version = "2.31.0" description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, @@ -1969,6 +2090,7 @@ version = "0.24.0" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "responses-0.24.0-py3-none-any.whl", hash = "sha256:060be153c270c06fa4d22c1ef8865fdef43902eb595204deeef736cddb62d353"}, {file = "responses-0.24.0.tar.gz", hash = "sha256:3df82f7d4dcd3e5f61498181aadb4381f291da25c7506c47fe8cb68ce29203e7"}, @@ -1980,7 +2102,7 @@ requests = ">=2.30.0,<3.0" urllib3 = ">=1.25.10,<3.0" [package.extras] -tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] [[package]] name = "rfc3339-validator" @@ -1988,6 +2110,7 @@ version = "0.1.4" description = "A pure python RFC3339 validator" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] files = [ {file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"}, {file = "rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b"}, @@ -2002,6 +2125,7 @@ version = "0.12.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "rpds_py-0.12.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:c694bee70ece3b232df4678448fdda245fd3b1bb4ba481fb6cd20e13bb784c46"}, {file = "rpds_py-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30e5ce9f501fb1f970e4a59098028cf20676dee64fc496d55c33e04bbbee097d"}, @@ -2110,6 +2234,7 @@ version = "4.9" description = "Pure-Python RSA implementation" optional = false python-versions = ">=3.6,<4" +groups = ["main"] files = [ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, @@ -2124,6 +2249,7 @@ version = "0.6.1" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">= 3.7" +groups = ["main"] files = [ {file = "s3transfer-0.6.1-py3-none-any.whl", hash = "sha256:3c0da2d074bf35d6870ef157158641178a4204a6e689e82546083e31e0311346"}, {file = "s3transfer-0.6.1.tar.gz", hash = "sha256:640bb492711f4c0c0905e1f62b6aaeb771881935ad27884852411f8e9cacbca9"}, @@ -2141,6 +2267,7 @@ version = "1.0.4" description = "Classes implementing the SARIF 2.1.0 object model." optional = false python-versions = ">= 2.7" +groups = ["main"] files = [ {file = "sarif_om-1.0.4-py3-none-any.whl", hash = "sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911"}, {file = "sarif_om-1.0.4.tar.gz", hash = "sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98"}, @@ -2156,6 +2283,7 @@ version = "67.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"}, {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"}, @@ -2163,7 +2291,7 @@ files = [ [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov ; platform_python_implementation != \"PyPy\"", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf", "pytest-ruff ; sys_platform != \"cygwin\"", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -2172,6 +2300,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main", "dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -2183,6 +2312,7 @@ version = "1.3.0" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, @@ -2194,6 +2324,7 @@ version = "0.16.0" description = "SQLAlchemy admin for FastAPI and Starlette" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "sqladmin-0.16.0-py3-none-any.whl", hash = "sha256:57aa3347a81348685938b16d6aa8ac9cd72f29fcf0f7516e834a00ea8b45462b"}, {file = "sqladmin-0.16.0.tar.gz", hash = "sha256:1b3e2062cf19adcd70a46edda30674d8a8e94f3bc6e008befc9037195e00785b"}, @@ -2215,6 +2346,7 @@ version = "2.0.17" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "SQLAlchemy-2.0.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04383f1e3452f6739084184e427e9d5cb4e68ddc765d52157bf5ef30d5eca14f"}, {file = "SQLAlchemy-2.0.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:724355973297bbe547f3eb98b46ade65a67a3d5a6303f17ab59a2dc6fb938943"}, @@ -2260,7 +2392,7 @@ files = [ ] [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\""} +greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} typing-extensions = ">=4.2.0" [package.extras] @@ -2293,6 +2425,7 @@ version = "1.6.1" description = "\"SSE plugin for Starlette\"" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "sse-starlette-1.6.1.tar.gz", hash = "sha256:6208af2bd7d0887c92f1379da14bd1f4db56bd1274cc5d36670c683d2aa1de6a"}, {file = "sse_starlette-1.6.1-py3-none-any.whl", hash = "sha256:d8f18f1c633e355afe61cc5e9c92eea85badcb8b2d56ec8cfb0a006994aa55da"}, @@ -2307,6 +2440,7 @@ version = "3.3.1" description = "SSH public key parser" optional = false python-versions = ">=3" +groups = ["main"] files = [ {file = "sshpubkeys-3.3.1-py2.py3-none-any.whl", hash = "sha256:946f76b8fe86704b0e7c56a00d80294e39bc2305999844f079a217885060b1ac"}, {file = "sshpubkeys-3.3.1.tar.gz", hash = "sha256:3020ed4f8c846849299370fbe98ff4157b0ccc1accec105e07cfa9ae4bb55064"}, @@ -2325,6 +2459,7 @@ version = "0.27.0" description = "The little ASGI library that shines." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, @@ -2342,6 +2477,7 @@ version = "1.12" description = "Computer algebra system (CAS) in Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"}, {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"}, @@ -2356,6 +2492,7 @@ version = "9.0.0" description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, @@ -2371,6 +2508,7 @@ version = "0.11.8" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "tomlkit-0.11.8-py3-none-any.whl", hash = "sha256:8c726c4c202bdb148667835f68d68780b9a003a9ec34167b6c673b38eff2a171"}, {file = "tomlkit-0.11.8.tar.gz", hash = "sha256:9330fc7faa1db67b541b28e62018c17d20be733177d290a13b24c62d1614e0c3"}, @@ -2382,6 +2520,7 @@ version = "4.6.3" description = "Backported and Experimental Type Hints for Python 3.7+" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26"}, {file = "typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"}, @@ -2393,14 +2532,15 @@ version = "1.26.16" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +groups = ["main"] files = [ {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +brotli = ["brotli (>=1.0.9) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -2409,6 +2549,7 @@ version = "0.22.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "uvicorn-0.22.0-py3-none-any.whl", hash = "sha256:e9434d3bbf05f310e762147f769c9f21235ee118ba2d2bf1155a7196448bd996"}, {file = "uvicorn-0.22.0.tar.gz", hash = "sha256:79277ae03db57ce7d9aa0567830bbb51d7a612f54d6e1e3e92da3ef24c2c8ed8"}, @@ -2419,7 +2560,7 @@ 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)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "virtualenv" @@ -2427,6 +2568,7 @@ version = "20.23.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "virtualenv-20.23.1-py3-none-any.whl", hash = "sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419"}, {file = "virtualenv-20.23.1.tar.gz", hash = "sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1"}, @@ -2439,7 +2581,7 @@ platformdirs = ">=3.5.1,<4" [package.extras] docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "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.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6) ; platform_python_implementation == \"PyPy\"", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9) ; platform_python_implementation == \"CPython\""] [[package]] name = "websocket-client" @@ -2447,6 +2589,7 @@ version = "1.6.4" description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "websocket-client-1.6.4.tar.gz", hash = "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df"}, {file = "websocket_client-1.6.4-py3-none-any.whl", hash = "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24"}, @@ -2463,6 +2606,7 @@ version = "3.0.1" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"}, {file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"}, @@ -2480,13 +2624,15 @@ version = "1.1.0" description = "A small Python utility to set file creation time on Windows" optional = false python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, ] [package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [[package]] name = "wrapt" @@ -2494,6 +2640,7 @@ version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, @@ -2578,6 +2725,7 @@ version = "3.1.1" description = "Form validation and rendering for Python web development." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "wtforms-3.1.1-py3-none-any.whl", hash = "sha256:ae7c54b29806c70f7bce8eb9f24afceb10ca5c32af3d9f04f74d2f66ccc5c7e0"}, {file = "wtforms-3.1.1.tar.gz", hash = "sha256:5e51df8af9a60f6beead75efa10975e97768825a82146a65c7cbf5b915990620"}, @@ -2595,12 +2743,13 @@ version = "0.13.0" description = "Makes working with XML feel like you are working with JSON" optional = false python-versions = ">=3.4" +groups = ["main"] files = [ {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, ] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.11" -content-hash = "4355add579af78ebdf3dea846f92eeec750d3915c139ac5a446b80c5054f9394" +content-hash = "2199e546c2f723d2cd2458041ef06bdbb99e1034437a175db9de49a4950af5e2" diff --git a/pyproject.toml b/pyproject.toml index 678eb9a3..fb8ac5ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ pyjwt = "^2.8.0" pytest-mock = "3.8.2" moto = {version = "4.2.7", extras = ["all", "ec2", "s3"]} tenacity = "^9.0.0" +pyyaml = "6.0.2" [tool.poetry.group.dev.dependencies] diff --git a/wacruit/src/apps/problem/models.py b/wacruit/src/apps/problem/models.py index 9e13a842..659cd3d1 100644 --- a/wacruit/src/apps/problem/models.py +++ b/wacruit/src/apps/problem/models.py @@ -8,6 +8,7 @@ from sqlalchemy import Numeric from sqlalchemy import text from sqlalchemy import Text +from sqlalchemy.dialects.mysql import MEDIUMTEXT from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship @@ -107,8 +108,8 @@ class Testcase(DeclarativeBase): problem_id: Mapped[int | None] = mapped_column( ForeignKey("problem.id", ondelete="SET NULL") ) - stdin: Mapped[str] = mapped_column(Text, nullable=False) - expected_output: Mapped[str] = mapped_column(Text, nullable=False) + stdin: Mapped[str] = mapped_column(MEDIUMTEXT, nullable=False) + expected_output: Mapped[str] = mapped_column(MEDIUMTEXT, nullable=False) time_limit: Mapped[Decimal] = mapped_column(Numeric(10, 5)) extra_time: Mapped[Decimal] = mapped_column( Numeric(10, 5), server_default=text("'0.00000'") diff --git a/wacruit/src/database/migrations/versions/2025_08_04_0020-4869ce051b80_change_text_to_mediumtext.py b/wacruit/src/database/migrations/versions/2025_08_04_0020-4869ce051b80_change_text_to_mediumtext.py new file mode 100644 index 00000000..d40faca7 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_08_04_0020-4869ce051b80_change_text_to_mediumtext.py @@ -0,0 +1,54 @@ +"""change text to mediumtext + +Revision ID: 4869ce051b80 +Revises: b306477282a1 +Create Date: 2025-08-04 00:20:25.182483 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "4869ce051b80" +down_revision = "b306477282a1" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "testcase", + "stdin", + existing_type=mysql.TEXT(), + type_=mysql.MEDIUMTEXT(), + existing_nullable=False, + ) + op.alter_column( + "testcase", + "expected_output", + existing_type=mysql.TEXT(), + type_=mysql.MEDIUMTEXT(), + existing_nullable=False, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "testcase", + "expected_output", + existing_type=mysql.MEDIUMTEXT(), + type_=mysql.TEXT(), + existing_nullable=False, + ) + op.alter_column( + "testcase", + "stdin", + existing_type=mysql.MEDIUMTEXT(), + type_=mysql.TEXT(), + existing_nullable=False, + ) + # ### end Alembic commands ### From a1721bbe00d5f547210c35a46a747eec3dbcd329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:23:26 +0900 Subject: [PATCH 26/36] =?UTF-8?q?fix:=20testcase=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * testcase의 stdin, expected_output이 Textarea로 보이도록 수정했습니다. --- wacruit/src/admin/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wacruit/src/admin/views.py b/wacruit/src/admin/views.py index 8fbe62d0..e9c665be 100644 --- a/wacruit/src/admin/views.py +++ b/wacruit/src/admin/views.py @@ -2,6 +2,7 @@ from sqladmin import ModelView from sqlalchemy import Column +from wtforms import TextAreaField from wacruit.src.admin.formatters import member_formatter from wacruit.src.admin.formatters import recruiting_formatter @@ -193,6 +194,8 @@ class TestcaseAdmin(ModelView, model=Testcase): Testcase.expected_output: shorten_column(), # type: ignore } + form_overrides = {"stdin": TextAreaField, "expected_output": TextAreaField} + form_excluded_columns = [ Testcase.submission_results, ] From e07579f5a681fb80dca3d9a442c2d1c1f9be1c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Thu, 7 Aug 2025 16:11:00 +0900 Subject: [PATCH 27/36] =?UTF-8?q?fix:=20submission=20result=20cascade=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20(#97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * testcase를 삭제할 때 해당하는 code submission result도 같이 삭제되도록 수정했습니다. --- wacruit/src/apps/problem/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wacruit/src/apps/problem/models.py b/wacruit/src/apps/problem/models.py index 659cd3d1..9776a46a 100644 --- a/wacruit/src/apps/problem/models.py +++ b/wacruit/src/apps/problem/models.py @@ -120,7 +120,7 @@ class Testcase(DeclarativeBase): problem: Mapped["Problem"] = relationship(back_populates="testcases") submission_results: Mapped[list["CodeSubmissionResult"]] = relationship( - back_populates="testcase" + back_populates="testcase", passive_deletes=True ) def __str__(self) -> str: From b190ef5e0a98359c990b2816570c153cceb3a966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Sun, 17 Aug 2025 13:51:59 +0900 Subject: [PATCH 28/36] =?UTF-8?q?chore:=20timeline=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 타임라인 테스트 코드를 작성했습니다 --- wacruit/src/tests/timeline/conftest.py | 121 ++++++++++++++++++ .../tests/timeline/test_timeline_service.py | 100 +++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 wacruit/src/tests/timeline/conftest.py create mode 100644 wacruit/src/tests/timeline/test_timeline_service.py diff --git a/wacruit/src/tests/timeline/conftest.py b/wacruit/src/tests/timeline/conftest.py new file mode 100644 index 00000000..0109bc82 --- /dev/null +++ b/wacruit/src/tests/timeline/conftest.py @@ -0,0 +1,121 @@ +from datetime import datetime +from typing import List + +import pytest +from sqlalchemy.orm import Session + +from wacruit.src.apps.timeline.models import Timeline +from wacruit.src.apps.timeline.models import TimelineCategory +from wacruit.src.apps.timeline.models import TimelineGroupType +from wacruit.src.apps.timeline.repositories import TimelineRepository +from wacruit.src.apps.timeline.schemas import TimelineCategoryCreateUpdateRequest +from wacruit.src.apps.timeline.schemas import TimelineCreateRequest +from wacruit.src.apps.timeline.schemas import TimelineUpdateRequest +from wacruit.src.apps.timeline.services import TimelineService +from wacruit.src.apps.user.models import User +from wacruit.src.database.connection import Transaction + + +@pytest.fixture +def timeline_repository(db_session: Session): + return TimelineRepository(session=db_session, transaction=Transaction(db_session)) + + +@pytest.fixture +def timeline_service(timeline_repository: TimelineRepository) -> TimelineService: + return TimelineService(timeline_repository=timeline_repository) + + +@pytest.fixture +def timeline_category1(db_session: Session) -> TimelineCategory: + timeline_category = TimelineCategory(title="친목") + db_session.add(timeline_category) + db_session.commit() + return timeline_category + + +@pytest.fixture +def timeline_category2(db_session: Session) -> TimelineCategory: + timeline_category = TimelineCategory(title="세미나") + db_session.add(timeline_category) + db_session.commit() + return timeline_category + + +@pytest.fixture +def created_category_list(db_session: Session) -> List[TimelineCategory]: + category_list = [] + for i in range(0, 5): + timeline_category = TimelineCategory(title=f"카테고리 {i}") + db_session.add(timeline_category) + category_list.append(timeline_category) + db_session.commit() + return category_list + + +@pytest.fixture +def created_timeline( + db_session: Session, timeline_category1: TimelineCategory +) -> Timeline: + timeline = Timeline( + title="Created Timeline", + group=TimelineGroupType.ROOKIE, + category_id=timeline_category1.id, + start_date=datetime(2025, 1, 1), + end_date=datetime(2025, 1, 5), + ) + db_session.add(timeline) + db_session.commit() + return timeline + + +@pytest.fixture +def created_timeline_list( + db_session: Session, timeline_category1: TimelineCategory +) -> List[Timeline]: + timeline_list = [] + + for i in range(0, 5): + timeline = Timeline( + title=f"Created Timeline {i}", + group=TimelineGroupType.ROOKIE, + category_id=timeline_category1.id, + start_date=datetime(2025, 1, 1), + end_date=datetime(2025, 1, 5), + ) + db_session.add(timeline) + timeline_list.append(timeline) + db_session.commit() + return timeline_list + + +@pytest.fixture +def timeline_create_dto(timeline_category1: TimelineCategory) -> TimelineCreateRequest: + return TimelineCreateRequest( + title="New Timeline", + group=TimelineGroupType.ROOKIE, + category_id=timeline_category1.id, + start_date=datetime(2025, 1, 1), + end_date=datetime(2025, 1, 5), + ) + + +@pytest.fixture +def timeline_update_dto(timeline_category2: TimelineCategory) -> TimelineUpdateRequest: + return TimelineUpdateRequest( + title="Updated Timeline", + group=TimelineGroupType.PROGRAMMER, + category_id=timeline_category2.id, + start_date=datetime(2025, 3, 1), + end_date=datetime(2025, 3, 5), + ) + + +@pytest.fixture +def category_create_dto() -> TimelineCategoryCreateUpdateRequest: + return TimelineCategoryCreateUpdateRequest(title="친목") + + +@pytest.fixture +def category_update_dto() -> TimelineCategoryCreateUpdateRequest: + return TimelineCategoryCreateUpdateRequest(title="세미나") diff --git a/wacruit/src/tests/timeline/test_timeline_service.py b/wacruit/src/tests/timeline/test_timeline_service.py new file mode 100644 index 00000000..e6cfa95b --- /dev/null +++ b/wacruit/src/tests/timeline/test_timeline_service.py @@ -0,0 +1,100 @@ +from datetime import datetime +from typing import List + +import pytest + +from wacruit.src.apps.timeline.exceptions import TimelineCategoryNotFoundException +from wacruit.src.apps.timeline.exceptions import TimelineNotFoundException +from wacruit.src.apps.timeline.models import Timeline +from wacruit.src.apps.timeline.models import TimelineCategory +from wacruit.src.apps.timeline.schemas import TimelineGroupType +from wacruit.src.apps.timeline.services import TimelineService + + +def test_create_category(timeline_service: TimelineService, category_create_dto): + response = timeline_service.create_category(category_create_dto) + + assert response.title == "친목" + + +def test_update_category( + timeline_service: TimelineService, + timeline_category1: TimelineCategory, + category_update_dto, +): + response = timeline_service.update_category( + timeline_category1.id, category_update_dto + ) + + assert response.title == "세미나" + + +def test_get_all_categories( + timeline_service: TimelineService, created_category_list: List[TimelineCategory] +): + response = timeline_service.get_all_categories() + category_list = response.items + + assert len(created_category_list) == len(category_list) + for i in range(0, 5): + assert category_list[i].title == f"카테고리 {i}" + + +def test_delete_category(timeline_service: TimelineService, timeline_category1): + created_id = timeline_category1.id + timeline_service.delete_category(created_id) + with pytest.raises(TimelineCategoryNotFoundException): + timeline_service.delete_category(created_id) + + +def test_create_timeline(timeline_service: TimelineService, timeline_create_dto): + response = timeline_service.create_timeline(timeline_create_dto) + + assert response.title == "New Timeline" + assert response.group == TimelineGroupType.ROOKIE + assert response.category.title == "친목" + assert response.start_date == datetime(2025, 1, 1) + assert response.end_date == datetime(2025, 1, 5) + + +def test_update_timeline( + timeline_service: TimelineService, created_timeline: Timeline, timeline_update_dto +): + response = timeline_service.update_timeline( + created_timeline.id, timeline_update_dto + ) + + assert response.title == "Updated Timeline" + assert response.group == TimelineGroupType.PROGRAMMER + assert response.category.title == "세미나" + assert response.start_date == datetime(2025, 3, 1) + assert response.end_date == datetime(2025, 3, 5) + + +def test_get_all_timelines( + timeline_service: TimelineService, created_timeline_list: List[Timeline] +): + response = timeline_service.get_all_timelines(TimelineGroupType.ROOKIE) + timeline_list = response.items + + assert len(timeline_list) == len(created_timeline_list) + for i in range(0, 5): + assert timeline_list[i].title == f"Created Timeline {i}" + assert timeline_list[i].group == TimelineGroupType.ROOKIE + assert timeline_list[i].category.title == "친목" + assert timeline_list[i].start_date == datetime(2025, 1, 1) + assert timeline_list[i].end_date == datetime(2025, 1, 5) + + +def test_delete_timeline_success( + timeline_service: TimelineService, created_timeline: Timeline +): + created_id = created_timeline.id + timeline_service.delete_timeline(created_id) + with pytest.raises(TimelineNotFoundException): + timeline_service.get_timeline_by_id(created_id) + + +def test_delete_timeline_not_found(timeline_service: TimelineService): + with pytest.raises(TimelineNotFoundException): + timeline_service.delete_timeline(999) From 5979069c311688775284e2293e47ba46e02701a6 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Sun, 17 Aug 2025 23:51:40 +0900 Subject: [PATCH 29/36] =?UTF-8?q?Feat:=20pre-registration=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20?= =?UTF-8?q?(#99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pre-registration pytest 코드를 작성했습니다 --- .../src/tests/pre_registration/conftest.py | 96 ++++++++++++++++++ .../test_pre_resgistration_service.py | 97 +++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 wacruit/src/tests/pre_registration/conftest.py create mode 100644 wacruit/src/tests/pre_registration/test_pre_resgistration_service.py diff --git a/wacruit/src/tests/pre_registration/conftest.py b/wacruit/src/tests/pre_registration/conftest.py new file mode 100644 index 00000000..9561ed8c --- /dev/null +++ b/wacruit/src/tests/pre_registration/conftest.py @@ -0,0 +1,96 @@ +from typing import List + +import pytest +from sqlalchemy.orm import Session + +from wacruit.src.apps.pre_registration.models import PreRegistration +from wacruit.src.apps.pre_registration.repositories import PreRegistrationRepository +from wacruit.src.apps.pre_registration.schemas import CreatePreRegistrationRequest +from wacruit.src.apps.pre_registration.schemas import UpdatePreRegistrationRequest +from wacruit.src.apps.pre_registration.services import PreRegistrationService +from wacruit.src.database.connection import Transaction + + +@pytest.fixture +def pre_registration_repository(db_session: Session) -> PreRegistrationRepository: + return PreRegistrationRepository( + session=db_session, transaction=Transaction(db_session) + ) + + +@pytest.fixture +def pre_registration_service( + pre_registration_repository: PreRegistrationRepository, +) -> PreRegistrationService: + return PreRegistrationService( + pre_registration_repository=pre_registration_repository + ) + + +@pytest.fixture +def created_active_pre_registration( + db_session: Session, +): + pre_registration = PreRegistration( + url="https://wafflestudio.com/24_5_pre_registration", + generation="24.5", + is_active=True, + ) + db_session.add(pre_registration) + db_session.commit() + return pre_registration + + +@pytest.fixture +def created_no_active_pre_registration( + db_session: Session, +): + pre_registration = PreRegistration( + url="https://wafflestudio.com/23_5_pre_registration", + generation="23.5", + is_active=False, + ) + db_session.add(pre_registration) + db_session.commit() + return pre_registration + + +@pytest.fixture +def created_pre_registration_list(db_session: Session) -> List[PreRegistration]: + pre_registration_list = [] + for i in range(4): + pre_registration = PreRegistration( + url=f"https://wafflestudio.com/{i}_5_pre_registration", + generation=f"{i + 20}.5", + is_active=False, + ) + db_session.add(pre_registration) + pre_registration_list.append(pre_registration) + + pre_registration = PreRegistration( + url="https://wafflestudio.com/24_5_pre_registration", + generation="24.5", + is_active=True, + ) + pre_registration_list.append(pre_registration) + db_session.add(pre_registration) + db_session.commit() + return pre_registration_list + + +@pytest.fixture +def pre_registration_create_dto() -> CreatePreRegistrationRequest: + return CreatePreRegistrationRequest( + url="https://wafflestudio.com/24_5_pre_registration", + generation="24.5", + is_active=True, + ) + + +@pytest.fixture +def pre_registration_update_dto() -> UpdatePreRegistrationRequest: + return UpdatePreRegistrationRequest( + url="https://wafflestudio.com/23_5_pre_registration", + generation="23.5", + is_active=False, + ) diff --git a/wacruit/src/tests/pre_registration/test_pre_resgistration_service.py b/wacruit/src/tests/pre_registration/test_pre_resgistration_service.py new file mode 100644 index 00000000..bc7ecde8 --- /dev/null +++ b/wacruit/src/tests/pre_registration/test_pre_resgistration_service.py @@ -0,0 +1,97 @@ +from typing import List + +import pytest + +from wacruit.src.apps.pre_registration.exceptions import PreRegistAlreadyExistException +from wacruit.src.apps.pre_registration.exceptions import PreRegistNotActiveException +from wacruit.src.apps.pre_registration.exceptions import PreRegistNotExistException +from wacruit.src.apps.pre_registration.models import PreRegistration +from wacruit.src.apps.pre_registration.schemas import PreRegistrationResponse +from wacruit.src.apps.pre_registration.services import PreRegistrationService + + +def test_create_pre_registration( + pre_registration_service: PreRegistrationService, pre_registration_create_dto +): + PreRegistration = pre_registration_service.create_pre_registration( + pre_registration_create_dto + ) + response = PreRegistrationResponse.from_orm(PreRegistration) + + assert response.url == "https://wafflestudio.com/24_5_pre_registration" + assert response.generation == "24.5" + assert response.is_active is True + + +def test_create_multiple_active_pre_registration( + pre_registration_service: PreRegistrationService, pre_registration_create_dto +): + pre_registration_service.create_pre_registration(pre_registration_create_dto) + with pytest.raises(PreRegistAlreadyExistException): + new_request = pre_registration_create_dto.copy() + pre_registration_service.create_pre_registration(new_request) + + +def test_active_pre_registration( + pre_registration_service: PreRegistrationService, + created_active_pre_registration: PreRegistration, + created_no_active_pre_registration: PreRegistration, +): + response = pre_registration_service.get_active_pre_registration() + + assert response.url == "https://wafflestudio.com/24_5_pre_registration" + assert response.generation == "24.5" + assert response.is_active is True + + +def test_no_active_pre_registration( + pre_registration_service: PreRegistrationService, + created_no_active_pre_registration: PreRegistration, +): + with pytest.raises(PreRegistNotActiveException): + response = pre_registration_service.get_active_pre_registration() + + +def test_get_all_pre_registrations( + pre_registration_service: PreRegistrationService, + created_pre_registration_list: List[PreRegistration], +): + response = pre_registration_service.get_pre_registration() + assert len(response) == len(created_pre_registration_list) + for i in range(5): + assert response[i].generation == created_pre_registration_list[i].generation + assert response[i].url == created_pre_registration_list[i].url + assert response[i].is_active == created_pre_registration_list[i].is_active + + +def test_update_pre_registration( + pre_registration_service: PreRegistrationService, + created_active_pre_registration: PreRegistration, + pre_registration_update_dto, +): + PreRegistration = pre_registration_service.update_pre_registration( + created_active_pre_registration.id, pre_registration_update_dto + ) + + response = PreRegistrationResponse.from_orm(PreRegistration) + + assert response.url == "https://wafflestudio.com/23_5_pre_registration" + assert response.generation == "23.5" + assert response.is_active is False + + +def test_delete_pre_registration_success( + pre_registration_service: PreRegistrationService, + created_active_pre_registration: PreRegistration, +): + pre_registration_service.delete_pre_registration(created_active_pre_registration.id) + + with pytest.raises(PreRegistNotActiveException): + pre_registration_service.get_active_pre_registration() + + +def test_delete_pre_registration_not_exist( + pre_registration_service: PreRegistrationService, +): + with pytest.raises(PreRegistNotExistException): + pre_registration_service.delete_pre_registration(999) From cfd69e8b3f51a86b48bf8b164f84f4eebb2b80d7 Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:31:59 +0900 Subject: [PATCH 30/36] =?UTF-8?q?Feat:=20sponsor=20test=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1=20&=20sponsor=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sponsor 테스트 코드를 작성했습니다 - sponsor api 코드를 수정했습니다 - amount 자료형을 int으로 하고, sponsored_date의 자료형을 Date로 바꿨습니다 - 제가 그 당시에 코드를 대충 짰나 봅니다 ㅠㅠ ## Summary by CodeRabbit - Refactor - Standardized sponsor amount to an integer value. - Sponsored date is now date-only (no time/timezone). - API endpoints now accept numeric sponsor IDs instead of strings. - Tests - Added comprehensive tests for sponsor creation, retrieval, listing, and updates. - Chores - Database schema updated to align column types with the new formats. --- wacruit/src/apps/sponsor/models.py | 8 +- wacruit/src/apps/sponsor/repositories.py | 2 +- wacruit/src/apps/sponsor/services.py | 4 +- wacruit/src/apps/sponsor/views.py | 4 +- ..._fix_sponsor_amount_sponsored_date_type.py | 40 +++++++++ ...42-f1532cef4ac5_fix_sponsored_date_type.py | 40 +++++++++ wacruit/src/tests/sponsor/conftest.py | 89 +++++++++++++++++++ .../src/tests/sponsor/test_sponsor_service.py | 69 ++++++++++++++ 8 files changed, 247 insertions(+), 9 deletions(-) create mode 100644 wacruit/src/database/migrations/versions/2025_08_25_2136-16b579609e97_fix_sponsor_amount_sponsored_date_type.py create mode 100644 wacruit/src/database/migrations/versions/2025_08_25_2142-f1532cef4ac5_fix_sponsored_date_type.py create mode 100644 wacruit/src/tests/sponsor/conftest.py create mode 100644 wacruit/src/tests/sponsor/test_sponsor_service.py diff --git a/wacruit/src/apps/sponsor/models.py b/wacruit/src/apps/sponsor/models.py index a9b823f0..541ea87e 100644 --- a/wacruit/src/apps/sponsor/models.py +++ b/wacruit/src/apps/sponsor/models.py @@ -1,6 +1,6 @@ -from datetime import datetime +from datetime import date -from sqlalchemy import DateTime +from sqlalchemy import Date from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column @@ -14,7 +14,7 @@ class Sponsor(DeclarativeBase): id: Mapped[intpk] name: Mapped[str30] - amount: Mapped[str30] + amount: Mapped[int] email: Mapped[str30 | None] phone_number: Mapped[str30 | None] - sponsored_date: Mapped[datetime] = mapped_column(DateTime(timezone=True)) + sponsored_date: Mapped[date] = mapped_column(Date) diff --git a/wacruit/src/apps/sponsor/repositories.py b/wacruit/src/apps/sponsor/repositories.py index 6594b46c..758bb02c 100644 --- a/wacruit/src/apps/sponsor/repositories.py +++ b/wacruit/src/apps/sponsor/repositories.py @@ -23,7 +23,7 @@ def create_sponsor(self, sponsor: Sponsor): def get_sponsor_by_sponsor_name(self, sponsor_name: str) -> Sponsor | None: return self.session.query(Sponsor).filter(Sponsor.name == sponsor_name).first() - def get_sponsor_by_id(self, sponsor_id: str) -> Sponsor | None: + def get_sponsor_by_id(self, sponsor_id: int) -> Sponsor | None: return self.session.query(Sponsor).filter(Sponsor.id == sponsor_id).first() def get_all_sponsors(self) -> list[Sponsor]: diff --git a/wacruit/src/apps/sponsor/services.py b/wacruit/src/apps/sponsor/services.py index 1f8e18fa..f5ddb85d 100644 --- a/wacruit/src/apps/sponsor/services.py +++ b/wacruit/src/apps/sponsor/services.py @@ -29,7 +29,7 @@ def create_sponsor(self, request: SponsorCreateRequest): ) return self.sponsor_repository.create_sponsor(sponsor) - def get_sponsor_by_id(self, sponsor_id: str) -> SponsorInfoResponse: + def get_sponsor_by_id(self, sponsor_id: int) -> SponsorInfoResponse: sponsor = self.sponsor_repository.get_sponsor_by_id(sponsor_id) if not sponsor: raise SponsorNotFoundException @@ -46,7 +46,7 @@ def get_all_sponsors(self) -> ListResponse[SponsorBriefResponse]: ) def update_sponsor( - self, sponsor_id: str, request: SponsorUpdateRequest + self, sponsor_id: int, request: SponsorUpdateRequest ) -> SponsorInfoResponse: sponsor = self.sponsor_repository.get_sponsor_by_id(sponsor_id) if not sponsor: diff --git a/wacruit/src/apps/sponsor/views.py b/wacruit/src/apps/sponsor/views.py index 48a0c818..5997d4c6 100644 --- a/wacruit/src/apps/sponsor/views.py +++ b/wacruit/src/apps/sponsor/views.py @@ -27,7 +27,7 @@ def create_sponsor( @v3_router.get("/{sponsor_id}") def get_sponsor( admin_user: AdminUser, - sponsor_id: str, + sponsor_id: int, sponsor_service: Annotated[SponsorService, Depends()], ) -> SponsorInfoResponse: return sponsor_service.get_sponsor_by_id(sponsor_id) @@ -43,7 +43,7 @@ def get_all_sponsors( @v3_router.patch("/{sponsor_id}") def update_sponsor( admin_user: AdminUser, - sponsor_id: str, + sponsor_id: int, request: SponsorUpdateRequest, sponsor_service: Annotated[SponsorService, Depends()], ) -> SponsorInfoResponse: diff --git a/wacruit/src/database/migrations/versions/2025_08_25_2136-16b579609e97_fix_sponsor_amount_sponsored_date_type.py b/wacruit/src/database/migrations/versions/2025_08_25_2136-16b579609e97_fix_sponsor_amount_sponsored_date_type.py new file mode 100644 index 00000000..d708f18e --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_08_25_2136-16b579609e97_fix_sponsor_amount_sponsored_date_type.py @@ -0,0 +1,40 @@ +"""fix sponsor amount & sponsored_date type + +Revision ID: 16b579609e97 +Revises: 4869ce051b80 +Create Date: 2025-08-25 21:36:23.655109 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "16b579609e97" +down_revision = "4869ce051b80" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "sponsor", + "amount", + existing_type=mysql.VARCHAR(length=30), + type_=sa.Integer(), + existing_nullable=False, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "sponsor", + "amount", + existing_type=sa.Integer(), + type_=mysql.VARCHAR(length=30), + existing_nullable=False, + ) + # ### end Alembic commands ### diff --git a/wacruit/src/database/migrations/versions/2025_08_25_2142-f1532cef4ac5_fix_sponsored_date_type.py b/wacruit/src/database/migrations/versions/2025_08_25_2142-f1532cef4ac5_fix_sponsored_date_type.py new file mode 100644 index 00000000..a2380c05 --- /dev/null +++ b/wacruit/src/database/migrations/versions/2025_08_25_2142-f1532cef4ac5_fix_sponsored_date_type.py @@ -0,0 +1,40 @@ +"""fix sponsored_date type + +Revision ID: f1532cef4ac5 +Revises: 16b579609e97 +Create Date: 2025-08-25 21:42:00.063456 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = "f1532cef4ac5" +down_revision = "16b579609e97" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "sponsor", + "sponsored_date", + existing_type=mysql.DATETIME(), + type_=sa.Date(), + existing_nullable=False, + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column( + "sponsor", + "sponsored_date", + existing_type=sa.Date(), + type_=mysql.DATETIME(), + existing_nullable=False, + ) + # ### end Alembic commands ### diff --git a/wacruit/src/tests/sponsor/conftest.py b/wacruit/src/tests/sponsor/conftest.py new file mode 100644 index 00000000..7d1b9a45 --- /dev/null +++ b/wacruit/src/tests/sponsor/conftest.py @@ -0,0 +1,89 @@ +import datetime +from typing import List + +from pydantic import EmailStr +import pytest +from sqlalchemy.orm import Session + +from wacruit.src.apps.sponsor.models import Sponsor +from wacruit.src.apps.sponsor.repositories import SponsorRepository +from wacruit.src.apps.sponsor.schemas import SponsorCreateRequest +from wacruit.src.apps.sponsor.schemas import SponsorUpdateRequest +from wacruit.src.apps.sponsor.services import SponsorService +from wacruit.src.database.connection import Transaction + + +@pytest.fixture +def sponsor_repository(db_session: Session) -> SponsorRepository: + return SponsorRepository(session=db_session, transaction=Transaction(db_session)) + + +@pytest.fixture +def sponsor_service(sponsor_repository: SponsorRepository) -> SponsorService: + return SponsorService(sponsor_repository=sponsor_repository) + + +@pytest.fixture +def created_sponsor(db_session: Session) -> Sponsor: + sponsor = Sponsor( + name="김와플", + amount=50000, + email="waffle@snu.ac.kr", + phone_number="010-1234-5678", + sponsored_date=datetime.date(2025, 1, 1), + ) + db_session.add(sponsor) + db_session.commit() + return sponsor + + +@pytest.fixture +def created_sponsor_list(db_session: Session) -> List[Sponsor]: + sponsors = [ + Sponsor( + name="김와플", + amount=50000, + email="waffle1@snu.ac.kr", + phone_number="010-1111-1111", + sponsored_date=datetime.date(2025, 1, 1), + ), + Sponsor( + name="이와플", + amount=200000, + email="waffle2@snu.ac.kr", + phone_number="010-2222-2222", + sponsored_date=datetime.date(2025, 1, 1), + ), + Sponsor( + name="최와플", + amount=300000, + email="waffle3@snu.ac.kr", + phone_number="010-3333-3333", + sponsored_date=datetime.date(2025, 1, 1), + ), + ] + + db_session.add_all(sponsors) + db_session.commit() + + return sponsors + + +@pytest.fixture +def sponsor_create_dto(): + return SponsorCreateRequest( + name="김와플", + amount=100000, + sponsored_date=datetime.date(2025, 1, 1), + email=EmailStr("waffle@snu.ac.kr"), + phone_number="010-1234-5678", + ) + + +@pytest.fixture +def sponsor_update_dto(): + return SponsorUpdateRequest( + amount=1000000, + email=EmailStr("waffle3@snu.ac.kr"), + phone_number="010-2222-3333", + ) diff --git a/wacruit/src/tests/sponsor/test_sponsor_service.py b/wacruit/src/tests/sponsor/test_sponsor_service.py new file mode 100644 index 00000000..28e12cf7 --- /dev/null +++ b/wacruit/src/tests/sponsor/test_sponsor_service.py @@ -0,0 +1,69 @@ +from typing import List + +import pytest + +from wacruit.src.apps.sponsor.exceptions import SponsorAlreadyExistsException +from wacruit.src.apps.sponsor.exceptions import SponsorNotFoundException +from wacruit.src.apps.sponsor.models import Sponsor +from wacruit.src.apps.sponsor.schemas import SponsorCreateRequest +from wacruit.src.apps.sponsor.schemas import SponsorUpdateRequest +from wacruit.src.apps.sponsor.services import SponsorService + + +def test_create_sponsor( + sponsor_service: SponsorService, sponsor_create_dto: SponsorCreateRequest +): + created_sponsor = sponsor_service.create_sponsor(sponsor_create_dto) + + assert created_sponsor.id is not None + assert created_sponsor.name == sponsor_create_dto.name + assert created_sponsor.email == sponsor_create_dto.email + assert created_sponsor.phone_number == sponsor_create_dto.phone_number + assert created_sponsor.sponsored_date == sponsor_create_dto.sponsored_date + + +def test_create_duplicate_sponsor( + sponsor_service: SponsorService, sponsor_create_dto: SponsorCreateRequest +): + sponsor_service.create_sponsor(sponsor_create_dto) + with pytest.raises(SponsorAlreadyExistsException): + sponsor_service.create_sponsor(sponsor_create_dto) + + +def test_get_sponsor_by_existing_id( + sponsor_service: SponsorService, created_sponsor: Sponsor +): + sponsor = sponsor_service.get_sponsor_by_id(created_sponsor.id) + assert sponsor.id == created_sponsor.id + assert sponsor.name == created_sponsor.name + assert sponsor.amount == created_sponsor.amount + assert sponsor.email == created_sponsor.email + assert sponsor.phone_number == created_sponsor.phone_number + assert sponsor.sponsored_date == created_sponsor.sponsored_date + + +def test_get_sponsor_by_non_existing_id(sponsor_service: SponsorService): + with pytest.raises(SponsorNotFoundException): + sponsor_service.get_sponsor_by_id(1234) + + +def test_get_all_sponsors( + sponsor_service: SponsorService, created_sponsor_list: List[Sponsor] +): + sponsors = sponsor_service.get_all_sponsors() + assert len(sponsors.items) == len(created_sponsor_list) + for i in range(3): + assert sponsors.items[i].name == created_sponsor_list[i].name + + +def test_update_sponsor( + sponsor_service: SponsorService, + sponsor_create_dto: SponsorCreateRequest, + sponsor_update_dto: SponsorUpdateRequest, +): + sponsor = sponsor_service.create_sponsor(sponsor_create_dto) + assert sponsor.id is not None + updated_sponsor = sponsor_service.update_sponsor(sponsor.id, sponsor_update_dto) + assert updated_sponsor.amount == sponsor_update_dto.amount + assert updated_sponsor.email == sponsor_update_dto.email + assert updated_sponsor.phone_number == sponsor_update_dto.phone_number From 566fe494da92fbc3026c695d04ab1f6e49c80177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Sun, 31 Aug 2025 14:15:18 +0900 Subject: [PATCH 31/36] =?UTF-8?q?chore:=20=EC=84=B8=EB=AF=B8=EB=82=98=20ap?= =?UTF-8?q?i=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 세미나 API에 대해 테스트 코드를 추가했습니다. ## Summary by CodeRabbit - Tests - Added comprehensive tests for seminar management, covering creation, retrieval (active, all, by ID), and updates to ensure correct behavior and data integrity. - Introduced reusable test fixtures to standardize test setup and improve consistency. - Expanded coverage across active and inactive scenarios to increase reliability. - Chores - No user-facing changes; improvements focus on test robustness and maintainability. --- wacruit/src/tests/seminar/conftest.py | 87 +++++++++++++++++++ .../src/tests/seminar/test_seminar_service.py | 78 +++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 wacruit/src/tests/seminar/conftest.py create mode 100644 wacruit/src/tests/seminar/test_seminar_service.py diff --git a/wacruit/src/tests/seminar/conftest.py b/wacruit/src/tests/seminar/conftest.py new file mode 100644 index 00000000..cba99cea --- /dev/null +++ b/wacruit/src/tests/seminar/conftest.py @@ -0,0 +1,87 @@ +from typing import List + +import pytest +from sqlalchemy.orm import Session + +from wacruit.src.apps.seminar.models import Seminar +from wacruit.src.apps.seminar.models import SeminarType +from wacruit.src.apps.seminar.repositories import SeminarRepository +from wacruit.src.apps.seminar.schemas import CreateSeminarRequest +from wacruit.src.apps.seminar.schemas import UpdateSeminarRequest +from wacruit.src.apps.seminar.services import SeminarService +from wacruit.src.database.connection import Transaction + + +@pytest.fixture +def created_active_seminar(db_session: Session) -> Seminar: + seminar = Seminar( + seminar_type=SeminarType("FASTAPI"), + curriculum_info="FastAPI curriculum", + prerequisite_info="Python", + is_active=True, + ) + db_session.add(seminar) + db_session.commit() + + return seminar + + +@pytest.fixture +def created_not_active_seminar(db_session: Session) -> Seminar: + seminar = Seminar( + seminar_type=SeminarType("SPRING"), + curriculum_info="Spring curriculum", + prerequisite_info="Kotlin", + is_active=False, + ) + db_session.add(seminar) + db_session.commit() + + return seminar + + +@pytest.fixture +def created_seminars(db_session: Session) -> List[Seminar]: + seminar_list = [] + for i in range(5): + seminar = Seminar( + seminar_type=SeminarType("FASTAPI"), + curriculum_info=f"curriculum {i}", + prerequisite_info=f"{i}", + is_active=True, + ) + db_session.add(seminar) + seminar_list.append(seminar) + db_session.commit() + + return seminar_list + + +@pytest.fixture +def create_seminar_dto() -> CreateSeminarRequest: + return CreateSeminarRequest( + seminar_type=SeminarType("FASTAPI"), + curriculum_info="New curriculum", + prerequisite_info="Python", + is_active=True, + ) + + +@pytest.fixture +def update_seminar_dto() -> UpdateSeminarRequest: + return UpdateSeminarRequest( + seminar_type=SeminarType("SPRING"), + curriculum_info="Updated curriculum", + prerequisite_info="Kotlin", + is_active=False, + ) + + +@pytest.fixture +def seminar_service(seminar_repository: SeminarRepository) -> SeminarService: + return SeminarService(seminar_repository=seminar_repository) + + +@pytest.fixture +def seminar_repository(db_session: Session) -> SeminarRepository: + return SeminarRepository(session=db_session, transaction=Transaction(db_session)) diff --git a/wacruit/src/tests/seminar/test_seminar_service.py b/wacruit/src/tests/seminar/test_seminar_service.py new file mode 100644 index 00000000..942b7ab6 --- /dev/null +++ b/wacruit/src/tests/seminar/test_seminar_service.py @@ -0,0 +1,78 @@ +from typing import List + +import pytest + +from wacruit.src.apps.seminar.models import Seminar +from wacruit.src.apps.seminar.models import SeminarType +from wacruit.src.apps.seminar.schemas import CreateSeminarRequest +from wacruit.src.apps.seminar.schemas import UpdateSeminarRequest +from wacruit.src.apps.seminar.services import SeminarService + + +def test_create_seminar( + create_seminar_dto: CreateSeminarRequest, seminar_service: SeminarService +): + res = seminar_service.create_seminar(create_seminar_dto) + + assert res.seminar_type == create_seminar_dto.seminar_type + assert res.curriculum_info == create_seminar_dto.curriculum_info + assert res.prerequisite_info == create_seminar_dto.prerequisite_info + assert res.is_active == create_seminar_dto.is_active + + +def test_get_active_seminar( + created_active_seminar: Seminar, + created_not_active_seminar: Seminar, + seminar_service: SeminarService, +): + res = seminar_service.get_active_seminar() + + assert len(res) == 1 + assert res[0].id == created_active_seminar.id + assert res[0].seminar_type == created_active_seminar.seminar_type + assert res[0].curriculum_info == created_active_seminar.curriculum_info + assert res[0].prerequisite_info == created_active_seminar.prerequisite_info + assert res[0].is_active == created_active_seminar.is_active + + +def test_get_all_seminars( + created_seminars: List[Seminar], seminar_service: SeminarService +): + created_size = len(created_seminars) + res = seminar_service.get_all_seminars() + + assert len(res) == created_size + + for i in range(created_size): + assert res[i].id == created_seminars[i].id + assert res[i].seminar_type == created_seminars[i].seminar_type + assert res[i].curriculum_info == created_seminars[i].curriculum_info + assert res[i].prerequisite_info == created_seminars[i].prerequisite_info + assert res[i].is_active == created_seminars[i].is_active + + +def test_get_seminar_by_id( + seminar_service: SeminarService, created_active_seminar: Seminar +): + created_id = created_active_seminar.id + res = seminar_service.get_seminar_by_id(created_id) + + assert res.id == created_id + assert res.seminar_type == created_active_seminar.seminar_type + assert res.curriculum_info == created_active_seminar.curriculum_info + assert res.prerequisite_info == created_active_seminar.prerequisite_info + assert res.is_active == created_active_seminar.is_active + + +def test_update_seminar( + seminar_service: SeminarService, + created_active_seminar: Seminar, + update_seminar_dto: UpdateSeminarRequest, +): + created_id = created_active_seminar.id + res = seminar_service.update_seminar(created_id, update_seminar_dto) + + assert res.seminar_type == update_seminar_dto.seminar_type + assert res.curriculum_info == update_seminar_dto.curriculum_info + assert res.prerequisite_info == update_seminar_dto.prerequisite_info + assert res.is_active == update_seminar_dto.is_active From b7b996785756cc0491b09b9bb2e40332fec1c44b Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:08:21 +0900 Subject: [PATCH 32/36] =?UTF-8?q?Feat:=20faq=20api=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84=20(#102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - faq 테스트 코드를 작성했습니다 - faq api에서 QuestionNowFoundException를 QuestionNotFoundException로 바꿨습니다 - faq 생성 api에서 Service 레이어에 request를 넘겨주는 것으로 수정했습니다 ## Summary by CodeRabbit * Bug Fixes * Ensured FAQ “question not found” responses consistently return 404 with a clear message. * Refactor * Streamlined FAQ creation flow to improve consistency and maintainability. * Tests * Added comprehensive tests covering FAQ create, read, update, and delete scenarios. * Updated sponsor tests to support and verify partial updates. --- wacruit/src/apps/faq/exceptions.py | 2 +- wacruit/src/apps/faq/services.py | 8 ++- wacruit/src/apps/faq/views.py | 4 +- wacruit/src/tests/faq/conftest.py | 64 +++++++++++++++++++ wacruit/src/tests/faq/test_faq_service.py | 60 +++++++++++++++++ .../src/tests/sponsor/test_sponsor_service.py | 9 ++- 6 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 wacruit/src/tests/faq/conftest.py create mode 100644 wacruit/src/tests/faq/test_faq_service.py diff --git a/wacruit/src/apps/faq/exceptions.py b/wacruit/src/apps/faq/exceptions.py index 7d958701..98edd911 100644 --- a/wacruit/src/apps/faq/exceptions.py +++ b/wacruit/src/apps/faq/exceptions.py @@ -1,6 +1,6 @@ from wacruit.src.apps.common.exceptions import WacruitException -class QuestionNowFoundException(WacruitException): +class QuestionNotFoundException(WacruitException): def __init__(self): super().__init__(status_code=404, detail="존재하지 않는 질문입니다.") diff --git a/wacruit/src/apps/faq/services.py b/wacruit/src/apps/faq/services.py index 46053525..98a97409 100644 --- a/wacruit/src/apps/faq/services.py +++ b/wacruit/src/apps/faq/services.py @@ -2,9 +2,10 @@ from fastapi import Depends -from wacruit.src.apps.faq.exceptions import QuestionNowFoundException +from wacruit.src.apps.faq.exceptions import QuestionNotFoundException from wacruit.src.apps.faq.models import FAQ from wacruit.src.apps.faq.repositories import QuestionRepository +from wacruit.src.apps.faq.schemas import CreateQuestionRequest from wacruit.src.apps.faq.schemas import UpdateQuestionRequest @@ -15,10 +16,11 @@ def __init__(self, question_repository: Annotated[QuestionRepository, Depends()] def get_question_by_id(self, questions_id: int) -> FAQ: question = self.question_repository.get_question_by_id(questions_id) if question is None: - raise QuestionNowFoundException + raise QuestionNotFoundException return question - def create_question(self, question: FAQ) -> FAQ: + def create_question(self, request: CreateQuestionRequest) -> FAQ: + question = FAQ(question=request.question, answer=request.answer) return self.question_repository.create_questions(question) def get_questions(self) -> list[FAQ]: diff --git a/wacruit/src/apps/faq/views.py b/wacruit/src/apps/faq/views.py index 470aa152..3b6ee76e 100644 --- a/wacruit/src/apps/faq/views.py +++ b/wacruit/src/apps/faq/views.py @@ -34,9 +34,7 @@ def create_question( request: CreateQuestionRequest, question_service: Annotated[QuestionService, Depends()], ) -> QuestionResponse: - res = question_service.create_question( - FAQ(question=request.question, answer=request.answer) - ) + res = question_service.create_question(request) return QuestionResponse.from_orm(res) diff --git a/wacruit/src/tests/faq/conftest.py b/wacruit/src/tests/faq/conftest.py new file mode 100644 index 00000000..53d7726d --- /dev/null +++ b/wacruit/src/tests/faq/conftest.py @@ -0,0 +1,64 @@ +from typing import List + +import pytest +from sqlalchemy.orm import Session + +from wacruit.src.apps.faq.models import FAQ +from wacruit.src.apps.faq.repositories import QuestionRepository +from wacruit.src.apps.faq.schemas import CreateQuestionRequest +from wacruit.src.apps.faq.schemas import UpdateQuestionRequest +from wacruit.src.apps.faq.services import QuestionService +from wacruit.src.database.connection import Transaction + + +@pytest.fixture +def created_question(db_session: Session) -> FAQ: + question = FAQ( + question="개발 경험이 없는데 지원 가능한가요?", + answer="개발을 처음 접하시는 분들도 물론 준회원(Rookies)으로 지원 가능합니다! \ + 와플스튜디오에서 제공하는 세미나를 통해 지식과 협업 경험을 쌓으실 수 있습니다.", + ) + db_session.add(question) + db_session.commit() + return question + + +@pytest.fixture +def created_questions(db_session: Session) -> List[FAQ]: + question1 = FAQ( + question="개발 경험이 없는데 지원 가능한가요?", + answer="개발을 처음 접하시는 분들도 물론 준회원(Rookies)으로 지원 가능합니다! \ + 와플스튜디오에서 제공하는 세미나를 통해 지식과 협업 경험을 쌓으실 수 있습니다.", + ) + question2 = FAQ( + question="저학번/고학번/졸업생인데 지원해도 되나요?", + answer="와플스튜디오는 서울대학교에 재학중이거나 졸업했던 누구나, 나이 제한 없이 가입 가능합니다. \ + 다양한 학번으로 구성되어 있어 학번 및 졸업 여부와 상관 없이 지원 가능하니 부담 가지지 말고 지원해주세요!", + ) + db_session.add_all([question1, question2]) + db_session.commit() + return [question1, question2] + + +@pytest.fixture +def create_question_dto() -> CreateQuestionRequest: + return CreateQuestionRequest( + question="개발 경험이 없는데 지원 가능한가요?", + answer="개발을 처음 접하시는 분들도 물론 준회원(Rookies)으로 지원 가능합니다! \ + 와플스튜디오에서 제공하는 세미나를 통해 지식과 협업 경험을 쌓으실 수 있습니다.", + ) + + +@pytest.fixture +def update_question_dto() -> UpdateQuestionRequest: + return UpdateQuestionRequest(answer="답변 수정") + + +@pytest.fixture +def question_service(question_repository: QuestionRepository) -> QuestionService: + return QuestionService(question_repository=question_repository) + + +@pytest.fixture +def question_repository(db_session: Session) -> QuestionRepository: + return QuestionRepository(session=db_session, transaction=Transaction(db_session)) diff --git a/wacruit/src/tests/faq/test_faq_service.py b/wacruit/src/tests/faq/test_faq_service.py new file mode 100644 index 00000000..0d025ea7 --- /dev/null +++ b/wacruit/src/tests/faq/test_faq_service.py @@ -0,0 +1,60 @@ +from typing import List + +import pytest + +from wacruit.src.apps.faq.exceptions import QuestionNotFoundException +from wacruit.src.apps.faq.models import FAQ +from wacruit.src.apps.faq.schemas import CreateQuestionRequest +from wacruit.src.apps.faq.schemas import UpdateQuestionRequest +from wacruit.src.apps.faq.services import QuestionService + + +def test_create_question( + question_service: QuestionService, create_question_dto: CreateQuestionRequest +): + created_question = question_service.create_question(create_question_dto) + assert created_question.id is not None + assert created_question.question == create_question_dto.question + assert created_question.answer == create_question_dto.answer + + +def test_update_question( + question_service: QuestionService, + created_question: FAQ, + update_question_dto: UpdateQuestionRequest, +): + question_id = created_question.id + updated_question = question_service.update_question( + question_id, update_question_dto + ) + assert updated_question.id == question_id + if update_question_dto.question is not None: + assert updated_question.question == update_question_dto.question + if update_question_dto.answer is not None: + assert updated_question.answer == update_question_dto.answer + + +def test_get_questions(question_service: QuestionService, created_questions: list[FAQ]): + questions = question_service.get_questions() + assert len(questions) == len(created_questions) + + for got, exp in zip(questions, created_questions): + assert got.id == exp.id + assert got.question == exp.question + assert got.answer == exp.answer + + +def test_delete_question(question_service: QuestionService, created_question: FAQ): + question_id = created_question.id + question_service.delete_question(question_id) + with pytest.raises(QuestionNotFoundException): + question_service.get_question_by_id(question_id) + + +def test_delete_question_twice( + question_service: QuestionService, created_question: FAQ +): + question_id = created_question.id + question_service.delete_question(question_id) + with pytest.raises(QuestionNotFoundException): + question_service.delete_question(question_id) diff --git a/wacruit/src/tests/sponsor/test_sponsor_service.py b/wacruit/src/tests/sponsor/test_sponsor_service.py index 28e12cf7..6bfbb71f 100644 --- a/wacruit/src/tests/sponsor/test_sponsor_service.py +++ b/wacruit/src/tests/sponsor/test_sponsor_service.py @@ -64,6 +64,9 @@ def test_update_sponsor( sponsor = sponsor_service.create_sponsor(sponsor_create_dto) assert sponsor.id is not None updated_sponsor = sponsor_service.update_sponsor(sponsor.id, sponsor_update_dto) - assert updated_sponsor.amount == sponsor_update_dto.amount - assert updated_sponsor.email == sponsor_update_dto.email - assert updated_sponsor.phone_number == sponsor_update_dto.phone_number + if sponsor_update_dto.email is not None: + assert updated_sponsor.email == sponsor_update_dto.email + if sponsor_update_dto.amount is not None: + assert updated_sponsor.amount == sponsor_update_dto.amount + if sponsor_update_dto.phone_number is not None: + assert updated_sponsor.phone_number == sponsor_update_dto.phone_number From 47dcc458ebeeab82d2eafbe32793a73052c801e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Thu, 4 Sep 2025 22:59:35 +0900 Subject: [PATCH 33/36] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20yaml=20=EC=B6=94=EA=B0=80=20(#103)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * coderabbit yaml 파일을 추가했습니다. --- .coderabbit.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..2e3beeb7 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,11 @@ +language: "ko-KR" + +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: false + poem: false + review_status: true + auto_review: + enabled: true + drafts: false From 664238edfab2b665d018118aeb0930c101ef388d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Sun, 7 Sep 2025 18:19:18 +0900 Subject: [PATCH 34/36] =?UTF-8?q?feat:=20arm64=20=EC=A0=84=ED=99=98=20(#10?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy-dev.yaml | 2 +- .github/workflows/deploy-prod.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-dev.yaml b/.github/workflows/deploy-dev.yaml index 945d95d5..d6aba15d 100644 --- a/.github/workflows/deploy-dev.yaml +++ b/.github/workflows/deploy-dev.yaml @@ -7,7 +7,7 @@ on: jobs: deploy: name: deploy dev - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm env: IMAGE_TAG: ${{ github.run_number }} diff --git a/.github/workflows/deploy-prod.yaml b/.github/workflows/deploy-prod.yaml index 4f48c5a4..869d9636 100644 --- a/.github/workflows/deploy-prod.yaml +++ b/.github/workflows/deploy-prod.yaml @@ -7,7 +7,7 @@ on: jobs: deploy: name: deploy prod - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm env: IMAGE_TAG: ${{ github.run_number }} From c33697a716ce528857814243b8dc65ddaf5a6c6b Mon Sep 17 00:00:00 2001 From: Kyeongpyo Lee <70705786+deveroskp@users.noreply.github.com> Date: Tue, 9 Sep 2025 01:26:49 +0900 Subject: [PATCH 35/36] =?UTF-8?q?Fix:=20Review=20Response=EC=97=90=20?= =?UTF-8?q?=EB=A9=A4=EB=B2=84=20=EA=B8=B0=EC=88=98=20=ED=8F=AC=ED=95=A8=20?= =?UTF-8?q?(#105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - review를 불러오는 api에서 멤버의 기수를 불러오도록 수정하였습니다. --- wacruit/src/apps/review/schemas.py | 1 + wacruit/src/apps/review/services.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/wacruit/src/apps/review/schemas.py b/wacruit/src/apps/review/schemas.py index 0bf4e755..cf60f523 100644 --- a/wacruit/src/apps/review/schemas.py +++ b/wacruit/src/apps/review/schemas.py @@ -16,6 +16,7 @@ class ReviewResponse(BaseModel): member_id: int member_first_name: str member_last_name: str + member_generation: str member_position: Position | None is_active: bool diff --git a/wacruit/src/apps/review/services.py b/wacruit/src/apps/review/services.py index 3b8c574e..b1e8e348 100644 --- a/wacruit/src/apps/review/services.py +++ b/wacruit/src/apps/review/services.py @@ -44,6 +44,7 @@ def get_review(self, review_id: int): member_id=review.member_id, member_first_name=writer.first_name, member_last_name=writer.last_name, + member_generation=writer.generation, member_position=writer.position if writer.position else None, is_active=writer.is_active, ) @@ -62,6 +63,7 @@ def get_reviews( member_id=review.member_id, member_first_name=review.member.first_name, member_last_name=review.member.last_name, + member_generation=review.member.generation, member_position=review.member.position if review.member.position else None, @@ -101,6 +103,7 @@ def update_review( member_id=updated_review.member_id, member_first_name=updated_review.member.first_name, member_last_name=updated_review.member.last_name, + member_generation=updated_review.member.generation, member_position=updated_review.member.position if updated_review.member.position else None, From bfa442165ebb469ab6583d3e1b03cce67dd1e794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=EC=9E=AC=EC=98=81?= <152019282+MunJaeyoung@users.noreply.github.com> Date: Tue, 9 Sep 2025 21:19:25 +0900 Subject: [PATCH 36/36] =?UTF-8?q?fix:=20merge=20conflict=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * develop -> main 으로 갈 때 충돌이 일어나서 수정했습니다. --------- Co-authored-by: MINKYU LEE Co-authored-by: fivessun-ridi --- wacruit/src/apps/problem/services_v2.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/wacruit/src/apps/problem/services_v2.py b/wacruit/src/apps/problem/services_v2.py index 30f27e84..09a9bc6f 100644 --- a/wacruit/src/apps/problem/services_v2.py +++ b/wacruit/src/apps/problem/services_v2.py @@ -148,12 +148,11 @@ def merge_status( response = await self.hodu_api_repository.submit(hodu_request) if isinstance(response, HoduSubmitResponse): submission_result_status = response.status.to_submission_result_status() - if ( - submission_result_status != CodeSubmissionResultStatus.CORRECT - and submission_result_status - != CodeSubmissionResultStatus.INTERNAL_SERVER_ERROR + if submission_result_status not in ( + CodeSubmissionResultStatus.CORRECT, + CodeSubmissionResultStatus.INTERNAL_SERVER_ERROR, ): - print(f"############################################") + print("############################################") print(f"hodu_request: {hodu_request.json()}") print(f"status: {response.status}") print(f"stdout: {response.fields.stdout}")