feat: Update investor and fund schemas for streamlined investment responses

This commit is contained in:
bolade
2025-10-08 11:48:26 +01:00
parent faf92a3b47
commit 37e1ad01c4
5 changed files with 284 additions and 277 deletions
Binary file not shown.
Binary file not shown.
+220 -273
View File
@@ -1,14 +1,16 @@
from typing import Optional from typing import Optional
from db.db import get_db from db.db import get_db
from db.models import InvestorTable, SectorTable from db.models import FundTable, InvestorTable, SectorTable
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel from pydantic import BaseModel
from schemas.router_schemas import ( from schemas.router_schemas import (
CompanyMinimal,
InvestmentResponse,
InvestmentStage, InvestmentStage,
InvestorData, InvestorData,
InvestorFundData,
PaginatedResponse, PaginatedResponse,
SectorMinimal,
) )
from sqlalchemy.orm import Session, selectinload from sqlalchemy.orm import Session, selectinload
@@ -40,7 +42,7 @@ class InvestorUpdate(BaseModel):
number_of_investments: Optional[int] = None number_of_investments: Optional[int] = None
@router.get("/investors", response_model=PaginatedResponse[InvestorFundData]) @router.get("/investors", response_model=PaginatedResponse[InvestmentResponse])
def read_investors( def read_investors(
page: int = Query(1, ge=1, description="Page number (starts at 1)"), page: int = Query(1, ge=1, description="Page number (starts at 1)"),
page_size: int = Query(10, ge=1, le=100, description="Items per page (max 100)"), page_size: int = Query(10, ge=1, le=100, description="Items per page (max 100)"),
@@ -71,78 +73,67 @@ def read_investors(
.all() .all()
) )
# Transform to InvestorFundData format (one row per investor-fund combination) # Transform to InvestmentResponse format (one row per investor-fund combination)
investor_fund_list = [] investment_responses = []
for investor in investors: for investor in investors:
# Get top 3 portfolio companies (id and name only)
portfolio_companies = [
CompanyMinimal(id=company.id, name=company.name)
for company in investor.portfolio_companies[:3]
]
# If investor has funds, create one entry per fund # If investor has funds, create one entry per fund
if investor.funds: if investor.funds:
for fund in investor.funds: for fund in investor.funds:
investor_fund_data = InvestorFundData( # Get stage focus as comma-separated string
# Investor fields stage_focus = (
investor_id=investor.id, ", ".join([stage.name for stage in fund.investment_stages])
investor_name=investor.name, if fund.investment_stages
investor_description=investor.description, else None
investor_website=investor.website, )
investor_headquarters=investor.headquarters,
# Get top 3 sectors from fund (id and name only)
fund_sectors = [
SectorMinimal(id=sector.id, name=sector.name)
for sector in (fund.sectors[:3] if fund.sectors else [])
]
investment_response = InvestmentResponse(
id=investor.id,
name=f"{investor.name} - {fund.fund_name}"
if fund.fund_name
else investor.name,
aum=investor.aum, aum=investor.aum,
aum_as_of_date=investor.aum_as_of_date,
aum_source_url=investor.aum_source_url,
investment_thesis=investor.investment_thesis,
portfolio_highlights=investor.portfolio_highlights,
number_of_investments=investor.number_of_investments,
# Fund fields
fund_id=fund.id,
fund_name=fund.fund_name,
fund_size=fund.fund_size,
fund_size_source_url=fund.fund_size_source_url,
check_size_lower=fund.check_size_lower, check_size_lower=fund.check_size_lower,
check_size_upper=fund.check_size_upper, check_size_upper=fund.check_size_upper,
geographic_focus=fund.geographic_focus, geographic_focus=fund.geographic_focus,
fund_investment_stages=fund.investment_stages, # Now a relationship stage_focus=stage_focus,
fund_sectors=fund.sectors, # Now a relationship portfolio_companies=portfolio_companies,
# Related data (same for all funds of this investor) sectors=fund_sectors,
portfolio_companies=investor.portfolio_companies, compatibility_score=1.0,
team_members=investor.team_members,
sectors=investor.sectors,
) )
investor_fund_list.append(investor_fund_data) investment_responses.append(investment_response)
else: else:
# If no funds, create one entry with null fund fields # If no funds, create one entry with null fund fields
investor_fund_data = InvestorFundData( investment_response = InvestmentResponse(
# Investor fields id=investor.id,
investor_id=investor.id, name=investor.name,
investor_name=investor.name,
investor_description=investor.description,
investor_website=investor.website,
investor_headquarters=investor.headquarters,
aum=investor.aum, aum=investor.aum,
aum_as_of_date=investor.aum_as_of_date,
aum_source_url=investor.aum_source_url,
investment_thesis=investor.investment_thesis,
portfolio_highlights=investor.portfolio_highlights,
number_of_investments=investor.number_of_investments,
# Fund fields (null)
fund_id=None,
fund_name=None,
fund_size=None,
fund_size_source_url=None,
check_size_lower=None, check_size_lower=None,
check_size_upper=None, check_size_upper=None,
geographic_focus=None, geographic_focus=None,
fund_investment_stages=None, stage_focus=None,
fund_sectors=None, portfolio_companies=portfolio_companies,
# Related data sectors=[],
portfolio_companies=investor.portfolio_companies, compatibility_score=1.0,
team_members=investor.team_members,
sectors=investor.sectors,
) )
investor_fund_list.append(investor_fund_data) investment_responses.append(investment_response)
# Calculate total pages # Calculate total pages
total_pages = (total_count + page_size - 1) // page_size total_pages = (total_count + page_size - 1) // page_size
return PaginatedResponse( return PaginatedResponse(
items=investor_fund_list, items=investment_responses,
total=total_count, total=total_count,
page=page, page=page,
page_size=page_size, page_size=page_size,
@@ -150,7 +141,7 @@ def read_investors(
) )
@router.get("/investors/filter", response_model=PaginatedResponse[InvestorFundData]) @router.get("/investors/filter", response_model=PaginatedResponse[InvestmentResponse])
def filter_investors( def filter_investors(
stage: Optional[InvestmentStage] = Query( stage: Optional[InvestmentStage] = Query(
None, description="Filter by investment stage" None, description="Filter by investment stage"
@@ -170,40 +161,42 @@ def filter_investors(
"""Filter investors based on various criteria (paginated) """Filter investors based on various criteria (paginated)
Returns investor-fund combinations as separate rows. Returns investor-fund combinations as separate rows.
An investor with 3 funds will appear as 3 entries. Queries the funds table to find matching funds.
""" """
# Start with base query # Start with base query on funds table
query = db.query(InvestorTable).options( query = db.query(FundTable).options(
selectinload(InvestorTable.portfolio_companies), selectinload(FundTable.investor).selectinload(
selectinload(InvestorTable.team_members), InvestorTable.portfolio_companies
selectinload(InvestorTable.sectors), ),
selectinload(InvestorTable.funds), selectinload(FundTable.investor).selectinload(InvestorTable.team_members),
selectinload(FundTable.investor).selectinload(InvestorTable.sectors),
selectinload(FundTable.investment_stages),
selectinload(FundTable.sectors),
) )
# Apply filters # Apply filters at fund level
# Note: stage filtering is now done at fund level via fund.investment_stages
# if stage:
# query = query.filter(InvestorTable.stage_focus == stage)
if min_check_size is not None: if min_check_size is not None:
query = query.filter(InvestorTable.check_size_lower >= min_check_size) query = query.filter(FundTable.check_size_lower >= min_check_size)
if max_check_size is not None: if max_check_size is not None:
query = query.filter(InvestorTable.check_size_upper <= max_check_size) query = query.filter(FundTable.check_size_upper <= max_check_size)
if geography: if geography:
query = query.filter(InvestorTable.geographic_focus.ilike(f"%{geography}%")) query = query.filter(FundTable.geographic_focus.ilike(f"%{geography}%"))
# Apply filters at investor level (through relationship)
if min_aum is not None: if min_aum is not None:
query = query.filter(InvestorTable.aum >= min_aum) query = query.join(FundTable.investor).filter(InvestorTable.aum >= min_aum)
if max_aum is not None: if max_aum is not None:
if min_aum is None: # Only join if not already joined
query = query.join(FundTable.investor)
query = query.filter(InvestorTable.aum <= max_aum) query = query.filter(InvestorTable.aum <= max_aum)
# Filter by sector if provided # Filter by sector if provided (at fund level)
if sector: if sector:
query = query.join(InvestorTable.sectors).filter( query = query.join(FundTable.sectors).filter(
SectorTable.name.ilike(f"%{sector}%") SectorTable.name.ilike(f"%{sector}%")
) )
@@ -212,80 +205,53 @@ def filter_investors(
# Calculate offset and apply pagination # Calculate offset and apply pagination
offset = (page - 1) * page_size offset = (page - 1) * page_size
investors = query.offset(offset).limit(page_size).all() funds = query.offset(offset).limit(page_size).all()
# Transform to InvestorFundData format (one row per investor-fund combination) # Transform to InvestmentResponse format (one row per fund)
investor_fund_list = [] investment_responses = []
for investor in investors: for fund in funds:
# If investor has funds, create one entry per fund investor = fund.investor
if investor.funds:
for fund in investor.funds: # Get top 3 portfolio companies (id and name only)
investor_fund_data = InvestorFundData( portfolio_companies = [
# Investor fields CompanyMinimal(id=company.id, name=company.name)
investor_id=investor.id, for company in investor.portfolio_companies[:3]
investor_name=investor.name, ]
investor_description=investor.description,
investor_website=investor.website, # Get stage focus as comma-separated string
investor_headquarters=investor.headquarters, stage_focus = (
aum=investor.aum, ", ".join([stage.name for stage in fund.investment_stages])
aum_as_of_date=investor.aum_as_of_date, if fund.investment_stages
aum_source_url=investor.aum_source_url, else None
investment_thesis=investor.investment_thesis, )
portfolio_highlights=investor.portfolio_highlights,
number_of_investments=investor.number_of_investments, # Get top 3 sectors from fund (id and name only)
# Fund fields fund_sectors = [
fund_id=fund.id, SectorMinimal(id=sector.id, name=sector.name)
fund_name=fund.fund_name, for sector in (fund.sectors[:3] if fund.sectors else [])
fund_size=fund.fund_size, ]
fund_size_source_url=fund.fund_size_source_url,
check_size_lower=fund.check_size_lower, investment_response = InvestmentResponse(
check_size_upper=fund.check_size_upper, id=investor.id,
geographic_focus=fund.geographic_focus, name=f"{investor.name} - {fund.fund_name}"
fund_investment_stages=fund.investment_stages, # Now a relationship if fund.fund_name
fund_sectors=fund.sectors, # Now a relationship else investor.name,
# Related data aum=investor.aum,
portfolio_companies=investor.portfolio_companies, check_size_lower=fund.check_size_lower,
team_members=investor.team_members, check_size_upper=fund.check_size_upper,
sectors=investor.sectors, geographic_focus=fund.geographic_focus,
) stage_focus=stage_focus,
investor_fund_list.append(investor_fund_data) portfolio_companies=portfolio_companies,
else: sectors=fund_sectors,
# If no funds, create one entry with null fund fields compatibility_score=1.0,
investor_fund_data = InvestorFundData( )
# Investor fields investment_responses.append(investment_response)
investor_id=investor.id,
investor_name=investor.name,
investor_description=investor.description,
investor_website=investor.website,
investor_headquarters=investor.headquarters,
aum=investor.aum,
aum_as_of_date=investor.aum_as_of_date,
aum_source_url=investor.aum_source_url,
investment_thesis=investor.investment_thesis,
portfolio_highlights=investor.portfolio_highlights,
number_of_investments=investor.number_of_investments,
# Fund fields (null)
fund_id=None,
fund_name=None,
fund_size=None,
fund_size_source_url=None,
check_size_lower=None,
check_size_upper=None,
geographic_focus=None,
fund_investment_stages=None,
fund_sectors=None,
# Related data
portfolio_companies=investor.portfolio_companies,
team_members=investor.team_members,
sectors=investor.sectors,
)
investor_fund_list.append(investor_fund_data)
# Calculate total pages # Calculate total pages
total_pages = (total_count + page_size - 1) // page_size total_pages = (total_count + page_size - 1) // page_size
return PaginatedResponse( return PaginatedResponse(
items=investor_fund_list, items=investment_responses,
total=total_count, total=total_count,
page=page, page=page,
page_size=page_size, page_size=page_size,
@@ -409,7 +375,7 @@ def delete_investor(investor_id: int, db: Session = Depends(get_db)):
@router.get( @router.get(
"/investors/{investor_id}/similar", "/investors/{investor_id}/similar",
response_model=PaginatedResponse[InvestorFundData], response_model=PaginatedResponse[InvestmentResponse],
) )
def find_similar_investors( def find_similar_investors(
investor_id: int, investor_id: int,
@@ -421,16 +387,18 @@ def find_similar_investors(
"""Find investors similar to a given investor based on characteristics (paginated) """Find investors similar to a given investor based on characteristics (paginated)
Returns investor-fund combinations as separate rows. Returns investor-fund combinations as separate rows.
Queries the funds table to find matching funds.
""" """
# Get the target investor # Get the target investor to get their funds for comparison
target_investor = ( target_investor = (
db.query(InvestorTable) db.query(InvestorTable)
.options( .options(
selectinload(InvestorTable.portfolio_companies), selectinload(InvestorTable.portfolio_companies),
selectinload(InvestorTable.team_members), selectinload(InvestorTable.team_members),
selectinload(InvestorTable.sectors), selectinload(InvestorTable.sectors),
selectinload(InvestorTable.funds), selectinload(InvestorTable.funds).selectinload(FundTable.investment_stages),
selectinload(InvestorTable.funds).selectinload(FundTable.sectors),
) )
.filter(InvestorTable.id == investor_id) .filter(InvestorTable.id == investor_id)
.first() .first()
@@ -439,168 +407,147 @@ def find_similar_investors(
if not target_investor: if not target_investor:
raise HTTPException(status_code=404, detail="Investor not found") raise HTTPException(status_code=404, detail="Investor not found")
# Get target investor's sector IDs for comparison # Get target investor's sector IDs for comparison (from their funds)
target_sector_ids = {sector.id for sector in target_investor.sectors} target_sector_ids = set()
target_stage_ids = set()
target_check_ranges = []
target_geographies = []
# Query all other investors with their relationships for fund in target_investor.funds:
candidates = ( if fund.sectors:
db.query(InvestorTable) target_sector_ids.update({sector.id for sector in fund.sectors})
if fund.investment_stages:
target_stage_ids.update({stage.id for stage in fund.investment_stages})
if fund.check_size_lower and fund.check_size_upper:
target_check_ranges.append((fund.check_size_lower, fund.check_size_upper))
if fund.geographic_focus:
target_geographies.append(fund.geographic_focus.lower())
# Query all funds from other investors
candidate_funds = (
db.query(FundTable)
.options( .options(
selectinload(InvestorTable.portfolio_companies), selectinload(FundTable.investor).selectinload(
selectinload(InvestorTable.team_members), InvestorTable.portfolio_companies
selectinload(InvestorTable.sectors), ),
selectinload(InvestorTable.funds), selectinload(FundTable.investor).selectinload(InvestorTable.team_members),
selectinload(FundTable.investor).selectinload(InvestorTable.sectors),
selectinload(FundTable.investment_stages),
selectinload(FundTable.sectors),
) )
.join(FundTable.investor)
.filter(InvestorTable.id != investor_id) .filter(InvestorTable.id != investor_id)
.all() .all()
) )
# Calculate similarity scores # Calculate similarity scores for each fund
scored_investors = [] scored_funds = []
for candidate in candidates: for fund in candidate_funds:
score = 0 score = 0
# Stage focus match is now handled at fund level
# Skip stage matching at investor level since stage_focus no longer exists
# if candidate.stage_focus == target_investor.stage_focus:
# score += 30
# Geographic focus match (20 points for exact, 10 for partial) # Geographic focus match (20 points for exact, 10 for partial)
if candidate.geographic_focus and target_investor.geographic_focus: if fund.geographic_focus and target_geographies:
if ( fund_geo_lower = fund.geographic_focus.lower()
candidate.geographic_focus.lower() for target_geo in target_geographies:
== target_investor.geographic_focus.lower() if fund_geo_lower == target_geo:
): score += 20
score += 20 break
elif ( elif fund_geo_lower in target_geo or target_geo in fund_geo_lower:
candidate.geographic_focus.lower() score += 10
in target_investor.geographic_focus.lower() break
or target_investor.geographic_focus.lower()
in candidate.geographic_focus.lower()
):
score += 10
# Check size overlap (20 points max) # Check size overlap (20 points max)
if ( if fund.check_size_lower and fund.check_size_upper and target_check_ranges:
candidate.check_size_lower max_overlap_score = 0
and candidate.check_size_upper for target_lower, target_upper in target_check_ranges:
and target_investor.check_size_lower overlap_start = max(fund.check_size_lower, target_lower)
and target_investor.check_size_upper overlap_end = min(fund.check_size_upper, target_upper)
): if overlap_end > overlap_start:
# Calculate overlap percentage overlap = overlap_end - overlap_start
overlap_start = max( target_range = target_upper - target_lower
candidate.check_size_lower, target_investor.check_size_lower overlap_ratio = overlap / target_range if target_range > 0 else 0
) max_overlap_score = max(max_overlap_score, int(20 * overlap_ratio))
overlap_end = min( score += max_overlap_score
candidate.check_size_upper, target_investor.check_size_upper
)
if overlap_end > overlap_start:
overlap = overlap_end - overlap_start
target_range = (
target_investor.check_size_upper - target_investor.check_size_lower
)
overlap_ratio = overlap / target_range if target_range > 0 else 0
score += int(20 * overlap_ratio)
# AUM similarity (15 points max) # AUM similarity (15 points max)
if candidate.aum and target_investor.aum: if fund.investor.aum and target_investor.aum:
aum_diff = abs(candidate.aum - target_investor.aum) aum_diff = abs(fund.investor.aum - target_investor.aum)
max_aum = max(candidate.aum, target_investor.aum) max_aum = max(fund.investor.aum, target_investor.aum)
similarity_ratio = 1 - (aum_diff / max_aum) if max_aum > 0 else 0 similarity_ratio = 1 - (aum_diff / max_aum) if max_aum > 0 else 0
score += int(15 * similarity_ratio) score += int(15 * similarity_ratio)
# Sector overlap (30 points max) # Sector overlap (30 points max)
candidate_sector_ids = {sector.id for sector in candidate.sectors} if fund.sectors and target_sector_ids:
if target_sector_ids and candidate_sector_ids: fund_sector_ids = {sector.id for sector in fund.sectors}
common_sectors = target_sector_ids.intersection(candidate_sector_ids) common_sectors = target_sector_ids.intersection(fund_sector_ids)
overlap_ratio = len(common_sectors) / len(target_sector_ids) overlap_ratio = len(common_sectors) / len(target_sector_ids)
score += int(30 * overlap_ratio) score += int(30 * overlap_ratio)
if score > 0: # Only include investors with some similarity # Investment stage match (15 points max)
scored_investors.append((score, candidate)) if fund.investment_stages and target_stage_ids:
fund_stage_ids = {stage.id for stage in fund.investment_stages}
common_stages = target_stage_ids.intersection(fund_stage_ids)
overlap_ratio = len(common_stages) / len(target_stage_ids)
score += int(15 * overlap_ratio)
if score > 0: # Only include funds with some similarity
scored_funds.append((score, fund))
# Sort by score (descending) and take top N based on limit # Sort by score (descending) and take top N based on limit
scored_investors.sort(key=lambda x: x[0], reverse=True) scored_funds.sort(key=lambda x: x[0], reverse=True)
top_similar = scored_investors[:limit] top_similar = scored_funds[:limit]
# Apply pagination to the top similar investors # Apply pagination to the top similar funds
total_count = len(top_similar) total_count = len(top_similar)
offset = (page - 1) * page_size offset = (page - 1) * page_size
paginated_similar = top_similar[offset : offset + page_size] paginated_similar = top_similar[offset : offset + page_size]
similar_investors = [inv for score, inv in paginated_similar] similar_funds = [fund for score, fund in paginated_similar]
# Transform to InvestorFundData format (one row per investor-fund combination) # Transform to InvestmentResponse format (one row per fund)
investor_fund_list = [] investment_responses = []
for investor in similar_investors: for fund in similar_funds:
# If investor has funds, create one entry per fund investor = fund.investor
if investor.funds:
for fund in investor.funds: # Get top 3 portfolio companies (id and name only)
investor_fund_data = InvestorFundData( portfolio_companies = [
# Investor fields CompanyMinimal(id=company.id, name=company.name)
investor_id=investor.id, for company in investor.portfolio_companies[:3]
investor_name=investor.name, ]
investor_description=investor.description,
investor_website=investor.website, # Get stage focus as comma-separated string
investor_headquarters=investor.headquarters, stage_focus = (
aum=investor.aum, ", ".join([stage.name for stage in fund.investment_stages])
aum_as_of_date=investor.aum_as_of_date, if fund.investment_stages
aum_source_url=investor.aum_source_url, else None
investment_thesis=investor.investment_thesis, )
portfolio_highlights=investor.portfolio_highlights,
number_of_investments=investor.number_of_investments, # Get top 3 sectors from fund (id and name only)
# Fund fields fund_sectors = [
fund_id=fund.id, SectorMinimal(id=sector.id, name=sector.name)
fund_name=fund.fund_name, for sector in (fund.sectors[:3] if fund.sectors else [])
fund_size=fund.fund_size, ]
fund_size_source_url=fund.fund_size_source_url,
check_size_lower=fund.check_size_lower, investment_response = InvestmentResponse(
check_size_upper=fund.check_size_upper, id=investor.id,
geographic_focus=fund.geographic_focus, name=f"{investor.name} - {fund.fund_name}"
fund_investment_stages=fund.investment_stages, # Now a relationship if fund.fund_name
fund_sectors=fund.sectors, # Now a relationship else investor.name,
# Related data aum=investor.aum,
portfolio_companies=investor.portfolio_companies, check_size_lower=fund.check_size_lower,
team_members=investor.team_members, check_size_upper=fund.check_size_upper,
sectors=investor.sectors, geographic_focus=fund.geographic_focus,
) stage_focus=stage_focus,
investor_fund_list.append(investor_fund_data) portfolio_companies=portfolio_companies,
else: sectors=fund_sectors,
# If no funds, create one entry with null fund fields compatibility_score=1.0,
investor_fund_data = InvestorFundData( )
# Investor fields investment_responses.append(investment_response)
investor_id=investor.id,
investor_name=investor.name,
investor_description=investor.description,
investor_website=investor.website,
investor_headquarters=investor.headquarters,
aum=investor.aum,
aum_as_of_date=investor.aum_as_of_date,
aum_source_url=investor.aum_source_url,
investment_thesis=investor.investment_thesis,
portfolio_highlights=investor.portfolio_highlights,
number_of_investments=investor.number_of_investments,
# Fund fields (null)
fund_id=None,
fund_name=None,
fund_size=None,
fund_size_source_url=None,
check_size_lower=None,
check_size_upper=None,
geographic_focus=None,
fund_investment_stages=None,
fund_sectors=None,
# Related data
portfolio_companies=investor.portfolio_companies,
team_members=investor.team_members,
sectors=investor.sectors,
)
investor_fund_list.append(investor_fund_data)
# Calculate total pages # Calculate total pages
total_pages = (total_count + page_size - 1) // page_size total_pages = (total_count + page_size - 1) // page_size
return PaginatedResponse( return PaginatedResponse(
items=investor_fund_list, items=investment_responses,
total=total_count, total=total_count,
page=page, page=page,
page_size=page_size, page_size=page_size,
+64 -4
View File
@@ -168,12 +168,29 @@ class InvestorFundData(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
class InvestorMinimal(BaseModel):
"""Minimal investor info with just id and name"""
id: int
name: str
class Config:
from_attributes = True
class CompanySchemaMinimal(BaseModel):
id: int
name: str
industry: str | None
location: str | None
founded_year: Optional[int]
website: Optional[str]
class Config:
from_attributes = True
class CompanyData(BaseModel): # Renamed from CompaniesData for consistency class CompanyData(BaseModel): # Renamed from CompaniesData for consistency
company: CompanySchema company: CompanySchemaMinimal
sectors: List[SectorSchema] investors: List[InvestorMinimal]
members: List[CompanyMemberSchema]
investors: List[InvestorSchema]
class Config: class Config:
from_attributes = True from_attributes = True
@@ -189,6 +206,49 @@ class InvestorFundList(BaseModel):
investor_funds: List[InvestorFundData] investor_funds: List[InvestorFundData]
class CompanyMinimal(BaseModel):
"""Minimal company info with just id and name"""
id: int
name: str
class Config:
from_attributes = True
class SectorMinimal(BaseModel):
"""Minimal sector info with just id and name"""
id: int
name: str
class Config:
from_attributes = True
class InvestmentResponse(BaseModel):
"""Simplified investment response schema
One row per investor-fund combination with streamlined data
"""
id: int # Investor ID
name: (
str # Combination of investor name and fund name (e.g., "Investor A - Fund A")
)
aum: int | None # From investor
check_size_lower: int | None # From fund
check_size_upper: int | None # From fund
geographic_focus: str | None # From fund
stage_focus: str | None # Comma-separated stages from fund
portfolio_companies: List[CompanyMinimal] # Top 3 companies from investor
sectors: List[SectorMinimal] # Top 3 sectors from fund
compatibility_score: float # 0 to 1 (default 1 for now)
class Config:
from_attributes = True
class PaginatedResponse(BaseModel, Generic[T]): class PaginatedResponse(BaseModel, Generic[T]):
"""Generic paginated response schema""" """Generic paginated response schema"""