269 lines
8.8 KiB
Python
269 lines
8.8 KiB
Python
"""
|
|
USASpending.gov Contract Search
|
|
|
|
Search federal contract awards to understand company capabilities,
|
|
contract history, and funding relationships.
|
|
|
|
API Documentation: https://api.usaspending.gov/
|
|
Free, no API key required.
|
|
"""
|
|
|
|
import logging
|
|
import requests
|
|
from typing import List, Optional, Dict, Any
|
|
from datetime import datetime, timedelta
|
|
|
|
from ..search.base import BaseSearcher, SearchResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ContractSearcher(BaseSearcher):
|
|
"""
|
|
Search USASpending.gov for federal contracts.
|
|
|
|
Useful for:
|
|
- Finding companies with relevant contract experience
|
|
- Understanding funding levels for technologies
|
|
- Identifying prime contractors and their focus areas
|
|
"""
|
|
|
|
BASE_URL = "https://api.usaspending.gov/api/v2"
|
|
|
|
def __init__(self, timeout: int = 30):
|
|
self.timeout = timeout
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "USASpending"
|
|
|
|
@property
|
|
def source_type(self) -> str:
|
|
return "contract"
|
|
|
|
def search(
|
|
self,
|
|
query: str,
|
|
max_results: int = 20,
|
|
agency: Optional[str] = None,
|
|
min_amount: Optional[float] = None,
|
|
years_back: int = 5
|
|
) -> List[SearchResult]:
|
|
"""
|
|
Search federal contracts.
|
|
|
|
Args:
|
|
query: Keyword search
|
|
max_results: Maximum results
|
|
agency: Filter by agency (e.g., "Department of Defense")
|
|
min_amount: Minimum contract value
|
|
years_back: How many years back to search
|
|
|
|
Returns:
|
|
List of SearchResult objects
|
|
"""
|
|
# Calculate date range
|
|
end_date = datetime.now()
|
|
start_date = end_date - timedelta(days=years_back * 365)
|
|
|
|
payload = {
|
|
"filters": {
|
|
"keywords": [query],
|
|
"time_period": [{
|
|
"start_date": start_date.strftime("%Y-%m-%d"),
|
|
"end_date": end_date.strftime("%Y-%m-%d")
|
|
}]
|
|
},
|
|
"fields": [
|
|
"Award ID",
|
|
"Recipient Name",
|
|
"Description",
|
|
"Award Amount",
|
|
"Start Date",
|
|
"Awarding Agency",
|
|
"Awarding Sub Agency",
|
|
"Contract Award Type"
|
|
],
|
|
"page": 1,
|
|
"limit": min(max_results, 100),
|
|
"sort": "Award Amount",
|
|
"order": "desc"
|
|
}
|
|
|
|
if agency:
|
|
payload["filters"]["agencies"] = [{
|
|
"type": "awarding",
|
|
"tier": "toptier",
|
|
"name": agency
|
|
}]
|
|
|
|
if min_amount:
|
|
payload["filters"]["award_amounts"] = [{
|
|
"lower_bound": min_amount
|
|
}]
|
|
|
|
results = []
|
|
|
|
try:
|
|
response = requests.post(
|
|
f"{self.BASE_URL}/search/spending_by_award/",
|
|
json=payload,
|
|
timeout=self.timeout,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
for rank, award in enumerate(data.get("results", []), 1):
|
|
if rank > max_results:
|
|
break
|
|
|
|
award_id = award.get("Award ID", "")
|
|
# USASpending award detail URL
|
|
url = f"https://www.usaspending.gov/award/{award_id}" if award_id else ""
|
|
|
|
results.append(SearchResult(
|
|
title=award.get("Description", "")[:200] if award.get("Description") else "Contract Award",
|
|
url=url,
|
|
snippet=award.get("Description", "")[:500] if award.get("Description") else "",
|
|
source=self.name,
|
|
source_type=self.source_type,
|
|
rank=rank,
|
|
published_date=award.get("Start Date"),
|
|
organization=award.get("Recipient Name", ""),
|
|
award_amount=self._parse_amount(award.get("Award Amount")),
|
|
award_id=award_id,
|
|
trl_estimate=6, # Contracts typically for more mature tech
|
|
raw_data=award
|
|
))
|
|
|
|
logger.info(f"Contract search for '{query}' returned {len(results)} results")
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"USASpending API request failed: {e}")
|
|
except Exception as e:
|
|
logger.error(f"Contract search error: {e}")
|
|
|
|
return results
|
|
|
|
def _parse_amount(self, amount) -> Optional[float]:
|
|
"""Parse award amount to float."""
|
|
if amount is None:
|
|
return None
|
|
try:
|
|
return float(amount)
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
def search_dod(self, query: str, max_results: int = 20) -> List[SearchResult]:
|
|
"""Search specifically DOD contracts."""
|
|
return self.search(query, max_results=max_results, agency="Department of Defense")
|
|
|
|
def search_space_force(self, query: str, max_results: int = 20) -> List[SearchResult]:
|
|
"""Search Space Force / space-related DOD contracts."""
|
|
space_query = f"{query} (space OR satellite OR orbital OR launch)"
|
|
return self.search(space_query, max_results=max_results, agency="Department of Defense")
|
|
|
|
def get_recipient_profile(self, recipient_name: str) -> Dict[str, Any]:
|
|
"""
|
|
Get profile information about a contract recipient.
|
|
|
|
Returns summary of their contract history.
|
|
"""
|
|
payload = {
|
|
"filters": {
|
|
"recipient_search_text": [recipient_name],
|
|
"time_period": [{
|
|
"start_date": "2019-01-01",
|
|
"end_date": datetime.now().strftime("%Y-%m-%d")
|
|
}]
|
|
},
|
|
"category": "awarding_agency",
|
|
"limit": 10
|
|
}
|
|
|
|
try:
|
|
response = requests.post(
|
|
f"{self.BASE_URL}/search/spending_by_category/",
|
|
json=payload,
|
|
timeout=self.timeout,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
return {
|
|
"recipient_name": recipient_name,
|
|
"total_contracts": data.get("total", 0),
|
|
"agencies": data.get("results", [])
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to get recipient profile: {e}")
|
|
return {"recipient_name": recipient_name, "error": str(e)}
|
|
|
|
def get_company_contracts(
|
|
self,
|
|
company_name: str,
|
|
max_results: int = 50
|
|
) -> List[SearchResult]:
|
|
"""Get all recent contracts for a specific company."""
|
|
payload = {
|
|
"filters": {
|
|
"recipient_search_text": [company_name],
|
|
"time_period": [{
|
|
"start_date": "2020-01-01",
|
|
"end_date": datetime.now().strftime("%Y-%m-%d")
|
|
}]
|
|
},
|
|
"fields": [
|
|
"Award ID",
|
|
"Recipient Name",
|
|
"Description",
|
|
"Award Amount",
|
|
"Start Date",
|
|
"Awarding Agency",
|
|
"Awarding Sub Agency"
|
|
],
|
|
"page": 1,
|
|
"limit": min(max_results, 100),
|
|
"sort": "Start Date",
|
|
"order": "desc"
|
|
}
|
|
|
|
results = []
|
|
|
|
try:
|
|
response = requests.post(
|
|
f"{self.BASE_URL}/search/spending_by_award/",
|
|
json=payload,
|
|
timeout=self.timeout,
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
for rank, award in enumerate(data.get("results", []), 1):
|
|
award_id = award.get("Award ID", "")
|
|
url = f"https://www.usaspending.gov/award/{award_id}" if award_id else ""
|
|
|
|
results.append(SearchResult(
|
|
title=award.get("Description", "")[:200] if award.get("Description") else "Contract",
|
|
url=url,
|
|
snippet=f"Agency: {award.get('Awarding Agency', 'Unknown')}",
|
|
source=self.name,
|
|
source_type=self.source_type,
|
|
rank=rank,
|
|
published_date=award.get("Start Date"),
|
|
organization=award.get("Recipient Name", ""),
|
|
award_amount=self._parse_amount(award.get("Award Amount")),
|
|
award_id=award_id,
|
|
raw_data=award
|
|
))
|
|
|
|
logger.info(f"Found {len(results)} contracts for {company_name}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Company contract search failed: {e}")
|
|
|
|
return results
|