-
Notifications
You must be signed in to change notification settings - Fork 122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ftr/dev 10981 spending by subawards grouped endpoint #4312
base: qat
Are you sure you want to change the base?
Ftr/dev 10981 spending by subawards grouped endpoint #4312
Conversation
usaspending_api/search/v2/views/spending_by_subaward_grouped.py
Outdated
Show resolved
Hide resolved
How many results are returned | ||
+ `page` (optional, number) | ||
The page number that is currently returned. | ||
+ `sort`: Any field within the result can be sort (optional, string) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+ `sort`: Any field within the result can be sort (optional, string) | |
+ `sort`: Any field within the result can be sort (optional, enum[string]) |
Since this has an enumerated set of members, we can use enum[string] as the type similar to the order.
+ `previous` (required, number, nullable) | ||
|
||
## Filter Objects | ||
### AdvancedFilterObject (object) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can link to another definition of AdvancedFilterObject to avoid having to copy it here. See
usaspending-api/usaspending_api/api_contracts/contracts/v2/search/spending_by_transaction_grouped.md
Line 203 in c1e202c
The filters available are defined in [AdvancedFilterObject](./spending_by_transaction.md#advanced-filter-object). The one difference is that `keywords` is not required. |
{ | ||
"name": "sorted", | ||
"key": "sort", | ||
"type": "text", | ||
"text_type": "search", | ||
"default": "award_id", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could make this an enum type and list the sort keys in order to provide some extra validation.
{
"name": "sort",
"key": "sort",
"type": "enum",
"enum_values": (<sort fields here>,),
"optional": True,
"default": "award_id",
}
|
||
# Accepts the same filters as spending_by_award | ||
spending_by_subaward_grouped_models.extend(copy.deepcopy(AWARD_FILTER_NO_RECIPIENT_ID)) | ||
spending_by_subaward_grouped_models.extend(copy.deepcopy(PAGINATION)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PAGINATION
includes a sort model, so you may want to exclude it to avoid conflicting with the one above.
spending_by_subaward_grouped_models.extend(copy.deepcopy(PAGINATION)) | |
spending_by_subaward_grouped_models.extend(copy.deepcopy([model for model in PAGINATION if model["name"] != "sort"])) |
filter_options = {} | ||
time_period_obj = SubawardSearchTimePeriod( | ||
default_end_date=settings.API_MAX_DATE, default_start_date=settings.API_SEARCH_MIN_DATE | ||
) | ||
filter_options["time_period_obj"] = time_period_obj | ||
|
||
query_with_filters = QueryWithFilters(QueryType.SUBAWARDS) | ||
filter_query = query_with_filters.generate_elasticsearch_query(self.filters, **filter_options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you are just using one filter option, you could consider skipping creating a dictionary and then unpacking it and send it as a keyword argument directly.
filter_options = {} | |
time_period_obj = SubawardSearchTimePeriod( | |
default_end_date=settings.API_MAX_DATE, default_start_date=settings.API_SEARCH_MIN_DATE | |
) | |
filter_options["time_period_obj"] = time_period_obj | |
query_with_filters = QueryWithFilters(QueryType.SUBAWARDS) | |
filter_query = query_with_filters.generate_elasticsearch_query(self.filters, **filter_options) | |
time_period_obj = SubawardSearchTimePeriod( | |
default_end_date=settings.API_MAX_DATE, default_start_date=settings.API_SEARCH_MIN_DATE | |
) | |
query_with_filters = QueryWithFilters(QueryType.SUBAWARDS) | |
filter_query = query_with_filters.generate_elasticsearch_query(self.filters, time_period_obj=time_period_obj) |
logger = logging.getLogger(__name__) | ||
|
||
|
||
class subaward_grouped_model: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the use of a class here to represent this data structure. Could use @dataclass
decorator to simplify the syntax. Also python naming best practice is to use TitleCase in class names e.g. SubawardGroupedModel
def post(self, request): | ||
"""Return all subawards matching given awards""" | ||
|
||
self.original_filters = request.data.get("filters") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, it is discouraged to create new properties on self
outside of the __init__
method. You could add an __init__
method to this class that calls the APIView
's __init__
first with super().__init__()
and then adds these properties e.g.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.original_filters: dict[str, Any] = {}
<set initial values of other properties of self here>
for result in response["aggregations"]["award_id"]["buckets"]: | ||
award_generated_internal_id = response["hits"]["hits"][count]["_source"]["unique_award_key"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of defining a count
variable and iterating it, you can use python's enumerate
to get the count this way:
for result in response["aggregations"]["award_id"]["buckets"]: | |
award_generated_internal_id = response["hits"]["hits"][count]["_source"]["unique_award_key"] | |
for i, result in enumerate(response["aggregations"]["award_id"]["buckets"]): | |
award_generated_internal_id = response["hits"]["hits"][i]["_source"]["unique_award_key"] |
Can we also check to make sure that the award_piid_fain
in result.key
matches the value on the hit to ensure that we're getting the right unique_award_key
? Relying on this list of hits always being in the right order seems like it could be a little fragile. Alternatively, this could be a nested agg
by adding another .metric("unique_award_key", "terms", field="unique_award_key")
and referenced like result.unique_award_key.buckets[0].key
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add type hints for the parameters and return values where possible? Some of the methods in this file are missing type hints and some have them.
Description:
Creates a new API endpoint
/api/v2/search/spending_by_subaward_grouped
that returns a list containingaward_id
,award_generated_internal_id
,subaward_count
, andsubaward_obligation
. This will be used to in Award Search 2.0 for the table results when filtered on subawardsTechnical details:
This endpoint accepts all of the same filters as the
/api/v2/search/spending_by_award
endpoint when filtering by Subawards. A new api contract was made and from line 149 to the end is identical to spending_by_award since the filter objects are the sameRequirements for PR merge:
Area for explaining above N/A when needed: