From 4f7f037f16d5e49c5265c6fc0d36081da9a93afe Mon Sep 17 00:00:00 2001 From: guineaaaa <165776804+guineaaaa@users.noreply.github.com> Date: Mon, 12 May 2025 17:42:17 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[FEAT]=20Spring=20Entity=EC=99=80=20?= =?UTF-8?q?=EC=9D=BC=EC=B9=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/db.py | 20 +++++-- app/db_models/authentication_code.py | 22 ++++++++ app/db_models/base_entity.py | 10 ++++ app/db_models/bookmark.py | 21 +++++++ .../{education.py => education_info.py} | 9 +-- app/db_models/{policy.py => policy_info.py} | 7 ++- app/db_models/refresh_token.py | 16 ++++++ app/db_models/resume.py | 40 +++++++++---- app/db_models/user.py | 56 +++++++++++++------ app/main.py | 2 +- app/services/education_service.py | 4 +- app/services/policy_service.py | 2 +- 12 files changed, 164 insertions(+), 45 deletions(-) create mode 100644 app/db_models/authentication_code.py create mode 100644 app/db_models/base_entity.py create mode 100644 app/db_models/bookmark.py rename app/db_models/{education.py => education_info.py} (82%) rename app/db_models/{policy.py => policy_info.py} (91%) create mode 100644 app/db_models/refresh_token.py diff --git a/app/core/db.py b/app/core/db.py index a1f533e..78a1c7b 100644 --- a/app/core/db.py +++ b/app/core/db.py @@ -1,17 +1,27 @@ -from sqlalchemy.orm import sessionmaker -from sqlalchemy import create_engine +from sqlalchemy import create_engine, Column, DateTime, func from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import Session +from sqlalchemy.orm import sessionmaker from app.core.config import settings -SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL # .env에서 불러온 URL +SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL engine = create_engine(SQLALCHEMY_DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base = declarative_base() + +# BaseEntity에 해당하는 공통 필드 믹스인 +class BaseEntity: + createdAt = Column(DateTime, default=func.now(), nullable=False) + updatedAt = Column( + DateTime, default=func.now(), onupdate=func.now(), nullable=False + ) + + +# 모든 모델이 상속할 Base +Base = declarative_base(cls=BaseEntity) +# DB 세션 dependency def get_db(): db = SessionLocal() try: diff --git a/app/db_models/authentication_code.py b/app/db_models/authentication_code.py new file mode 100644 index 0000000..aa43d2a --- /dev/null +++ b/app/db_models/authentication_code.py @@ -0,0 +1,22 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum as SqlEnum +from app.core.db import Base +import enum + + +# CodeStatus Enum +class CodeStatus(str, enum.Enum): + ACTIVE = "ACTIVE" + EXPIRED = "EXPIRED" + + +# AuthenticationCode 모델 +class AuthenticationCode(Base): + __tablename__ = "AuthenticationCode" + + id = Column(Integer, primary_key=True, index=True) + + email = Column(String(50), nullable=False) + code = Column(String, unique=True, nullable=False) + isVerified = Column(Boolean, nullable=False) + status = Column(SqlEnum(CodeStatus), nullable=True) + expirationDate = Column(DateTime, nullable=False) diff --git a/app/db_models/base_entity.py b/app/db_models/base_entity.py new file mode 100644 index 0000000..af6d03c --- /dev/null +++ b/app/db_models/base_entity.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, DateTime, func +from sqlalchemy.ext.declarative import declared_attr +from datetime import datetime + + +class TimestampMixin: + createdAt = Column(DateTime, default=func.now(), nullable=False) + updatedAt = Column( + DateTime, default=func.now(), onupdate=func.now(), nullable=False + ) diff --git a/app/db_models/bookmark.py b/app/db_models/bookmark.py new file mode 100644 index 0000000..e419501 --- /dev/null +++ b/app/db_models/bookmark.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship +from app.core.db import Base + + +class Bookmark(Base): + __tablename__ = "Bookmark" + + id = Column(Integer, primary_key=True, index=True) + + user_id = Column(Integer, ForeignKey("User.id", ondelete="CASCADE"), nullable=False) + user = relationship("User", back_populates="bookmarks") + + jobId = Column(Integer, nullable=True) + companyName = Column(String, nullable=True) + jobTitle = Column(String, nullable=True) + pay = Column(String, nullable=True) + time = Column(String, nullable=True) + location = Column(String, nullable=True) + deadline = Column(String, nullable=True) + registrationDate = Column(String, nullable=True) diff --git a/app/db_models/education.py b/app/db_models/education_info.py similarity index 82% rename from app/db_models/education.py rename to app/db_models/education_info.py index 9a8f8c0..3e55af1 100644 --- a/app/db_models/education.py +++ b/app/db_models/education_info.py @@ -1,14 +1,15 @@ from sqlalchemy import Column, Integer, String, Text, ForeignKey -from app.core.db import Base from sqlalchemy.orm import relationship +from app.core.db import Base class EducationInfo(Base): __tablename__ = "EducationInfo" id = Column(Integer, primary_key=True, index=True) - user_id = Column(Integer, ForeignKey("User.id", ondelete="CASCADE"), nullable=False) - title = Column(String(100), nullable=False) - url = Column(Text, nullable=False) + user_id = Column(Integer, ForeignKey("User.id", ondelete="CASCADE"), nullable=False) user = relationship("User", back_populates="educationInfos") + + title = Column(String(50), nullable=False) + url = Column(Text, nullable=True) diff --git a/app/db_models/policy.py b/app/db_models/policy_info.py similarity index 91% rename from app/db_models/policy.py rename to app/db_models/policy_info.py index a9e8796..bfa2520 100644 --- a/app/db_models/policy.py +++ b/app/db_models/policy_info.py @@ -7,10 +7,11 @@ class PolicyInfo(Base): __tablename__ = "PolicyInfo" id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("User.id", ondelete="CASCADE"), nullable=False) + user = relationship("User", back_populates="policyInfos") + category = Column(String(50), nullable=False) - title = Column(String(100), nullable=False) + title = Column(String(50), nullable=False) description = Column(Text, nullable=True) url = Column(Text, nullable=True) - - user = relationship("User", back_populates="policyInfos") diff --git a/app/db_models/refresh_token.py b/app/db_models/refresh_token.py new file mode 100644 index 0000000..37f9535 --- /dev/null +++ b/app/db_models/refresh_token.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.orm import relationship +from app.core.db import Base +from datetime import datetime + + +class RefreshToken(Base): + __tablename__ = "RefreshToken" + + id = Column(Integer, primary_key=True, index=True) + + refreshToken = Column(String(512), nullable=False) + + expiryDate = Column(DateTime, nullable=False) + + user = relationship("User", back_populates="refreshToken", uselist=False) diff --git a/app/db_models/resume.py b/app/db_models/resume.py index 53a4b4f..1f9fd47 100644 --- a/app/db_models/resume.py +++ b/app/db_models/resume.py @@ -1,26 +1,42 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Text, Enum +from sqlalchemy import Column, Integer, String, Text, Enum as SqlEnum, ForeignKey from sqlalchemy.orm import relationship from app.core.db import Base import enum -# Java의 ResumeCategory enum과 동일하게 정의 +# ------------------ ResumeCategory Enum ------------------ + + class ResumeCategory(str, enum.Enum): - GENERAL = "GENERAL" - TECH = "TECH" - CAREER = "CAREER" - ACADEMIC = "ACADEMIC" + GROWTH = "GROWTH" + MOTIVATION = "MOTIVATION" + ASPIRATION = "ASPIRATION" + STRENGTH = "STRENGTH" + PROJECT_EXPERIENCE = "PROJECT_EXPERIENCE" + + @property + def display_name(self): + return { + "GROWTH": "성장과정", + "MOTIVATION": "지원동기", + "ASPIRATION": "입사포부", + "STRENGTH": "강점약점", + "PROJECT_EXPERIENCE": "프로젝트경험", + }[self.value] + + +# ------------------ Resume 모델 ------------------ class Resume(Base): __tablename__ = "Resume" id = Column(Integer, primary_key=True, index=True) - user_id = Column( - Integer, ForeignKey("User.id", ondelete="CASCADE"), nullable=False - ) # 테이블명 수정 - title = Column(String(255), nullable=False) - content = Column(Text, nullable=False) - resume_category = Column(String(50), nullable=False) + user_id = Column(Integer, ForeignKey("User.id", ondelete="CASCADE"), nullable=False) user = relationship("User", back_populates="resumes") + + title = Column(String(50), nullable=False) + content = Column(Text, nullable=False) + + resumeCategory = Column(SqlEnum(ResumeCategory), nullable=False) diff --git a/app/db_models/user.py b/app/db_models/user.py index 08f6ee3..cc136d6 100644 --- a/app/db_models/user.py +++ b/app/db_models/user.py @@ -1,12 +1,12 @@ -# app/db_models/user.py - -from sqlalchemy import Column, Integer, String, Enum, ForeignKey +from sqlalchemy import Column, Integer, String, Enum as SqlEnum, ForeignKey from sqlalchemy.orm import relationship from app.core.db import Base import enum +from app.core.db import Base + +# ------------------ ENUM 정의 ------------------ -# enum 들 정의 class Gender(str, enum.Enum): MALE = "MALE" FEMALE = "FEMALE" @@ -14,16 +14,31 @@ class Gender(str, enum.Enum): class Region(str, enum.Enum): SEOUL = "SEOUL" - BUSAN = "BUSAN" - # etc... + GYEONGGI = "GYEONGGI" + INCHEON = "INCHEON" + GANGWON = "GANGWON" + DAEJEON = "DAEJEON" + SEJONG = "SEJONG" + CHUNGBUK = "CHUNGBUK" class FinalEdu(str, enum.Enum): HIGH_SCHOOL = "HIGH_SCHOOL" + ASSOCIATE = "ASSOCIATE" BACHELOR = "BACHELOR" MASTER = "MASTER" DOCTOR = "DOCTOR" + @property + def description(self): + return { + "HIGH_SCHOOL": "고등학교 졸업", + "ASSOCIATE": "전문대학 졸업", + "BACHELOR": "대학교 졸업", + "MASTER": "석사 학위", + "DOCTOR": "박사 학위", + }[self.value] + class UserStatus(str, enum.Enum): ACTIVE = "ACTIVE" @@ -35,29 +50,36 @@ class Role(str, enum.Enum): ADMIN = "ADMIN" -# User 모델 +# ------------------ User 모델 정의 ------------------ + + class User(Base): - __tablename__ = "User" # ERD에 맞게 수정 + __tablename__ = "User" id = Column(Integer, primary_key=True, index=True) name = Column(String(20), nullable=False) primaryEmail = Column(String(50), unique=True) password = Column(String(100), nullable=False) age = Column(Integer, nullable=False) - gender = Column(Enum(Gender), nullable=False) - region = Column(Enum(Region), nullable=False) + gender = Column(SqlEnum(Gender), nullable=False) + region = Column(SqlEnum(Region), nullable=False) job = Column(String(50), nullable=False) career = Column(Integer, nullable=False) - finalEdu = Column(Enum(FinalEdu), nullable=False) - status = Column(Enum(UserStatus), nullable=False) - role = Column(Enum(Role), nullable=True) + finalEdu = Column(SqlEnum(FinalEdu), nullable=False) + status = Column(SqlEnum(UserStatus), nullable=False) + role = Column(SqlEnum(Role), nullable=True) + refresh_token_id = Column(Integer, ForeignKey("RefreshToken.id"), nullable=True) + refreshToken = relationship("RefreshToken", back_populates="user", uselist=False) - resumes = relationship( - "Resume", back_populates="user", cascade="all, delete-orphan" + bookmarks = relationship( + "Bookmark", back_populates="user", cascade="all, delete-orphan" + ) + policyInfos = relationship( + "PolicyInfo", back_populates="user", cascade="all, delete-orphan" ) educationInfos = relationship( "EducationInfo", back_populates="user", cascade="all, delete-orphan" ) - policyInfos = relationship( - "PolicyInfo", back_populates="user", cascade="all, delete-orphan" + resumes = relationship( + "Resume", back_populates="user", cascade="all, delete-orphan" ) diff --git a/app/main.py b/app/main.py index 1734903..cab1fdc 100644 --- a/app/main.py +++ b/app/main.py @@ -3,7 +3,7 @@ from app.api import routes from fastapi.middleware.cors import CORSMiddleware from app.core.db import engine -from app.db_models.education import EducationInfo +from app.db_models.education_info import EducationInfo from app.db_models.user import User # 새로 만들 예정 from app.core.db import Base diff --git a/app/services/education_service.py b/app/services/education_service.py index b71828a..8cf5404 100644 --- a/app/services/education_service.py +++ b/app/services/education_service.py @@ -1,13 +1,13 @@ from app.core.config import settings import requests from app.models.eduSchemas import EducationBookmarkRequest -from app.db_models.education import EducationInfo as EducationInfoDB +from app.db_models.education_info import EducationInfo as EducationInfoDB from sqlalchemy.orm import Session import xml.etree.ElementTree as ET from app.models.eduSchemas import EducationInfo from sqlalchemy.orm import Session -from app.db_models.education import EducationInfo as EducationInfoDB +from app.db_models.education_info import EducationInfo as EducationInfoDB API_KEY = settings.seoul_openapi_key API_URL = settings.seoul_openapi_url diff --git a/app/services/policy_service.py b/app/services/policy_service.py index 6971078..7cbecc7 100644 --- a/app/services/policy_service.py +++ b/app/services/policy_service.py @@ -2,7 +2,7 @@ from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_pinecone import PineconeVectorStore from sqlalchemy.orm import Session -from app.db_models.policy import PolicyInfo +from app.db_models.policy_info import PolicyInfo from app.models.policySchemas import PolicySaveRequest from app.core.config import settings From b3da12e72f34a9bcd8d9b8223b37aab3a6653568 Mon Sep 17 00:00:00 2001 From: guineaaaa <165776804+guineaaaa@users.noreply.github.com> Date: Mon, 12 May 2025 17:49:42 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[FEAT]=20=EA=B3=B5=ED=86=B5=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=9E=90=EB=8F=99=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/common.py | 10 ++++++++++ app/models/eduSchemas.py | 3 ++- app/models/jobSchemas.py | 1 + app/models/policySchemas.py | 3 ++- app/models/reempSchemas.py | 1 + app/models/resumeSchemas.py | 5 +++-- 6 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 app/models/common.py diff --git a/app/models/common.py b/app/models/common.py new file mode 100644 index 0000000..07e1ee8 --- /dev/null +++ b/app/models/common.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel +from datetime import datetime + + +class TimestampMixin(BaseModel): + createdAt: datetime + updatedAt: datetime + + class Config: + orm_mode = True diff --git a/app/models/eduSchemas.py b/app/models/eduSchemas.py index 0866f69..0033da2 100644 --- a/app/models/eduSchemas.py +++ b/app/models/eduSchemas.py @@ -1,5 +1,6 @@ from pydantic import BaseModel from typing import List +from app.models.common import TimestampMixin class EducationSearchRequest(BaseModel): @@ -26,6 +27,6 @@ class EducationItem(BaseModel): url: str -class EducationBookmarkRequest(BaseModel): +class EducationBookmarkRequest(TimestampMixin): user_id: int bookmarks: List[EducationItem] diff --git a/app/models/jobSchemas.py b/app/models/jobSchemas.py index 6f68aa8..a78c8cb 100644 --- a/app/models/jobSchemas.py +++ b/app/models/jobSchemas.py @@ -1,5 +1,6 @@ from pydantic import BaseModel from typing import List +from app.models.common import TimestampMixin class JobSummary(BaseModel): diff --git a/app/models/policySchemas.py b/app/models/policySchemas.py index 7d87569..5f2289c 100644 --- a/app/models/policySchemas.py +++ b/app/models/policySchemas.py @@ -1,5 +1,6 @@ from pydantic import BaseModel from typing import List +from app.models.common import TimestampMixin class PolicyRecommendRequest(BaseModel): @@ -24,6 +25,6 @@ class PolicyItem(BaseModel): url: str -class PolicySaveRequest(BaseModel): +class PolicySaveRequest(TimestampMixin): user_id: int policies: List[PolicyItem] diff --git a/app/models/reempSchemas.py b/app/models/reempSchemas.py index 16d68a6..3cee638 100644 --- a/app/models/reempSchemas.py +++ b/app/models/reempSchemas.py @@ -1,6 +1,7 @@ # Pydantic Request/Response 모델 from pydantic import BaseModel from typing import List, Dict, Optional +from app.models.common import TimestampMixin class ReemploymentRequest(BaseModel): diff --git a/app/models/resumeSchemas.py b/app/models/resumeSchemas.py index a34959e..f9f3b2d 100644 --- a/app/models/resumeSchemas.py +++ b/app/models/resumeSchemas.py @@ -1,6 +1,7 @@ from pydantic import BaseModel from typing import Dict from enum import Enum +from app.models.common import TimestampMixin class ResumeInitRequest(BaseModel): @@ -24,12 +25,12 @@ class ResumeCategory(str, Enum): ACADEMIC = "ACADEMIC" -class ResumeResult(BaseModel): +class ResumeResult(TimestampMixin): title: str sections: Dict[str, str] -class ResumeSaveRequest(BaseModel): +class ResumeSaveRequest(TimestampMixin): user_id: int title: str sections: Dict[str, str] From df2e821d0edbef9d5a2c531cc0c915cd34689378 Mon Sep 17 00:00:00 2001 From: guineaaaa <165776804+guineaaaa@users.noreply.github.com> Date: Mon, 12 May 2025 18:09:44 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[FEAT]=20Spring=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=ED=95=9C=20jwt=ED=86=A0=ED=81=B0=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D,=20db=EA=B4=80=EB=A0=A8=20api=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/routes.py | 9 +++++- app/core/config.py | 12 +++++++ app/utils/jwt.py | 30 +++++++++++++++++ requirements.txt | 81 ++++++---------------------------------------- 4 files changed, 60 insertions(+), 72 deletions(-) create mode 100644 app/utils/jwt.py diff --git a/app/api/routes.py b/app/api/routes.py index 745acab..fdd630d 100644 --- a/app/api/routes.py +++ b/app/api/routes.py @@ -1,4 +1,5 @@ from fastapi import APIRouter, Depends, Body, Query +from app.utils.jwt import verify_jwt from fastapi.responses import JSONResponse from sqlalchemy.orm import Session @@ -212,6 +213,7 @@ def bookmark_education( } ), db: Session = Depends(get_db), + token_data=Depends(verify_jwt), ): return save_bookmarked_education(data, db) @@ -251,6 +253,7 @@ def bookmark_policy( } ), db: Session = Depends(get_db), + token_data=Depends(verify_jwt), ): return save_policy_bookmarks(data, db) @@ -360,6 +363,7 @@ def save_resume( } ), db: Session = Depends(get_db), + token_data=Depends(verify_jwt), ): saved = save_resume_to_db(db, data) return {"resume_id": saved.id, "message": "자기소개서가 저장되었습니다."} @@ -401,5 +405,8 @@ def save_resume( }, }, ) -def get_user_resumes(userId: int, db: Session = Depends(get_db)): +def get_user_resumes( + userId: int, + db: Session = Depends(get_db), + token_data=Depends(verify_jwt)): return get_resumes_by_user_id(db, userId) diff --git a/app/core/config.py b/app/core/config.py index d5dee83..cdf0a99 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,6 +1,7 @@ from dotenv import load_dotenv import os from pydantic_settings import BaseSettings +import base64 load_dotenv() @@ -20,6 +21,17 @@ class Settings(BaseSettings): JOB_INFO_KEY: str DATABASE_URL: str + JWT_SECRET_KEY_BASE64: str + JWT_ALGORITHM: str = "HS256" + + # ✅ Base64 디코딩된 JWT secret 키 + @property + def jwt_secret_bytes(self) -> bytes: + return base64.b64decode(self.JWT_SECRET_KEY_BASE64) + + class Config: + env_file = ".env" + class Config: env_file = ".env" diff --git a/app/utils/jwt.py b/app/utils/jwt.py new file mode 100644 index 0000000..306e1bb --- /dev/null +++ b/app/utils/jwt.py @@ -0,0 +1,30 @@ +from fastapi import Depends, HTTPException +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from jose import jwt, JWTError +from app.core.config import settings +from datetime import datetime, timezone + +bearer_scheme = HTTPBearer(auto_error=True) + + +def verify_jwt( + credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme), +): + token = credentials.credentials + try: + payload = jwt.decode( + token, + settings.jwt_secret_bytes, + algorithms=[settings.JWT_ALGORITHM], + ) + + # 토큰 만료 검사 (timezone-aware 방식) + if "exp" in payload and datetime.fromtimestamp( + payload["exp"], tz=timezone.utc + ) < datetime.now(timezone.utc): + raise HTTPException(status_code=401, detail="Access token expired") + + return payload + + except JWTError: + raise HTTPException(status_code=401, detail="Invalid access token") diff --git a/requirements.txt b/requirements.txt index 8571d5b..bcd41b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,116 +1,55 @@ -aiofiles==24.1.0 -aiohappyeyeballs==2.6.1 -aiohttp==3.10.11 -aiosignal==1.3.2 -alembic==1.15.2 annotated-types==0.7.0 anyio==4.9.0 -asyncio==3.4.3 -attrs==25.3.0 -backoff==2.2.1 -beautifulsoup4==4.13.3 certifi==2025.1.31 cffi==1.17.1 -chardet==5.2.0 charset-normalizer==3.4.1 -click==8.1.8 colorama==0.4.6 -cryptography==44.0.2 -dataclasses-json==0.6.7 +cryptography==44.0.3 distro==1.9.0 -emoji==2.14.1 -eval_type_backport==0.2.2 +ecdsa==0.19.1 +et_xmlfile==2.0.0 faiss-cpu==1.10.0 -fastapi==0.115.12 -filetype==1.2.0 -frozenlist==1.5.0 +git-filter-repo==2.45.0 greenlet==3.1.1 h11==0.14.0 -html5lib==1.1 httpcore==1.0.7 httpx==0.28.1 -httpx-sse==0.4.0 idna==3.10 -iniconfig==2.1.0 jiter==0.9.0 -joblib==1.4.2 jsonpatch==1.33 jsonpointer==3.0.0 langchain==0.3.23 -langchain-community==0.3.21 -langchain-core==0.3.56 -langchain-openai==0.3.14 -langchain-pinecone==0.2.5 -langchain-tests==0.3.19 +langchain-core==0.3.51 langchain-text-splitters==0.3.8 -langdetect==1.0.9 langsmith==0.3.28 -lxml==5.3.2 -Mako==1.3.10 -MarkupSafe==3.0.2 -marshmallow==3.26.1 -multidict==6.4.2 -mypy-extensions==1.0.0 -nest-asyncio==1.6.0 -nltk==3.9.1 numpy==2.2.4 -olefile==0.47 openai==1.72.0 +openpyxl==3.1.5 orjson==3.10.16 -outcome==1.3.0.post0 packaging==24.2 pandas==2.2.3 pinecone==6.0.2 pinecone-plugin-interface==0.0.7 -pluggy==1.5.0 -propcache==0.3.1 -psutil==7.0.0 +pyasn1==0.4.8 pycparser==2.22 pydantic==2.11.3 -pydantic-settings==2.8.1 +pydantic-settings==2.9.1 pydantic_core==2.33.1 -PyMySQL==1.1.1 -pypdf==5.4.0 -PySocks==1.7.1 -pytest==8.3.5 -pytest-asyncio==0.26.0 -pytest-socket==0.7.0 python-dateutil==2.9.0.post0 python-dotenv==1.1.0 -python-iso639==2025.2.18 -python-magic==0.4.27 -python-oxmsg==0.0.2 +python-jose==3.4.0 pytz==2025.2 PyYAML==6.0.2 -RapidFuzz==3.13.0 -regex==2024.11.6 requests==2.32.3 requests-toolbelt==1.0.0 -selenium==4.32.0 +rsa==4.9.1 six==1.17.0 sniffio==1.3.1 -sortedcontainers==2.4.0 -soupsieve==2.6 SQLAlchemy==2.0.40 -starlette==0.46.1 -syrupy==4.9.1 tenacity==9.1.2 -tiktoken==0.9.0 tqdm==4.67.1 -trio==0.30.0 -trio-websocket==0.12.2 -typing-inspect==0.9.0 typing-inspection==0.4.0 typing_extensions==4.13.1 tzdata==2025.2 -unstructured==0.17.2 -unstructured-client==0.32.3 urllib3==2.3.0 -uvicorn==0.34.0 -webdriver-manager==4.0.2 -webencodings==0.5.1 -websocket-client==1.8.0 -wrapt==1.17.2 -wsproto==1.2.0 -yarl==1.19.0 zstandard==0.23.0