From 100e0b2b0c470f64acb27661c2ce943cd2c6c05d Mon Sep 17 00:00:00 2001 From: michael Date: Wed, 26 Nov 2025 08:04:11 +0000 Subject: [PATCH] made improvements --- app/main.py | 2 + app/routers/addition.py | 370 ++++++++++++++++++++++++++++++++++++++++ app/routers/projects.py | 51 ++++++ app/services/insight.py | 2 +- investors.db | Bin 29966336 -> 29978624 bytes 5 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 app/routers/addition.py diff --git a/app/main.py b/app/main.py index a95376d..10cf78a 100644 --- a/app/main.py +++ b/app/main.py @@ -8,6 +8,7 @@ from fastapi import FastAPI, File, Form, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from routers import ( + addition, companies, folk_crm, insight_route, @@ -154,6 +155,7 @@ app.include_router(projects.router) app.include_router(folk_crm.router) app.include_router(insight_route.router) app.include_router(report_route.router) +app.include_router(addition.router) if __name__ == "__main__": import uvicorn diff --git a/app/routers/addition.py b/app/routers/addition.py new file mode 100644 index 0000000..252c687 --- /dev/null +++ b/app/routers/addition.py @@ -0,0 +1,370 @@ +from typing import Optional + +from db.db import get_db +from db.models import FundTable, InvestorTable, SectorTable +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from sqlalchemy.orm import Session + +router = APIRouter(tags=["Additional Routes"]) + + +# Response schemas +class SectorsResponse(BaseModel): + sectors: list[str] + total: int + + +class CountryInfo(BaseModel): + name: str + + +class ContinentInfo(BaseModel): + name: str + countries: list[str] + + +class GeographyResponse(BaseModel): + continents: list[ContinentInfo] + total_continents: int + total_countries: int + + +# Mapping of countries to continents +COUNTRY_TO_CONTINENT = { + # Africa + "Algeria": "Africa", + "Angola": "Africa", + "Benin": "Africa", + "Botswana": "Africa", + "Burkina Faso": "Africa", + "Burundi": "Africa", + "Cameroon": "Africa", + "Cape Verde": "Africa", + "Central African Republic": "Africa", + "Chad": "Africa", + "Comoros": "Africa", + "Congo": "Africa", + "Democratic Republic of the Congo": "Africa", + "Djibouti": "Africa", + "Egypt": "Africa", + "Equatorial Guinea": "Africa", + "Eritrea": "Africa", + "Eswatini": "Africa", + "Ethiopia": "Africa", + "Gabon": "Africa", + "Gambia": "Africa", + "Ghana": "Africa", + "Guinea": "Africa", + "Guinea-Bissau": "Africa", + "Ivory Coast": "Africa", + "Kenya": "Africa", + "Lesotho": "Africa", + "Liberia": "Africa", + "Libya": "Africa", + "Madagascar": "Africa", + "Malawi": "Africa", + "Mali": "Africa", + "Mauritania": "Africa", + "Mauritius": "Africa", + "Morocco": "Africa", + "Mozambique": "Africa", + "Namibia": "Africa", + "Niger": "Africa", + "Nigeria": "Africa", + "Rwanda": "Africa", + "Sao Tome and Principe": "Africa", + "Senegal": "Africa", + "Seychelles": "Africa", + "Sierra Leone": "Africa", + "Somalia": "Africa", + "South Africa": "Africa", + "South Sudan": "Africa", + "Sudan": "Africa", + "Tanzania": "Africa", + "Togo": "Africa", + "Tunisia": "Africa", + "Uganda": "Africa", + "Zambia": "Africa", + "Zimbabwe": "Africa", + # Asia + "Afghanistan": "Asia", + "Armenia": "Asia", + "Azerbaijan": "Asia", + "Bahrain": "Asia", + "Bangladesh": "Asia", + "Bhutan": "Asia", + "Brunei": "Asia", + "Cambodia": "Asia", + "China": "Asia", + "Cyprus": "Asia", + "Georgia": "Asia", + "Hong Kong": "Asia", + "India": "Asia", + "Indonesia": "Asia", + "Iran": "Asia", + "Iraq": "Asia", + "Israel": "Asia", + "Japan": "Asia", + "Jordan": "Asia", + "Kazakhstan": "Asia", + "Kuwait": "Asia", + "Kyrgyzstan": "Asia", + "Laos": "Asia", + "Lebanon": "Asia", + "Malaysia": "Asia", + "Maldives": "Asia", + "Mongolia": "Asia", + "Myanmar": "Asia", + "Nepal": "Asia", + "North Korea": "Asia", + "Oman": "Asia", + "Pakistan": "Asia", + "Palestine": "Asia", + "Philippines": "Asia", + "Qatar": "Asia", + "Saudi Arabia": "Asia", + "Singapore": "Asia", + "South Korea": "Asia", + "Sri Lanka": "Asia", + "Syria": "Asia", + "Taiwan": "Asia", + "Tajikistan": "Asia", + "Thailand": "Asia", + "Timor-Leste": "Asia", + "Turkey": "Asia", + "Turkmenistan": "Asia", + "United Arab Emirates": "Asia", + "UAE": "Asia", + "Uzbekistan": "Asia", + "Vietnam": "Asia", + "Yemen": "Asia", + # Europe + "Albania": "Europe", + "Andorra": "Europe", + "Austria": "Europe", + "Belarus": "Europe", + "Belgium": "Europe", + "Bosnia and Herzegovina": "Europe", + "Bulgaria": "Europe", + "Croatia": "Europe", + "Czech Republic": "Europe", + "Czechia": "Europe", + "Denmark": "Europe", + "Estonia": "Europe", + "Finland": "Europe", + "France": "Europe", + "Germany": "Europe", + "Greece": "Europe", + "Hungary": "Europe", + "Iceland": "Europe", + "Ireland": "Europe", + "Italy": "Europe", + "Kosovo": "Europe", + "Latvia": "Europe", + "Liechtenstein": "Europe", + "Lithuania": "Europe", + "Luxembourg": "Europe", + "Malta": "Europe", + "Moldova": "Europe", + "Monaco": "Europe", + "Montenegro": "Europe", + "Netherlands": "Europe", + "North Macedonia": "Europe", + "Norway": "Europe", + "Poland": "Europe", + "Portugal": "Europe", + "Romania": "Europe", + "Russia": "Europe", + "San Marino": "Europe", + "Serbia": "Europe", + "Slovakia": "Europe", + "Slovenia": "Europe", + "Spain": "Europe", + "Sweden": "Europe", + "Switzerland": "Europe", + "Ukraine": "Europe", + "United Kingdom": "Europe", + "UK": "Europe", + "Vatican City": "Europe", + # North America + "Antigua and Barbuda": "North America", + "Bahamas": "North America", + "Barbados": "North America", + "Belize": "North America", + "Canada": "North America", + "Costa Rica": "North America", + "Cuba": "North America", + "Dominica": "North America", + "Dominican Republic": "North America", + "El Salvador": "North America", + "Grenada": "North America", + "Guatemala": "North America", + "Haiti": "North America", + "Honduras": "North America", + "Jamaica": "North America", + "Mexico": "North America", + "Nicaragua": "North America", + "Panama": "North America", + "Saint Kitts and Nevis": "North America", + "Saint Lucia": "North America", + "Saint Vincent and the Grenadines": "North America", + "Trinidad and Tobago": "North America", + "United States": "North America", + "USA": "North America", + "US": "North America", + # South America + "Argentina": "South America", + "Bolivia": "South America", + "Brazil": "South America", + "Chile": "South America", + "Colombia": "South America", + "Ecuador": "South America", + "Guyana": "South America", + "Paraguay": "South America", + "Peru": "South America", + "Suriname": "South America", + "Uruguay": "South America", + "Venezuela": "South America", + # Oceania + "Australia": "Oceania", + "Fiji": "Oceania", + "Kiribati": "Oceania", + "Marshall Islands": "Oceania", + "Micronesia": "Oceania", + "Nauru": "Oceania", + "New Zealand": "Oceania", + "Palau": "Oceania", + "Papua New Guinea": "Oceania", + "Samoa": "Oceania", + "Solomon Islands": "Oceania", + "Tonga": "Oceania", + "Tuvalu": "Oceania", + "Vanuatu": "Oceania", +} + +# Valid continent names for direct matching +VALID_CONTINENTS = { + "Africa", + "Asia", + "Europe", + "North America", + "South America", + "Oceania", + "Antarctica", +} + + +def extract_countries_from_geographic_focus(geographic_focus: str) -> set[str]: + """ + Extract country names from a geographic_focus string. + Handles comma-separated values, slashes, and various formats. + """ + if not geographic_focus: + return set() + + countries = set() + # Split by common delimiters + parts = geographic_focus.replace("/", ",").replace(";", ",").split(",") + + for part in parts: + cleaned = part.strip() + if cleaned: + # Check if it's a known country + if cleaned in COUNTRY_TO_CONTINENT: + countries.add(cleaned) + # Check for partial matches (e.g., "United States of America" -> "United States") + else: + for country in COUNTRY_TO_CONTINENT.keys(): + if country.lower() in cleaned.lower() or cleaned.lower() in country.lower(): + countries.add(country) + break + + return countries + + +def organize_geography(geographic_data: list[str]) -> dict[str, set[str]]: + """ + Organize geographic data into continents and their countries. + Returns a dict with continent names as keys and sets of countries as values. + """ + continent_countries: dict[str, set[str]] = {} + + for geo_focus in geographic_data: + if not geo_focus: + continue + + # Extract countries from the geographic focus string + countries = extract_countries_from_geographic_focus(geo_focus) + + for country in countries: + continent = COUNTRY_TO_CONTINENT.get(country) + if continent: + if continent not in continent_countries: + continent_countries[continent] = set() + continent_countries[continent].add(country) + + # Also check if the geographic focus itself is a continent + cleaned_geo = geo_focus.strip() + if cleaned_geo in VALID_CONTINENTS: + if cleaned_geo not in continent_countries: + continent_countries[cleaned_geo] = set() + + return continent_countries + + +@router.get("/sectors", response_model=SectorsResponse) +def get_unique_sectors(db: Session = Depends(get_db)): + """ + Get all unique sectors from the database. + Returns a list of sector names sorted alphabetically. + """ + sectors = db.query(SectorTable.name).distinct().order_by(SectorTable.name).all() + sector_names = [s[0] for s in sectors if s[0]] + + return SectorsResponse(sectors=sector_names, total=len(sector_names)) + + +@router.get("/geography", response_model=GeographyResponse) +def get_arranged_geography(db: Session = Depends(get_db)): + """ + Get all unique geographic locations arranged by continent and countries. + Extracts geography from both investors and funds tables. + Returns continents with their associated countries. + """ + # Collect all geographic focus data from investors + investor_geo = ( + db.query(InvestorTable.geographic_focus) + .filter(InvestorTable.geographic_focus.isnot(None)) + .distinct() + .all() + ) + + # Collect all geographic focus data from funds + fund_geo = ( + db.query(FundTable.geographic_focus) + .filter(FundTable.geographic_focus.isnot(None)) + .distinct() + .all() + ) + + # Combine all geographic data + all_geo_data = [g[0] for g in investor_geo] + [g[0] for g in fund_geo] + + # Organize into continents and countries + continent_countries = organize_geography(all_geo_data) + + # Build response + continents = [] + total_countries = 0 + + for continent_name in sorted(continent_countries.keys()): + countries = sorted(continent_countries[continent_name]) + total_countries += len(countries) + continents.append(ContinentInfo(name=continent_name, countries=countries)) + + return GeographyResponse( + continents=continents, + total_continents=len(continents), + total_countries=total_countries, + ) diff --git a/app/routers/projects.py b/app/routers/projects.py index c4fcd17..9a0b70d 100644 --- a/app/routers/projects.py +++ b/app/routers/projects.py @@ -214,6 +214,57 @@ def unarchive_project(project_id: int, db: Session = Depends(get_db)): return {"message": "Project unarchived successfully", "project_id": project_id} +@router.get("/projects/archived", response_model=PaginatedResponse[ProjectData]) +def read_archived_projects( + 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)"), + db: Session = Depends(get_db), +): + """Get all archived projects (paginated)""" + # Calculate offset + offset = (page - 1) * page_size + + # Query only archived projects + query = db.query(ProjectTable).filter(ProjectTable.is_archived == 1) + + # Get total count + total_count = query.count() + + # Get paginated results + projects = ( + query.options( + selectinload(ProjectTable.sector), + selectinload(ProjectTable.investors), + selectinload(ProjectTable.companies), + ) + .offset(offset) + .limit(page_size) + .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) + + # Calculate total pages + total_pages = (total_count + page_size - 1) // page_size + + return PaginatedResponse( + items=project_data_list, + total=total_count, + page=page, + page_size=page_size, + total_pages=total_pages, + ) + + @router.get("/projects/filter", response_model=PaginatedResponse[ProjectData]) def filter_projects( stage: Optional[InvestmentStage] = Query( diff --git a/app/services/insight.py b/app/services/insight.py index 06cca4c..324a154 100644 --- a/app/services/insight.py +++ b/app/services/insight.py @@ -49,7 +49,7 @@ class QueryProcessor: """Tool to search the web using google, provide the relevant query to get the information""" logger.info(f"\nWeb Search Tool Called with query: {query}") if query: - result = self.ddg_search.text(query, max_results=10, backend="google") + result = self.ddg_search.text(query, max_results=10) return result return "No query provided." diff --git a/investors.db b/investors.db index dceae795ca494eff4398c18075b666e7a8d4a5ef..07c306b52608ef8a880076102152a9cd65fbc659 100644 GIT binary patch delta 6980 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 dVzA6pYy&NAiIo!$3%*XbngjC&1jQ4fzX6Y6DNX