From d36367fbe92a42a0f9368b92d875b8115b8d0fc7 Mon Sep 17 00:00:00 2001 From: bolade Date: Sat, 27 Sep 2025 08:53:59 +0100 Subject: [PATCH] Add project management functionality with CRUD operations and associations; introduce project schemas and update main application routing. --- app/__pycache__/main.cpython-312.pyc | Bin 3924 -> 4018 bytes app/db/__pycache__/models.cpython-312.pyc | Bin 5062 -> 6624 bytes app/db/models.py | 70 ++- app/main.py | 4 +- .../__pycache__/investors.cpython-312.pyc | Bin 9689 -> 9689 bytes app/routers/investors.py | 4 +- app/routers/projects.py | 447 ++++++++++++++++++ .../__pycache__/py_schemas.cpython-312.pyc | Bin 10845 -> 10845 bytes app/schemas/project_schemas.py | 117 +++++ .../__pycache__/llm_parser.cpython-312.pyc | Bin 13262 -> 13273 bytes app/services/llm_parser.py | 2 +- 11 files changed, 639 insertions(+), 5 deletions(-) create mode 100644 app/routers/projects.py create mode 100644 app/schemas/project_schemas.py diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc index b7576a75b10a53b4341e7f5db5d200909408bc78..b5a03bdd3179e9b3d88a8fa85faef52c22313d86 100644 GIT binary patch delta 954 zcmaJvp%yZb`gI zjP>HlI*I-Wyl4~ufJZ$@LW0?N@CTS^ypVWuW-BO~xXC>G&htL+$G-3GWnYgd?-V5p z*z@)I{>DyQ(cqKzM0LA9XbeJt0P3+$)RRUsu*23+T{YCej##O>W@r!y0Ec&Y0`@RZ zv;~YbED`03^c(2CafoV4qy5-J&;yc24vo6{p@P+a`AXh-q(O*K>&Lha1GH z;fmKe5N<=L(KRU#Roal=K%Gv6TB#w{4N*Cy82Iv%W3QW4lBI7$CsP5RXZ-a^ndG== zl*8CFIbGy5IUY?ioa4Zcn_%!5e2gU?T|1CMq3CzOpj#_L@5uARLGnn0DZ~S1>1c{%nXdIc>_1H|FqJQ} zDWkGW=IEzBERYy2DmskQyNb5m&;1D(lP!}~oC(sHc9rlxh{j=+10RnsRxFt0l z*YhRI@Jw4^RaIOK->odyI|nyr56kBg*`C-nNG*^65st#UwG3q delta 866 zcmZuv&rcIU6yDh_TNc>12yEM3O3M!^MMV<89}x-B5XBHf;=yE1+^pT9E$k0`vn85% z(4+^$1)E7c`5)lPsz(oAy>Kud_T+z{XHU*-#ei{=`SSL??|U=zy?s~wI;MV6)jol( z*AKqcpS9II+H99=D@bqzC+QA(!{%@t_qrpVW@>S)xLGe}=8zx?xc88Y*tL??@fom>sQ%^!DlbZl&ij?qf|)lM?)`bkwc#%H$g(2`il|Gzret!Ag4(VZDV z0E3x;+_G6_k1B6((tWY5iIGwbyj|OGU=k?}e-D!|fX+4P9zr>KkeEUmeV_P<47$_P z%8jsdLF15R5Xm)3X~I?OV~G}}aF@Lc#lF!bvFQGekmQ2?!z zNft$vrt`{T{{`N8j>AP7C`*I!6O>&W2)3$Z4^}%Llq8bRbH=&O=e`zN&#*XBcOj@^ zLafJ43{SyexrzigIE~K}Ru~ZeN0dsXt}p4BX&t*sV2vIOyvS5wiLoM?x$W2l7HCPu zi;+gEb?n(>4uWQg!TR$FDb5`t81@?M^f}k{toW_~Y5Gac>ilky<8Ye;_brlYID}dH zLp9`8o?N4u)aWHXizwM-eFE?!37Y;trvK_)_5!Er;uUzvxQK1CKp&+h&tvE+yDRo diff --git a/app/db/__pycache__/models.cpython-312.pyc b/app/db/__pycache__/models.cpython-312.pyc index e7219a527e3c7c0cc74736a5ed16b52edf19a797..b63691322b0dbe0ed0be9c1b6e5eba6d1fc050eb 100644 GIT binary patch literal 6624 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 L_6rUg%-#PBicb&E delta 1648 zcma)6O>7%Q6rS0ve|GI%$NzsxozP0|RwQ*H3Zf`UTcD+>l}IE~ge4TMvs2Y%*G^|0 zBC9}&K%8UdK)7buYIH0Nr#03tOKqTElix53WT{X@a;F>y!Yn4 zZ|wc~>933WH=3p*{tSHd!-ZzFt!E0SB!oUd7P7F7D)5$U>_`;}1MjojC$n)!t*B0@ z5)zVZhaIibzL>yT`#l2>wLdBN6iX!%yDp`RvK1l$2@$^)-okJYLR)>^H&+Z5Y zrM!~hieg2&nZ|!*_V3n>vIkODNs8Rqe(r8vOaIqXdvY(QBySB{;BP3*kDD>DC!aE1!anV5usd4n z+LSEe5OhL)03lZPtzqTbSH1$qQTD6@=~Z)Sd2yw&vTB=6 z;<8K1K$1Soi+Z6sveax)kuzgt**&F@6Dg8&^uH2{U~4D38r>pqdVy*IsrNF zzbU8ht0*yabEtG9_kFzlcKMc`+X*IjVk6y<-w}`daA|~|$YozMdnuLPiUZ88UOJ9O&CH8q_=wKP>BbGy}qTdQ9?gq>Cx0OBf z#<_V}_DlbVc#gdh&7_`!SmELk&Q6Oo`U==sd3?J((hJ(P`ZBR#ZZNH6o%S27_x-F!djSZhF{13wiePwOXq-< z0KgXdGQh(f#`qd8PQaU-xYKMqe(uTjv)!K^>=^@|OzNwS=d-G#R+Yks&{N%uwrMY2 zAWn-#(g%;x5UBb;>S%*H6h0E&YwUk$r`cNJf5_L;xkKVkMUSFAG4XN85w|I9n_yq2 z2aAM@@F8}etCp!pOCza&8W19y$h?7n!h+o9rU|^k8gzp~O)c-)72n+rO^7v#@ diff --git a/app/db/models.py b/app/db/models.py index a99459a..dd4af74 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -1,10 +1,11 @@ import enum -from db.db import Base from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Table, Text, func from sqlalchemy.orm import declarative_mixin, relationship from sqlalchemy.types import Enum +from db.db import Base + @declarative_mixin class TimestampMixin: @@ -48,6 +49,27 @@ company_sector_association = Table( Column("sector_id", Integer, ForeignKey("sectors.id")), ) +project_sector_association = Table( + "project_sector", + Base.metadata, + Column("project_id", Integer, ForeignKey("projects.id")), + Column("sector_id", Integer, ForeignKey("sectors.id")), +) + +project_investor_association = Table( + "project_investors", + Base.metadata, + Column("project_id", Integer, ForeignKey("projects.id")), + Column("investor_id", Integer, ForeignKey("investors.id")), +) + +project_company_association = Table( + "project_companies", + Base.metadata, + Column("project_id", Integer, ForeignKey("projects.id")), + Column("company_id", Integer, ForeignKey("companies.id")), +) + class InvestorTable(Base, TimestampMixin): __tablename__ = "investors" @@ -62,19 +84,27 @@ class InvestorTable(Base, TimestampMixin): stage_focus = Column(Enum(InvestmentStage), nullable=True) number_of_investments = Column(Integer, default=0, nullable=True) + team_members = relationship("InvestorMember", back_populates="investor") + # Relationship to portfolio companies portfolio_companies = relationship( "CompanyTable", secondary=investor_company_association, back_populates="investors", ) - team_members = relationship("InvestorMember", back_populates="investor") + sectors = relationship( "SectorTable", secondary=investor_sector_association, back_populates="investors", ) + projects = relationship( + "ProjectTable", + secondary=project_investor_association, + back_populates="investors", + ) + class InvestorMember(Base, TimestampMixin): __tablename__ = "investor_members" @@ -110,6 +140,12 @@ class CompanyTable(Base, TimestampMixin): "SectorTable", secondary=company_sector_association, back_populates="companies" ) + projects = relationship( + "ProjectTable", + secondary=project_company_association, + back_populates="companies", + ) + class CompanyMember(Base, TimestampMixin): __tablename__ = "company_members" @@ -138,3 +174,33 @@ class SectorTable(Base, TimestampMixin): companies = relationship( "CompanyTable", secondary=company_sector_association, back_populates="sectors" ) + + projects = relationship( + "ProjectTable", secondary=project_sector_association, back_populates="sector" + ) + + +class ProjectTable(Base, TimestampMixin): + __tablename__ = "projects" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + valuation = Column(Integer, nullable=True) + + stage = Column(Enum(InvestmentStage), nullable=True) + location = Column(String, nullable=True) + description = Column(Text, nullable=True) + start_date = Column(DateTime, nullable=True) + end_date = Column(DateTime, nullable=True) + + sector = relationship( + "SectorTable", secondary=project_sector_association, back_populates="projects" + ) + investors = relationship( + "InvestorTable", + secondary=project_investor_association, + back_populates="projects", + ) + companies = relationship( + "CompanyTable", secondary=project_company_association, back_populates="projects" + ) diff --git a/app/main.py b/app/main.py index c32d579..14f0f39 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from db.db import Base, db_dependency, engine from dotenv import load_dotenv from fastapi import FastAPI, File, Form, UploadFile from pydantic import BaseModel -from routers import companies, investors +from routers import companies, investors, projects from schemas.router_schemas import InvestorList from services.llm_parser import InvestorProcessor from services.querying import QueryProcessor @@ -78,6 +78,8 @@ async def query_investors(request: QueryRequest): app.include_router(investors.router) app.include_router(companies.router) +app.include_router(projects.router) + if __name__ == "__main__": import uvicorn diff --git a/app/routers/__pycache__/investors.cpython-312.pyc b/app/routers/__pycache__/investors.cpython-312.pyc index 76537986cb542ac2716e9dff34f64aa55ea41a59..0c218269bac2c6eff23958abb5b6568ec7af1a2f 100644 GIT binary patch delta 22 ccmccVebbxwG%qg~0}wo$em!I3M&2{309_~uVE_OC delta 22 ccmccVebbxwG%qg~0}$|~UCmgxk@t)$08{G+Qvd(} diff --git a/app/routers/investors.py b/app/routers/investors.py index d687b8c..d7078fc 100644 --- a/app/routers/investors.py +++ b/app/routers/investors.py @@ -231,4 +231,6 @@ def delete_investor(investor_id: int, db: Session = Depends(get_db)): db.delete(db_investor) db.commit() - return {"message": "Investor deleted successfully"} \ No newline at end of file + return {"message": "Investor deleted successfully"} + + diff --git a/app/routers/projects.py b/app/routers/projects.py new file mode 100644 index 0000000..ec49cd2 --- /dev/null +++ b/app/routers/projects.py @@ -0,0 +1,447 @@ +from typing import List, Optional + +from db.db import get_db +from db.models import ( + CompanyTable, + InvestorTable, + ProjectTable, + SectorTable, +) +from fastapi import APIRouter, Depends, HTTPException, Query +from schemas.project_schemas import ( + InvestmentStage, + ProjectCreate, + ProjectData, + ProjectUpdate, +) +from sqlalchemy.orm import Session, selectinload + +router = APIRouter(tags=["Project Routes"]) + + +@router.get("/projects", response_model=List[ProjectData]) +def read_projects(db: Session = Depends(get_db)): + """Get all projects with their related data""" + projects = ( + db.query(ProjectTable) + .options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + .all() + ) + + # Transform ProjectTable objects to ProjectData format + project_data_list = [] + for project in projects: + project_data = ProjectData( + project=project, + sector=project.sector, + investors=project.investors, + companies=project.companies, + ) + project_data_list.append(project_data) + + return project_data_list + + +@router.get("/projects/{project_id}", response_model=ProjectData) +def read_project(project_id: int, db: Session = Depends(get_db)): + """Get a specific project by ID""" + project = ( + db.query(ProjectTable) + .options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + .filter(ProjectTable.id == project_id) + .first() + ) + + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + return ProjectData( + project=project, + sector=project.sector, + investors=project.investors, + companies=project.companies, + ) + + +@router.post("/projects", response_model=ProjectData) +def create_project(project: ProjectCreate, db: Session = Depends(get_db)): + """Create a new project""" + db_project = ProjectTable(**project.dict()) + db.add(db_project) + db.commit() + db.refresh(db_project) + + # Reload with relationships + db_project = ( + db.query(ProjectTable) + .options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + .filter(ProjectTable.id == db_project.id) + .first() + ) + + return ProjectData( + project=db_project, + sector=db_project.sector, + investors=db_project.investors, + companies=db_project.companies, + ) + + +@router.put("/projects/{project_id}", response_model=ProjectData) +def update_project( + project_id: int, project: ProjectUpdate, db: Session = Depends(get_db) +): + """Update an existing project""" + db_project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + + if not db_project: + raise HTTPException(status_code=404, detail="Project not found") + + # Update only provided fields + update_data = project.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_project, key, value) + + db.commit() + db.refresh(db_project) + + # Reload with relationships + db_project = ( + db.query(ProjectTable) + .options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + .filter(ProjectTable.id == project_id) + .first() + ) + + return ProjectData( + project=db_project, + sector=db_project.sector, + investors=db_project.investors, + companies=db_project.companies, + ) + + +@router.delete("/projects/{project_id}") +def delete_project(project_id: int, db: Session = Depends(get_db)): + """Delete a project""" + db_project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + + if not db_project: + raise HTTPException(status_code=404, detail="Project not found") + + db.delete(db_project) + db.commit() + + return {"message": "Project deleted successfully"} + + +@router.get("/projects/filter", response_model=List[ProjectData]) +def filter_projects( + stage: Optional[InvestmentStage] = Query( + None, description="Filter by project stage" + ), + min_valuation: Optional[int] = Query(None, description="Minimum valuation"), + max_valuation: Optional[int] = Query(None, description="Maximum valuation"), + location: Optional[str] = Query(None, description="Location (partial match)"), + sector: Optional[str] = Query(None, description="Sector name (partial match)"), + investor_name: Optional[str] = Query( + None, description="Investor name (partial match)" + ), + company_name: Optional[str] = Query( + None, description="Company name (partial match)" + ), + db: Session = Depends(get_db), +): + """Filter projects based on various criteria""" + + # Start with base query + query = db.query(ProjectTable).options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + + # Apply filters + if stage: + query = query.filter(ProjectTable.stage == stage) + + if min_valuation is not None: + query = query.filter(ProjectTable.valuation >= min_valuation) + + if max_valuation is not None: + query = query.filter(ProjectTable.valuation <= max_valuation) + + if location: + query = query.filter(ProjectTable.location.ilike(f"%{location}%")) + + if sector: + query = query.join(ProjectTable.sector).filter( + SectorTable.name.ilike(f"%{sector}%") + ) + + if investor_name: + query = query.join(ProjectTable.investors).filter( + InvestorTable.name.ilike(f"%{investor_name}%") + ) + + if company_name: + query = query.join(ProjectTable.companies).filter( + CompanyTable.name.ilike(f"%{company_name}%") + ) + + projects = query.all() + + # Transform to ProjectData format + project_data_list = [] + for project in projects: + project_data = ProjectData( + project=project, + sector=project.sector, + investors=project.investors, + companies=project.companies, + ) + project_data_list.append(project_data) + + return project_data_list + + +# Association management routes +@router.post("/projects/{project_id}/investors/{investor_id}") +def add_investor_to_project( + project_id: int, investor_id: int, db: Session = Depends(get_db) +): + """Add an investor to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if investor exists + investor = db.query(InvestorTable).filter(InvestorTable.id == investor_id).first() + if not investor: + raise HTTPException(status_code=404, detail="Investor not found") + + # Check if association already exists + if investor in project.investors: + raise HTTPException( + status_code=400, detail="Investor already associated with project" + ) + + # Add association + project.investors.append(investor) + db.commit() + + return {"message": "Investor added to project successfully"} + + +@router.delete("/projects/{project_id}/investors/{investor_id}") +def remove_investor_from_project( + project_id: int, investor_id: int, db: Session = Depends(get_db) +): + """Remove an investor from a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if investor exists + investor = db.query(InvestorTable).filter(InvestorTable.id == investor_id).first() + if not investor: + raise HTTPException(status_code=404, detail="Investor not found") + + # Check if association exists + if investor not in project.investors: + raise HTTPException( + status_code=400, detail="Investor not associated with project" + ) + + # Remove association + project.investors.remove(investor) + db.commit() + + return {"message": "Investor removed from project successfully"} + + +@router.post("/projects/{project_id}/companies/{company_id}") +def add_company_to_project( + project_id: int, company_id: int, db: Session = Depends(get_db) +): + """Add a company to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if company exists + company = db.query(CompanyTable).filter(CompanyTable.id == company_id).first() + if not company: + raise HTTPException(status_code=404, detail="Company not found") + + # Check if association already exists + if company in project.companies: + raise HTTPException( + status_code=400, detail="Company already associated with project" + ) + + # Add association + project.companies.append(company) + db.commit() + + return {"message": "Company added to project successfully"} + + +@router.delete("/projects/{project_id}/companies/{company_id}") +def remove_company_from_project( + project_id: int, company_id: int, db: Session = Depends(get_db) +): + """Remove a company from a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if company exists + company = db.query(CompanyTable).filter(CompanyTable.id == company_id).first() + if not company: + raise HTTPException(status_code=404, detail="Company not found") + + # Check if association exists + if company not in project.companies: + raise HTTPException( + status_code=400, detail="Company not associated with project" + ) + + # Remove association + project.companies.remove(company) + db.commit() + + return {"message": "Company removed from project successfully"} + + +@router.post("/projects/{project_id}/sectors/{sector_id}") +def add_sector_to_project( + project_id: int, sector_id: int, db: Session = Depends(get_db) +): + """Add a sector to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if sector exists + sector = db.query(SectorTable).filter(SectorTable.id == sector_id).first() + if not sector: + raise HTTPException(status_code=404, detail="Sector not found") + + # Check if association already exists + if sector in project.sector: + raise HTTPException( + status_code=400, detail="Sector already associated with project" + ) + + # Add association + project.sector.append(sector) + db.commit() + + return {"message": "Sector added to project successfully"} + + +@router.delete("/projects/{project_id}/sectors/{sector_id}") +def remove_sector_from_project( + project_id: int, sector_id: int, db: Session = Depends(get_db) +): + """Remove a sector from a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Check if sector exists + sector = db.query(SectorTable).filter(SectorTable.id == sector_id).first() + if not sector: + raise HTTPException(status_code=404, detail="Sector not found") + + # Check if association exists + if sector not in project.sector: + raise HTTPException( + status_code=400, detail="Sector not associated with project" + ) + + # Remove association + project.sector.remove(sector) + db.commit() + + return {"message": "Sector removed from project successfully"} + + +# Bulk association management +@router.post("/projects/{project_id}/investors") +def add_multiple_investors_to_project( + project_id: int, investor_ids: List[int], db: Session = Depends(get_db) +): + """Add multiple investors to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Get all investors + investors = db.query(InvestorTable).filter(InvestorTable.id.in_(investor_ids)).all() + + if len(investors) != len(investor_ids): + raise HTTPException(status_code=404, detail="One or more investors not found") + + # Add associations (only if not already associated) + added_count = 0 + for investor in investors: + if investor not in project.investors: + project.investors.append(investor) + added_count += 1 + + db.commit() + + return {"message": f"Added {added_count} investors to project successfully"} + + +@router.post("/projects/{project_id}/companies") +def add_multiple_companies_to_project( + project_id: int, company_ids: List[int], db: Session = Depends(get_db) +): + """Add multiple companies to a project""" + # Check if project exists + project = db.query(ProjectTable).filter(ProjectTable.id == project_id).first() + if not project: + raise HTTPException(status_code=404, detail="Project not found") + + # Get all companies + companies = db.query(CompanyTable).filter(CompanyTable.id.in_(company_ids)).all() + + if len(companies) != len(company_ids): + raise HTTPException(status_code=404, detail="One or more companies not found") + + # Add associations (only if not already associated) + added_count = 0 + for company in companies: + if company not in project.companies: + project.companies.append(company) + added_count += 1 + + db.commit() + + return {"message": f"Added {added_count} companies to project successfully"} diff --git a/app/schemas/__pycache__/py_schemas.cpython-312.pyc b/app/schemas/__pycache__/py_schemas.cpython-312.pyc index e89ff8e2ffea520a8a35f35fe0796912c1ee75c7..9dd38935d4f4edc7ed7ba23d3f3779335720d3fb 100644 GIT binary patch delta 20 acmcZ`ayNwgG%qg~0}!-MyuOh;NDBZ;4F-Y$ delta 20 acmcZ`ayNwgG%qg~0}v#wyta`$NDBZ-qXu>W diff --git a/app/schemas/project_schemas.py b/app/schemas/project_schemas.py new file mode 100644 index 0000000..51663a3 --- /dev/null +++ b/app/schemas/project_schemas.py @@ -0,0 +1,117 @@ +from datetime import datetime +from enum import Enum +from typing import List, Optional + +from pydantic import BaseModel + + +class InvestmentStage(str, Enum): + SEED = "SEED" + SERIES_A = "SERIES_A" + SERIES_B = "SERIES_B" + SERIES_C = "SERIES_C" + GROWTH = "GROWTH" + LATE_STAGE = "LATE_STAGE" + + +class SectorSchema(BaseModel): + id: int + name: str + + class Config: + from_attributes = True + + +class InvestorSchema(BaseModel): + id: int + name: str + description: Optional[str] + aum: int | None + check_size_lower: int | None + check_size_upper: int | None + geographic_focus: str | None + stage_focus: InvestmentStage + number_of_investments: int | None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class CompanySchema(BaseModel): + id: int + name: str + industry: str | None + location: str | None + description: Optional[str] + founded_year: Optional[int] + website: Optional[str] + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class ProjectSchema(BaseModel): + id: int + name: str + valuation: int | None + stage: InvestmentStage | None + location: str | None + description: Optional[str] + start_date: Optional[datetime] + end_date: Optional[datetime] + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class ProjectCreate(BaseModel): + name: str + valuation: Optional[int] = None + stage: Optional[InvestmentStage] = None + location: Optional[str] = None + description: Optional[str] = None + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + + +class ProjectUpdate(BaseModel): + name: Optional[str] = None + valuation: Optional[int] = None + stage: Optional[InvestmentStage] = None + location: Optional[str] = None + description: Optional[str] = None + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + + +class ProjectData(BaseModel): + """Comprehensive project data schema""" + + project: ProjectSchema + sector: List[SectorSchema] + investors: List[InvestorSchema] + companies: List[CompanySchema] + + class Config: + from_attributes = True + + +class ProjectInvestorAssociation(BaseModel): + project_id: int + investor_id: int + + +class ProjectCompanyAssociation(BaseModel): + project_id: int + company_id: int + + +class ProjectSectorAssociation(BaseModel): + project_id: int + sector_id: int diff --git a/app/services/__pycache__/llm_parser.cpython-312.pyc b/app/services/__pycache__/llm_parser.cpython-312.pyc index 17400ff0cf0d4ce2c4bddfea26303c82d06703d7..e64b11789a573cbdc1281bde11e04fe26dbdf560 100644 GIT binary patch delta 64 zcmX??elwl-G%qg~0}vdXa6M!3M&5X4S()_w{Pdhu{q)q_%)CrpBRx~yw4B7^4Bec} TlGM$0%(je-2AlV=2