Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c17b66e
[DEV-15427] Fixed Incorrect Category Field Modification
DavidMikolaKC Jul 15, 2026
01be164
Merge branch 'qat' into bug/dev-15427-use-per-request-category-dataclass
DavidMikolaKC Jul 15, 2026
80865e7
[DEV-15427] Added New Category Instance per Request
DavidMikolaKC Jul 16, 2026
4fdf44f
[DEV-15427] - Added test case to ensure posts are cached correctly
DavidMikolaKC Jul 23, 2026
a6e0b69
[Dev-15427] testcase lint fixes
DavidMikolaKC Jul 23, 2026
0597042
Merge branch 'qat' of https://github.com/fedspendingtransparency/usas…
DavidMikolaKC Jul 24, 2026
e14a7ae
[Dev-15427] Added _category proprty
DavidMikolaKC Jul 24, 2026
18451b5
Ruff lint fix
DavidMikolaKC Jul 24, 2026
72139cc
Merge branch 'qat' into bug/dev-15427-use-per-request-category-dataclass
DavidMikolaKC Jul 24, 2026
d8f4e5a
Merge branch 'qat' of https://github.com/fedspendingtransparency/usas…
DavidMikolaKC Jul 27, 2026
d3223b8
[Dev-15427] Updated Category Declaration for Child Classes
DavidMikolaKC Jul 27, 2026
de9fe6d
Merge branch 'bug/dev-15427-use-per-request-category-dataclass' of ht…
DavidMikolaKC Jul 27, 2026
9ee057f
ruff lint fix
DavidMikolaKC Jul 27, 2026
fb2c4a9
[Dev-15427] Updated Category Declaration for other Child Classes
DavidMikolaKC Jul 27, 2026
8731e7a
Relocated Category Deepcopy
DavidMikolaKC Jul 27, 2026
6207ca0
Merge branch 'qat' into bug/dev-15427-use-per-request-category-dataclass
aguest-kc Jul 28, 2026
0ad35a6
Fixed recloning for self.category
DavidMikolaKC Jul 29, 2026
91134d1
Merge branch 'bug/dev-15427-use-per-request-category-dataclass' of ht…
DavidMikolaKC Jul 29, 2026
491fba6
Merge branch 'qat' into bug/dev-15427-use-per-request-category-dataclass
DavidMikolaKC Jul 29, 2026
8686506
Merge branch 'bug/dev-15427-use-per-request-category-dataclass' of ht…
DavidMikolaKC Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,48 @@ def test_correct_response_of_empty_list(client, monkeypatch, elasticsearch_trans
}
assert resp.status_code == status.HTTP_200_OK, "Failed to return 200 Response"
assert resp.json() == expected_response


def test_category_dataclass_subaward_caching(client, monkeypatch, awards_and_transactions,
elasticsearch_subaward_index, elasticsearch_transaction_index):
# Tests that the Category dataclass is not being cached incorrectly
# Originally would happen when updated by subaward spending levels
# tested by making a spending_level subawards request and then requesting on transaction level
setup_elasticsearch_test(monkeypatch, elasticsearch_subaward_index)

sub_resp = client.post(
"/api/v2/search/spending_by_category/county",
content_type="application/json",
data=json.dumps({
"filters": {"time_period": [{"start_date": "2018-10-01", "end_date": "2020-09-30"}]},
"spending_level": "subawards"
}),
)

assert sub_resp.status_code == status.HTTP_200_OK, "Failed to return 200 Response"
assert sub_resp.json().get("spending_level") == "subawards"
setup_elasticsearch_test(monkeypatch, elasticsearch_transaction_index)

trn_resp = client.post(
"/api/v2/search/spending_by_category/county",
content_type="application/json",
data=json.dumps({
"filters": {"time_period": [{"start_date": "2018-10-01", "end_date": "2020-09-30"}]},
"spending_level": "transactions"
}),
)

expected_response = {
"category": "county",
"limit": 10,
"page_metadata": {"page": 1, "next": None, "previous": None, "hasNext": False, "hasPrevious": False},
"results": [
{"amount": 550005.0, "code": "001", "id": None, "name": "CHARLESTON", "total_outlays": None},
{"amount": 5500.0, "code": "005", "id": None, "name": "TEST NAME", "total_outlays": None},
{"amount": 50.0, "code": "005", "id": None, "name": "TEST NAME", "total_outlays": None},
],
"messages": _expected_messages(),
"spending_level": "transactions",
}
assert trn_resp.status_code == status.HTTP_200_OK, "Failed to return 200 Response"
assert trn_resp.json() == expected_response
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ class AwardingAgencyViewSet(AbstractAgencyViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/awarding_agency.md"

agency_type = AgencyType.AWARDING_TOPTIER
category = Category(name="awarding_agency", agg_key="awarding_toptier_agency_agg_key")
_category = Category(name="awarding_agency", agg_key="awarding_toptier_agency_agg_key")


class AwardingSubagencyViewSet(AbstractAgencyViewSet):
Expand All @@ -106,7 +106,7 @@ class AwardingSubagencyViewSet(AbstractAgencyViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/awarding_subagency.md"

agency_type = AgencyType.AWARDING_SUBTIER
category = Category(name="awarding_subagency", agg_key="awarding_subtier_agency_agg_key")
_category = Category(name="awarding_subagency", agg_key="awarding_subtier_agency_agg_key")


class FundingAgencyViewSet(AbstractAgencyViewSet):
Expand All @@ -117,7 +117,7 @@ class FundingAgencyViewSet(AbstractAgencyViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/funding_agency.md"

agency_type = AgencyType.FUNDING_TOPTIER
category = Category(name="funding_agency", agg_key="funding_toptier_agency_agg_key")
_category = Category(name="funding_agency", agg_key="funding_toptier_agency_agg_key")


class FundingSubagencyViewSet(AbstractAgencyViewSet):
Expand All @@ -128,4 +128,4 @@ class FundingSubagencyViewSet(AbstractAgencyViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/funding_subagency.md"

agency_type = AgencyType.FUNDING_SUBTIER
category = Category(name="funding_subagency", agg_key="funding_subtier_agency_agg_key")
_category = Category(name="funding_subagency", agg_key="funding_subtier_agency_agg_key")
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import copy
import logging
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
from dataclasses import dataclass, replace
from typing import List, Optional, Union

from django.conf import settings
Expand Down Expand Up @@ -32,7 +32,7 @@
logger = logging.getLogger(__name__)


@dataclass
@dataclass(frozen=True)
class Category:
name: str
agg_key: str
Expand All @@ -49,7 +49,7 @@ class AbstractSpendingByCategoryViewSet(APIView, metaclass=ABCMeta):
Abstract class inherited by the different spending by category endpoints.
"""

category: Category
_category: Category
filters: dict
pagination: Pagination
high_cardinality_categories: List[str] = ["recipient", "recipient_duns"]
Expand Down Expand Up @@ -79,6 +79,11 @@ def post(self, request: Request, *args, **kwargs) -> Response:
return Response(raw_response)

def validate_payload(self, request: Request) -> dict:
# Creates an instance copy for the category for each request
# Prevents modifications from previous requests affecting current requests
if not hasattr(self, "category"):
self.category = copy.deepcopy(self._category)

models = [
{"name": "subawards", "key": "subawards", "type": "boolean", "default": False, "optional": True},
{
Expand Down Expand Up @@ -126,8 +131,10 @@ def validate_payload(self, request: Request) -> dict:
def perform_search(self, original_filters: dict) -> dict:
if self.spending_level == SpendingLevel.SUBAWARD:
# Swap the agg_key fields for the equivalent Subaward fields, if applicable
self.category.agg_key = self.subaward_agg_key_mapper.get(self.category.agg_key, self.category.agg_key)

self.category = replace(
self.category,
agg_key=self.subaward_agg_key_mapper.get(self.category.agg_key, self.category.agg_key)
)
query_with_filters = QueryWithFilters(QueryType.SUBAWARDS)
filter_query = query_with_filters.generate_elasticsearch_query(self.filters)
results = self.query_elasticsearch(filter_query)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,4 @@ class FederalAccountViewSet(AbstractAccountViewSet):

endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/federal_account.md"

category = Category(name="federal_account", agg_key="federal_accounts")
_category = Category(name="federal_account", agg_key="federal_accounts")
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from typing import List

from django.db.models import F
from rest_framework.request import Request
from rest_framework.response import Response

from usaspending_api.references.models import NAICS, PSC, Cfda, DisasterEmergencyFundCode
from usaspending_api.search.v2.views.enums import SpendingLevel
Expand Down Expand Up @@ -101,7 +103,7 @@ class CfdaViewSet(AbstractIndustryCodeViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/cfda.md"

industry_code_type = IndustryCodeType.CFDA
category = Category(name="cfda", agg_key="cfda_agg_key")
_category = Category(name="cfda", agg_key="cfda_agg_key")


class NAICSViewSet(AbstractIndustryCodeViewSet):
Expand All @@ -112,7 +114,7 @@ class NAICSViewSet(AbstractIndustryCodeViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/naics.md"

industry_code_type = IndustryCodeType.NAICS
category = Category(name="naics", agg_key="naics_agg_key")
_category = Category(name="naics", agg_key="naics_agg_key")


class PSCViewSet(AbstractIndustryCodeViewSet):
Expand All @@ -123,7 +125,7 @@ class PSCViewSet(AbstractIndustryCodeViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/psc.md"

industry_code_type = IndustryCodeType.PSC
category = Category(name="psc", agg_key="psc_agg_key")
_category = Category(name="psc", agg_key="psc_agg_key")


class DEFCViewSet(AbstractIndustryCodeViewSet):
Expand All @@ -134,9 +136,12 @@ class DEFCViewSet(AbstractIndustryCodeViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/defc.md"

industry_code_type = IndustryCodeType.DEFC
category = Category(name="defc", agg_key="defc_agg_key")
_category = Category(name="defc", agg_key="defc_agg_key")

def post(self, request: Request, *args, **kwargs) -> Response:
if hasattr(self, "category"):
delattr(self, "category")

def post(self, request, *args, **kwargs):
validated_payload = self.validate_payload(request)
nested_path = "spending_by_defc"
if self.spending_level == SpendingLevel.FILE_C:
Expand All @@ -147,7 +152,7 @@ def post(self, request, *args, **kwargs):
agg_key_suffix="",
obligation_field=f"{nested_path}.obligation",
outlay_field=f"{nested_path}.outlay",
filter_key_to_limit="def_codes",
filter_key_to_limit="def_codes"
)

return super().post(request, validated_payload=validated_payload)
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class LocationType(Enum):
CONGRESSIONAL_DISTRICT = "congressional"


def _combine_dicts_by_keys(dicts, keys, sum_key) -> List[Dict]:
def _combine_dicts_by_keys(dicts: list[dict], keys: list[str], sum_key: str) -> List[Dict]:
"""Combine all dictionaries in a list that have the same values for the given field(s)

Args:
Expand Down Expand Up @@ -186,7 +186,7 @@ class CountyViewSet(AbstractLocationViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/county.md"

location_type = LocationType.COUNTY
category = Category(name="county", agg_key="pop_county_agg_key")
_category = Category(name="county", agg_key="pop_county_agg_key")


class DistrictViewSet(AbstractLocationViewSet):
Expand All @@ -197,7 +197,7 @@ class DistrictViewSet(AbstractLocationViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/district.md"

location_type = LocationType.CONGRESSIONAL_DISTRICT
category = Category(name="district", agg_key="pop_congressional_cur_agg_key")
_category = Category(name="district", agg_key="pop_congressional_cur_agg_key")


class StateTerritoryViewSet(AbstractLocationViewSet):
Expand All @@ -208,7 +208,7 @@ class StateTerritoryViewSet(AbstractLocationViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/state_territory.md"

location_type = LocationType.STATE_TERRITORY
category = Category(name="state_territory", agg_key="pop_state_agg_key")
_category = Category(name="state_territory", agg_key="pop_state_agg_key")


class CountryViewSet(AbstractLocationViewSet):
Expand All @@ -219,4 +219,4 @@ class CountryViewSet(AbstractLocationViewSet):
endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/country.md"

location_type = LocationType.COUNTRY
category = Category(name="country", agg_key="pop_country_agg_key")
_category = Category(name="country", agg_key="pop_country_agg_key")
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class RecipientViewSet(AbstractSpendingByCategoryViewSet):
"""

endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/recipient.md"
category = Category(name="recipient", agg_key="recipient_agg_key")
_category = Category(name="recipient", agg_key="recipient_agg_key")

def build_elasticsearch_result(self, response: dict) -> List[dict]:
# Get the codes
Expand Down Expand Up @@ -81,4 +81,4 @@ class RecipientDunsViewSet(RecipientViewSet):
"""

endpoint_doc = "usaspending_api/api_contracts/contracts/v2/search/spending_by_category/recipient_duns.md"
category = Category(name="recipient_duns", agg_key="recipient_agg_key")
_category = Category(name="recipient_duns", agg_key="recipient_agg_key")
Loading