Aryan Jain commited on
Commit
ef8c55b
·
1 Parent(s): 8e1c132

update database schema

Browse files
alembic/versions/e825902b2e8a_alter_tables.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """alter tables
2
+
3
+ Revision ID: e825902b2e8a
4
+ Revises: 11d43047d470
5
+ Create Date: 2025-06-02 16:32:45.552509
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+
13
+ from sqlalchemy.dialects import postgresql
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = 'e825902b2e8a'
17
+ down_revision: Union[str, None] = '11d43047d470'
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ """Upgrade schema."""
24
+ op.add_column("rfps", sa.Column("name", sa.String(), nullable=True))
25
+
26
+ op.add_column("proposals", sa.Column("tep", sa.String(), nullable=True))
27
+
28
+ gate_criteria_enum = sa.Enum('PASS', 'FAIL', 'IN_REVIEW', name='gatecriteria')
29
+ gate_criteria_enum.create(op.get_bind())
30
+ op.add_column('proposals', sa.Column('gate_criteria', gate_criteria_enum, nullable=False, server_default='IN_REVIEW'))
31
+
32
+ op.create_table(
33
+ "letters",
34
+ sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
35
+ sa.Column("proposal_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("proposals.id", ondelete="CASCADE"), nullable=False),
36
+ sa.Column("letter", sa.Text(), nullable=True),
37
+ sa.Column("letter_type", sa.Enum("UO", "DEBRIEF", name="lettertype"), nullable=False),
38
+ sa.Column("created_at", sa.DateTime(), nullable=False),
39
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
40
+ )
41
+
42
+
43
+ def downgrade() -> None:
44
+ """Downgrade schema."""
45
+ op.drop_table("letters")
46
+ sa.Enum(name='gatecriteria').drop(op.get_bind(), checkfirst=True)
47
+ op.drop_column("proposals", "gate_criteria")
48
+ op.drop_column("proposals", "tep")
49
+ op.drop_column("rfps", "name")
src/controllers/__init__.py CHANGED
@@ -3,6 +3,7 @@ from ._rfp_controller import RFPController
3
  from ._proposal_controller import ProposalController
4
  from ._proposal_ai_analysis_controller import ProposalAIController
5
  from ._proposal_detailed_analysis_controller import ProposalDetailedController
 
6
 
7
  api_router = APIRouter()
8
 
@@ -10,6 +11,7 @@ api_router.include_router(RFPController().router, prefix="/rfp", tags=["RFP"])
10
  api_router.include_router(ProposalController().router, prefix="/proposal", tags=["Proposal"])
11
  api_router.include_router(ProposalAIController().router, prefix="/proposal_ai_analysis", tags=["Proposal AI Analysis"])
12
  api_router.include_router(ProposalDetailedController().router, prefix="/proposal_detailed_analysis", tags=["Proposal Detailed Analysis"])
 
13
 
14
  __all__ = ["api_router"]
15
  __version__ = "0.1.0"
 
3
  from ._proposal_controller import ProposalController
4
  from ._proposal_ai_analysis_controller import ProposalAIController
5
  from ._proposal_detailed_analysis_controller import ProposalDetailedController
6
+ from ._letter_controller import LetterController
7
 
8
  api_router = APIRouter()
9
 
 
11
  api_router.include_router(ProposalController().router, prefix="/proposal", tags=["Proposal"])
12
  api_router.include_router(ProposalAIController().router, prefix="/proposal_ai_analysis", tags=["Proposal AI Analysis"])
13
  api_router.include_router(ProposalDetailedController().router, prefix="/proposal_detailed_analysis", tags=["Proposal Detailed Analysis"])
14
+ api_router.include_router(LetterController().router, prefix="/letter", tags=["Letter"])
15
 
16
  __all__ = ["api_router"]
17
  __version__ = "0.1.0"
src/controllers/_letter_controller.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Query
2
+ from pydantic import BaseModel
3
+ from typing import List, Optional
4
+ from uuid import UUID
5
+ from datetime import datetime
6
+
7
+ from src.config import logger
8
+ from src.services import LetterService
9
+ from src.models import LetterType
10
+
11
+ class Letter(BaseModel):
12
+ id: UUID
13
+ proposal_id: UUID
14
+ letter: str
15
+ letter_type: LetterType
16
+ created_at: datetime
17
+ updated_at: datetime
18
+
19
+ class Response(BaseModel):
20
+ status: str
21
+ data: List[Letter]
22
+
23
+ class LetterRequest(BaseModel):
24
+ proposal_id: UUID
25
+ letter: str
26
+ letter_type: LetterType
27
+
28
+ class UpdateLetterRequest(BaseModel):
29
+ id: UUID
30
+ proposal_id: Optional[UUID] = None
31
+ letter: Optional[str] = None
32
+ letter_type: Optional[LetterType] = None
33
+
34
+ class DeleteResponse(BaseModel):
35
+ status: str
36
+
37
+ class LetterController:
38
+ def __init__(self):
39
+ self.letter_service = LetterService
40
+ self.router = APIRouter()
41
+ self.router.add_api_route(
42
+ "/get_letters",
43
+ self.get_letters,
44
+ methods=["GET"],
45
+ response_model=Response
46
+ )
47
+ self.router.add_api_route(
48
+ "/create_letter",
49
+ self.create_letter,
50
+ methods=["POST"],
51
+ response_model=Response
52
+ )
53
+ self.router.add_api_route(
54
+ "/update_letter",
55
+ self.update_letter,
56
+ methods=["PUT"],
57
+ response_model=Response
58
+ )
59
+ self.router.add_api_route(
60
+ "/delete_letter",
61
+ self.delete_letter,
62
+ methods=["DELETE"],
63
+ response_model=DeleteResponse
64
+ )
65
+
66
+ async def get_letters(self, id: Optional[str] = Query(None), proposal_id: Optional[str] = Query(None), letter_type: Optional[LetterType] = Query(None)):
67
+ try:
68
+ async with self.letter_service() as service:
69
+ result = await service.get_letters(id=id, proposal_id=proposal_id, letter_type=letter_type)
70
+ return Response(status="success", data=[Letter(**r) for r in result])
71
+ except Exception as e:
72
+ logger.error(e)
73
+ raise HTTPException(status_code=500, detail="Error fetching letters")
74
+
75
+ async def create_letter(self, letter: LetterRequest):
76
+ try:
77
+ async with self.letter_service() as service:
78
+ result = await service.create_letter(letter.model_dump(exclude_unset=True, mode="json"))
79
+ return Response(status="success", data=[Letter(**r) for r in result])
80
+ except Exception as e:
81
+ logger.error(e)
82
+ raise HTTPException(status_code=500, detail="Error creating letter")
83
+
84
+ async def update_letter(self, letter: UpdateLetterRequest):
85
+ try:
86
+ async with self.letter_service() as service:
87
+ result = await service.update_letter(letter.model_dump(exclude_unset=True, mode="json"))
88
+ return Response(status="success", data=[Letter(**r) for r in result])
89
+ except Exception as e:
90
+ logger.error(e)
91
+ raise HTTPException(status_code=500, detail="Error updating letter")
92
+
93
+ async def delete_letter(self, id: Optional[str] = Query(None), proposal_id: Optional[str] = Query(None)):
94
+ try:
95
+ if not id and not proposal_id:
96
+ return DeleteResponse(status="Failed to delete letter")
97
+ async with self.letter_service() as service:
98
+ result = await service.delete_letter(id)
99
+ return DeleteResponse(status="success")
100
+ except Exception as e:
101
+ logger.error(e)
102
+ raise HTTPException(status_code=500, detail="Error deleting letter")
src/controllers/_proposal_controller.py CHANGED
@@ -6,13 +6,15 @@ from datetime import datetime
6
 
7
  from src.config import logger
8
  from src.services import ProposalService
9
- from src.models import ProposalStatus
10
 
11
 
12
  class Proposal(BaseModel):
13
  id: UUID
14
  rfp_id: UUID
15
  name: str
 
 
16
  status: ProposalStatus
17
  created_at: datetime
18
  updated_at: datetime
@@ -21,6 +23,8 @@ class Proposal(BaseModel):
21
  class ProposalRequest(BaseModel):
22
  rfp_id: UUID
23
  name: str
 
 
24
  status: ProposalStatus
25
 
26
 
@@ -28,6 +32,8 @@ class ProposalUpdateRequest(BaseModel):
28
  id: UUID
29
  rfp_id: Optional[UUID] = None
30
  name: Optional[str] = None
 
 
31
  status: Optional[ProposalStatus] = None
32
 
33
 
 
6
 
7
  from src.config import logger
8
  from src.services import ProposalService
9
+ from src.models import ProposalStatus, GateCriteria
10
 
11
 
12
  class Proposal(BaseModel):
13
  id: UUID
14
  rfp_id: UUID
15
  name: str
16
+ tep: str
17
+ gate_criteria: GateCriteria
18
  status: ProposalStatus
19
  created_at: datetime
20
  updated_at: datetime
 
23
  class ProposalRequest(BaseModel):
24
  rfp_id: UUID
25
  name: str
26
+ tep: str
27
+ gate_criteria: GateCriteria
28
  status: ProposalStatus
29
 
30
 
 
32
  id: UUID
33
  rfp_id: Optional[UUID] = None
34
  name: Optional[str] = None
35
+ tep: Optional[str] = None
36
+ gate_criteria: Optional[GateCriteria] = None
37
  status: Optional[ProposalStatus] = None
38
 
39
 
src/controllers/_rfp_controller.py CHANGED
@@ -10,6 +10,7 @@ from datetime import datetime
10
  class RFP(BaseModel):
11
  id: UUID
12
  rfp_number: str
 
13
  received_proposals: int
14
  evaluated_count: int
15
  awaiting_evaluation: int
@@ -22,6 +23,7 @@ class ResponseRFP(BaseModel):
22
 
23
  class RFPRequest(BaseModel):
24
  rfp_number: str
 
25
  received_proposals: int
26
  evaluated_count: int
27
  awaiting_evaluation: int
@@ -29,6 +31,7 @@ class RFPRequest(BaseModel):
29
  class RFPUpdateRequest(BaseModel):
30
  id: str
31
  rfp_number: Optional[str] = None
 
32
  received_proposals: Optional[int] = None
33
  evaluated_count: Optional[int] = None
34
  awaiting_evaluation: Optional[int] = None
 
10
  class RFP(BaseModel):
11
  id: UUID
12
  rfp_number: str
13
+ name: str
14
  received_proposals: int
15
  evaluated_count: int
16
  awaiting_evaluation: int
 
23
 
24
  class RFPRequest(BaseModel):
25
  rfp_number: str
26
+ name: str
27
  received_proposals: int
28
  evaluated_count: int
29
  awaiting_evaluation: int
 
31
  class RFPUpdateRequest(BaseModel):
32
  id: str
33
  rfp_number: Optional[str] = None
34
+ name: Optional[str] = None
35
  received_proposals: Optional[int] = None
36
  evaluated_count: Optional[int] = None
37
  awaiting_evaluation: Optional[int] = None
src/models/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
  from ._base import Base
2
- from ._proposal import Proposal, ProposalStatus
3
  from ._rfp import RFP
4
  from ._proposal_ai_analysis import (
5
  ProposalAIAnalysis,
@@ -10,6 +10,7 @@ from ._proposal_ai_analysis import (
10
  )
11
  from ._proposal_detailed_analysis import ProposalDetailAnalysis, OperationCriteria
12
  from ._proposal_query_models import QueryType, CreateOrUpdateType
 
13
 
14
  __all__ = [
15
  "Base",
@@ -25,6 +26,9 @@ __all__ = [
25
  "QueryType",
26
  "CreateOrUpdateType",
27
  "OperationCriteria",
 
 
 
28
  ]
29
  __version__ = "0.1.0"
30
  __author__ = "Aryan Jain"
 
1
  from ._base import Base
2
+ from ._proposal import Proposal, ProposalStatus, GateCriteria
3
  from ._rfp import RFP
4
  from ._proposal_ai_analysis import (
5
  ProposalAIAnalysis,
 
10
  )
11
  from ._proposal_detailed_analysis import ProposalDetailAnalysis, OperationCriteria
12
  from ._proposal_query_models import QueryType, CreateOrUpdateType
13
+ from ._letters import Letter, LetterType
14
 
15
  __all__ = [
16
  "Base",
 
26
  "QueryType",
27
  "CreateOrUpdateType",
28
  "OperationCriteria",
29
+ "GateCriteria",
30
+ "Letter",
31
+ "LetterType",
32
  ]
33
  __version__ = "0.1.0"
34
  __author__ = "Aryan Jain"
src/models/_letters.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum as PyEnum
2
+
3
+ from sqlalchemy import (
4
+ Column,
5
+ DateTime,
6
+ Enum,
7
+ Float,
8
+ ForeignKey,
9
+ Integer,
10
+ String,
11
+ func,
12
+ )
13
+ from sqlalchemy.dialects.postgresql import UUID
14
+ from pydantic import BaseModel
15
+ from ._base import Base
16
+
17
+ class LetterType(PyEnum):
18
+ UO = "UO"
19
+ DEBRIEF = "DEBRIEF"
20
+
21
+ class Letter(Base):
22
+ __tablename__ = "letters"
23
+ id = Column(UUID(as_uuid=True), primary_key=True, nullable=False)
24
+ proposal_id = Column(UUID(as_uuid=True), ForeignKey("proposals.id", ondelete="CASCADE"), nullable=False)
25
+ letter = Column(String, nullable=True)
26
+ letter_type = Column(Enum(LetterType), nullable=False)
27
+ created_at = Column(DateTime, nullable=False, default=func.now())
28
+ updated_at = Column(DateTime, nullable=False, default=func.now(), onupdate=func.now())
src/models/_proposal.py CHANGED
@@ -19,6 +19,10 @@ class ProposalStatus(PyEnum):
19
  EVALUATED = "EVALUATED"
20
  IN_REVIEW = "IN_REVIEW"
21
 
 
 
 
 
22
 
23
  class Proposal(Base):
24
  __tablename__ = "proposals"
@@ -28,6 +32,8 @@ class Proposal(Base):
28
  UUID(as_uuid=True), ForeignKey("rfps.id", ondelete="CASCADE"), nullable=False
29
  )
30
  name = Column(String, nullable=False)
 
 
31
  status = Column(Enum(ProposalStatus), nullable=False)
32
  created_at = Column(DateTime, nullable=False, default=func.now())
33
  updated_at = Column(
 
19
  EVALUATED = "EVALUATED"
20
  IN_REVIEW = "IN_REVIEW"
21
 
22
+ class GateCriteria(PyEnum):
23
+ PASS = "PASS"
24
+ FAIL = "FAIL"
25
+ IN_REVIEW = "IN_REVIEW"
26
 
27
  class Proposal(Base):
28
  __tablename__ = "proposals"
 
32
  UUID(as_uuid=True), ForeignKey("rfps.id", ondelete="CASCADE"), nullable=False
33
  )
34
  name = Column(String, nullable=False)
35
+ tep = Column(String, nullable=False)
36
+ gate_criteria = Column(Enum(GateCriteria), nullable=False)
37
  status = Column(Enum(ProposalStatus), nullable=False)
38
  created_at = Column(DateTime, nullable=False, default=func.now())
39
  updated_at = Column(
src/models/_rfp.py CHANGED
@@ -19,6 +19,7 @@ class RFP(Base):
19
 
20
  id = Column(UUID(as_uuid=True), primary_key=True, nullable=False)
21
  rfp_number = Column(String, nullable=False)
 
22
  received_proposals = Column(Integer, nullable=False)
23
  evaluated_count = Column(Integer, nullable=False)
24
  awaiting_evaluation = Column(Integer, nullable=False)
 
19
 
20
  id = Column(UUID(as_uuid=True), primary_key=True, nullable=False)
21
  rfp_number = Column(String, nullable=False)
22
+ name = Column(String, nullable=False)
23
  received_proposals = Column(Integer, nullable=False)
24
  evaluated_count = Column(Integer, nullable=False)
25
  awaiting_evaluation = Column(Integer, nullable=False)
src/repositories/__init__.py CHANGED
@@ -2,7 +2,14 @@ from ._base_repository import BaseRepository
2
  from ._rfp_repository import RFPRepository
3
  from ._proposal_repository import ProposalRepository
4
  from ._proposal_detailed_analysis_repository import ProposalDetailedAnalysisRepository
 
5
 
6
- __all__ = ["BaseRepository", "RFPRepository", "ProposalRepository", "ProposalDetailedAnalysisRepository"]
 
 
 
 
 
 
7
  __version__ = "0.1.0"
8
- __author__ = "Aryan Jain"
 
2
  from ._rfp_repository import RFPRepository
3
  from ._proposal_repository import ProposalRepository
4
  from ._proposal_detailed_analysis_repository import ProposalDetailedAnalysisRepository
5
+ from ._letter_repository import LetterRepository
6
 
7
+ __all__ = [
8
+ "BaseRepository",
9
+ "RFPRepository",
10
+ "ProposalRepository",
11
+ "ProposalDetailedAnalysisRepository",
12
+ "LetterRepository",
13
+ ]
14
  __version__ = "0.1.0"
15
+ __author__ = "Aryan Jain"
src/repositories/_letter_repository.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ from sqlalchemy import select
3
+
4
+ from src.models import Letter, LetterType
5
+
6
+ from ._base_repository import BaseRepository
7
+
8
+ class LetterRepository(BaseRepository):
9
+ def __init__(self):
10
+ super().__init__(Letter)
11
+
12
+ async def __aenter__(self):
13
+ return self
14
+
15
+ async def __aexit__(self, exc_type, exc_value, traceback):
16
+ pass
17
+
18
+ async def get_letters(self, id: str = None, proposal_id: str = None, letter_type: LetterType = None):
19
+ async with self.get_session() as session:
20
+ query = select(Letter)
21
+ if id:
22
+ query = query.where(Letter.id == id)
23
+ if proposal_id:
24
+ query = query.where(Letter.proposal_id == proposal_id)
25
+ if letter_type:
26
+ query = query.where(Letter.letter_type == letter_type)
27
+ output = await session.execute(query)
28
+ results = output.scalars().all()
29
+ return [{k: v for k, v in result.__dict__.items() if not k.startswith('_')} for result in results]
30
+
31
+ async def create_letter(self, letter: dict):
32
+ async with self.get_session() as session:
33
+ letter_type = letter["letter_type"]
34
+ proposal_id = letter["proposal_id"]
35
+ query = select(Letter).where(Letter.proposal_id == proposal_id, Letter.letter_type == letter_type)
36
+ output = await session.execute(query)
37
+ instances = output.scalars().all()
38
+ if instances:
39
+ return False
40
+ id = str(uuid.uuid4())
41
+ letter["id"] = id
42
+ instance = Letter(**letter)
43
+ session.add(instance)
44
+ await session.commit()
45
+ await session.refresh(instance)
46
+ return [{k: v for k, v in instance.__dict__.items() if not k.startswith('_')}]
47
+
48
+ async def update_letter(self, letter: dict):
49
+ async with self.get_session() as session:
50
+ id = letter["id"]
51
+ query = select(Letter).where(Letter.id == id)
52
+ output = await session.execute(query)
53
+ instance = output.scalars().one()
54
+ proposal_id = instance.proposal_id
55
+ if "letter_type" in letter:
56
+ query = select(Letter).where(Letter.proposal_id == proposal_id, Letter.letter_type == letter["letter_type"])
57
+ output = await session.execute(query)
58
+ instances = output.scalars().all()
59
+ if instances:
60
+ return False
61
+ for key, value in letter.items():
62
+ setattr(instance, key, value)
63
+ await session.commit()
64
+ await session.refresh(instance)
65
+ return [{k: v for k, v in instance.__dict__.items() if not k.startswith('_')}]
66
+
67
+ async def delete_letter(self, id: str = None, proposal_id: str = None):
68
+ async with self.get_session() as session:
69
+ query = select(Letter)
70
+ if id:
71
+ query = query.where(Letter.id == id)
72
+ if proposal_id:
73
+ query = query.where(Letter.proposal_id == proposal_id)
74
+ output = await session.execute(query)
75
+ instances = output.scalars().all()
76
+ for instance in instances:
77
+ await session.delete(instance)
78
+ await session.commit()
79
+ return True
src/services/__init__.py CHANGED
@@ -6,6 +6,7 @@ from ._proposal_ai_analysis_service import (
6
  CreateOrUpdateType,
7
  )
8
  from ._proposal_detailed_analysis_service import ProposalDetailedAnalysisService
 
9
 
10
  __all__ = [
11
  "RFPService",
@@ -14,6 +15,7 @@ __all__ = [
14
  "QueryType",
15
  "CreateOrUpdateType",
16
  "ProposalDetailedAnalysisService",
 
17
  ]
18
  __version__ = "0.1.0"
19
  __author__ = "Aryan Jain"
 
6
  CreateOrUpdateType,
7
  )
8
  from ._proposal_detailed_analysis_service import ProposalDetailedAnalysisService
9
+ from ._letter_service import LetterService
10
 
11
  __all__ = [
12
  "RFPService",
 
15
  "QueryType",
16
  "CreateOrUpdateType",
17
  "ProposalDetailedAnalysisService",
18
+ "LetterService",
19
  ]
20
  __version__ = "0.1.0"
21
  __author__ = "Aryan Jain"
src/services/_letter_service.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.repositories import LetterRepository
2
+ from src.models import Letter, LetterType
3
+
4
+ class LetterService:
5
+ def __init__(self):
6
+ self.letter_repository = LetterRepository
7
+
8
+ async def __aenter__(self):
9
+ return self
10
+
11
+ async def __aexit__(self, exc_type, exc_value, traceback):
12
+ pass
13
+
14
+ async def get_letters(self, id: str = None, proposal_id: str = None, letter_type: LetterType = None):
15
+ async with self.letter_repository() as repository:
16
+ return await repository.get_letters(id=id, proposal_id=proposal_id, letter_type=letter_type)
17
+
18
+ async def create_letter(self, letter: dict):
19
+ async with self.letter_repository() as repository:
20
+ return await repository.create_letter(letter)
21
+
22
+ async def update_letter(self, letter: dict):
23
+ async with self.letter_repository() as repository:
24
+ return await repository.update_letter(letter)
25
+
26
+ async def delete_letter(self, id: str = None, proposal_id: str = None):
27
+ async with self.letter_repository() as repository:
28
+ return await repository.delete_letter(id)