usaspending-orm 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. usaspending/__init__.py +144 -0
  2. usaspending/cli/__init__.py +2 -0
  3. usaspending/cli/download_award.py +113 -0
  4. usaspending/client.py +550 -0
  5. usaspending/config.py +119 -0
  6. usaspending/download/__init__.py +8 -0
  7. usaspending/download/job.py +219 -0
  8. usaspending/download/manager.py +140 -0
  9. usaspending/exceptions.py +71 -0
  10. usaspending/logging_config.py +310 -0
  11. usaspending/models/__init__.py +86 -0
  12. usaspending/models/agency.py +631 -0
  13. usaspending/models/award.py +933 -0
  14. usaspending/models/award_factory.py +72 -0
  15. usaspending/models/award_types.py +114 -0
  16. usaspending/models/base_model.py +214 -0
  17. usaspending/models/contract.py +147 -0
  18. usaspending/models/district_spending.py +70 -0
  19. usaspending/models/download.py +74 -0
  20. usaspending/models/funding.py +182 -0
  21. usaspending/models/grant.py +147 -0
  22. usaspending/models/idv.py +165 -0
  23. usaspending/models/lazy_record.py +82 -0
  24. usaspending/models/loan.py +85 -0
  25. usaspending/models/location.py +125 -0
  26. usaspending/models/period_of_performance.py +43 -0
  27. usaspending/models/recipient.py +204 -0
  28. usaspending/models/recipient_business_categories.py +364 -0
  29. usaspending/models/recipient_spending.py +58 -0
  30. usaspending/models/spending.py +70 -0
  31. usaspending/models/state_spending.py +43 -0
  32. usaspending/models/subaward.py +270 -0
  33. usaspending/models/subtier_agency.py +98 -0
  34. usaspending/models/transaction.py +89 -0
  35. usaspending/queries/__init__.py +87 -0
  36. usaspending/queries/agencies_search.py +196 -0
  37. usaspending/queries/agency_award_summary.py +134 -0
  38. usaspending/queries/agency_query.py +102 -0
  39. usaspending/queries/award_query.py +40 -0
  40. usaspending/queries/awarding_agencies_search.py +16 -0
  41. usaspending/queries/awards_search.py +602 -0
  42. usaspending/queries/filters.py +437 -0
  43. usaspending/queries/funding_agencies_search.py +20 -0
  44. usaspending/queries/funding_search.py +180 -0
  45. usaspending/queries/query_builder.py +1430 -0
  46. usaspending/queries/recipient_query.py +66 -0
  47. usaspending/queries/recipients_search.py +167 -0
  48. usaspending/queries/single_resource_base.py +83 -0
  49. usaspending/queries/spending_search.py +261 -0
  50. usaspending/queries/sub_agency_query.py +177 -0
  51. usaspending/queries/subawards_search.py +196 -0
  52. usaspending/queries/transactions_search.py +213 -0
  53. usaspending/resources/__init__.py +17 -0
  54. usaspending/resources/agency_resource.py +105 -0
  55. usaspending/resources/award_resource.py +119 -0
  56. usaspending/resources/base_resource.py +31 -0
  57. usaspending/resources/download_resource.py +102 -0
  58. usaspending/resources/funding_resource.py +41 -0
  59. usaspending/resources/recipients_resource.py +104 -0
  60. usaspending/resources/spending_resource.py +45 -0
  61. usaspending/resources/subawards_resource.py +68 -0
  62. usaspending/resources/transactions_resource.py +39 -0
  63. usaspending/utils/__init__.py +6 -0
  64. usaspending/utils/formatter.py +550 -0
  65. usaspending/utils/rate_limit.py +135 -0
  66. usaspending/utils/retry.py +257 -0
  67. usaspending/utils/special_cases.yaml +155 -0
  68. usaspending_orm-0.6.0.dist-info/LICENSE +18 -0
  69. usaspending_orm-0.6.0.dist-info/METADATA +324 -0
  70. usaspending_orm-0.6.0.dist-info/RECORD +71 -0
  71. usaspending_orm-0.6.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,196 @@
1
+ """Agencies search query implementation for funding agency/office autocomplete."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Dict, Any, TYPE_CHECKING
5
+ from ..exceptions import ValidationError
6
+ from ..models.agency import Agency
7
+ from ..models.subtier_agency import SubTierAgency
8
+ from .query_builder import QueryBuilder
9
+ from ..logging_config import USASpendingLogger
10
+
11
+ if TYPE_CHECKING:
12
+ from ..client import USASpendingClient
13
+
14
+ logger = USASpendingLogger.get_logger(__name__)
15
+
16
+
17
+ class AgenciesSearch(QueryBuilder[Agency]):
18
+ """Search for funding agencies and offices by name using autocomplete.
19
+
20
+ This query builder uses the /v2/autocomplete/funding_agency_office/ endpoint
21
+ to search for agencies by name. Results can be filtered by type (toptier,
22
+ subtier, or office).
23
+ """
24
+
25
+ def __init__(self, client: USASpendingClient):
26
+ """Initialize AgenciesSearch with client."""
27
+ super().__init__(client)
28
+ self._search_text = ""
29
+ self._limit = 100 # Default limit
30
+ self._result_type = None # Filter: None, 'toptier', 'subtier', 'office'
31
+
32
+ @property
33
+ def _endpoint(self) -> str:
34
+ """API endpoint for agency autocomplete."""
35
+ raise NotImplementedError("Subclasses must implement _endpoint")
36
+
37
+ def _build_payload(self, page: int) -> Dict[str, Any]:
38
+ """Build request payload."""
39
+ if not self._search_text:
40
+ raise ValidationError(
41
+ "search_text is required. Use search_text() method."
42
+ )
43
+
44
+ return {"search_text": self._search_text, "limit": self._limit}
45
+
46
+ def _execute_query(self, page: int) -> Dict[str, Any]:
47
+ """Execute the autocomplete query.
48
+
49
+ This endpoint doesn't support pagination, so only return results on page 1.
50
+ """
51
+ # No pagination - return empty after first page
52
+ if page > 1:
53
+ return {"results": [], "page_metadata": {"hasNext": False}}
54
+
55
+ payload = self._build_payload(1)
56
+ response = self._client._make_request("POST", self._endpoint, json=payload)
57
+
58
+ # The response has results as an object with three arrays
59
+ # We need to flatten this into a single results array for QueryBuilder
60
+ results_obj = response.get("results", {})
61
+ flat_results = []
62
+
63
+ # Add results based on filter type
64
+ if self._result_type is None:
65
+ # No filter - return all types
66
+ # Add toptier agencies
67
+ for agency in results_obj.get("toptier_agency", []):
68
+ flat_results.append({"type": "toptier", "data": agency})
69
+
70
+ # Add subtier agencies
71
+ for subtier in results_obj.get("subtier_agency", []):
72
+ flat_results.append({"type": "subtier", "data": subtier})
73
+
74
+ # Add offices
75
+ for office in results_obj.get("office", []):
76
+ flat_results.append({"type": "office", "data": office})
77
+
78
+ elif self._result_type == "toptier":
79
+ for agency in results_obj.get("toptier_agency", []):
80
+ flat_results.append({"type": "toptier", "data": agency})
81
+
82
+ elif self._result_type == "subtier":
83
+ for subtier in results_obj.get("subtier_agency", []):
84
+ flat_results.append({"type": "subtier", "data": subtier})
85
+
86
+ elif self._result_type == "office":
87
+ for office in results_obj.get("office", []):
88
+ flat_results.append({"type": "office", "data": office})
89
+
90
+ # Return flattened structure for QueryBuilder
91
+ return {
92
+ "results": flat_results,
93
+ "page_metadata": {"hasNext": False},
94
+ "messages": response.get("messages", []),
95
+ }
96
+
97
+ def _transform_result(self, result: Dict[str, Any]) -> Agency:
98
+ """Transform result into Agency object based on type."""
99
+ if not result:
100
+ return None
101
+
102
+ result_type = result.get("type")
103
+ data = result.get("data", {})
104
+
105
+ if result_type == "toptier":
106
+ # Direct toptier agency result
107
+ agency_data = {
108
+ "code": data.get("code"),
109
+ "toptier_code": data.get("code"),
110
+ "name": data.get("name"),
111
+ "abbreviation": data.get("abbreviation"),
112
+ }
113
+ return Agency(agency_data, self._client)
114
+
115
+ elif result_type == "subtier":
116
+ # Include subtier data
117
+ return SubTierAgency(data, self._client)
118
+
119
+ elif result_type == "office":
120
+ return SubTierAgency(data, self._client)
121
+
122
+ return None
123
+
124
+ def count(self) -> int:
125
+ """Get total count of matching agencies/offices.
126
+
127
+ Returns:
128
+ Total number of matching results
129
+ """
130
+ logger.debug(f"{self.__class__.__name__}.count() called")
131
+
132
+ if not self._search_text:
133
+ raise ValidationError(
134
+ "search_text is required. Use search_text() method."
135
+ )
136
+
137
+ # Execute query to get all results
138
+ response = self._execute_query(1)
139
+ results = response.get("results", [])
140
+
141
+ count = len(results)
142
+
143
+ logger.info(
144
+ f"{self.__class__.__name__}.count() = {count} results "
145
+ f"for search text '{self._search_text}'"
146
+ )
147
+ return count
148
+
149
+ def search_text(self, search_text: str) -> AgenciesSearch:
150
+ """Set the search text for the query.
151
+
152
+ Args:
153
+ search_text: Text to search for in agency names
154
+
155
+ Returns:
156
+ New AgenciesSearch instance with search text set
157
+ """
158
+ clone = self._clone()
159
+ clone._search_text = search_text
160
+ return clone
161
+
162
+ def toptier(self) -> AgenciesSearch:
163
+ """Filter to only return toptier agency matches.
164
+
165
+ Returns:
166
+ New AgenciesSearch instance filtered to toptier agencies
167
+ """
168
+ clone = self._clone()
169
+ clone._result_type = "toptier"
170
+ return clone
171
+
172
+ def subtier(self) -> AgenciesSearch:
173
+ """Filter to only return subtier agency matches.
174
+
175
+ Returns:
176
+ New AgenciesSearch instance filtered to subtier agencies
177
+ """
178
+ clone = self._clone()
179
+ clone._result_type = "subtier"
180
+ return clone
181
+
182
+ def office(self) -> AgenciesSearch:
183
+ """Filter to only return office matches.
184
+
185
+ Returns:
186
+ New AgenciesSearch instance filtered to offices
187
+ """
188
+ clone = self._clone()
189
+ clone._result_type = "office"
190
+ return clone
191
+
192
+ def _clone(self) -> AgenciesSearch:
193
+ """Create immutable copy for chaining."""
194
+ clone = super()._clone()
195
+ clone._search_text = self._search_text
196
+ return clone
@@ -0,0 +1,134 @@
1
+ """Agency award summary query implementation for retrieving award aggregations."""
2
+
3
+ from typing import TYPE_CHECKING, Optional, Dict, Any, List
4
+ from .single_resource_base import SingleResourceBase
5
+ from ..exceptions import ValidationError
6
+ from ..client import USASpendingClient
7
+ from ..logging_config import USASpendingLogger
8
+
9
+ if TYPE_CHECKING:
10
+ pass
11
+
12
+ logger = USASpendingLogger.get_logger(__name__)
13
+
14
+
15
+ class AgencyAwardSummary(SingleResourceBase):
16
+ """Retrieve agency award summary data from the USAspending API.
17
+
18
+ This query class handles fetching aggregated award information including
19
+ transaction counts and obligations filtered by fiscal year, agency type,
20
+ and award type codes.
21
+ """
22
+
23
+ def __init__(self, client: USASpendingClient):
24
+ """Initialize AgencyAwardSummary with client.
25
+
26
+ Args:
27
+ client: USASpendingClient client instance
28
+ """
29
+ super().__init__(client)
30
+ logger.debug("AgencyAwardSummary initialized with client: %s", client)
31
+
32
+ @property
33
+ def _endpoint(self) -> str:
34
+ """Base endpoint for agency award summary retrieval."""
35
+ return "/agency/"
36
+
37
+ def _construct_endpoint(self, resource_id: str) -> str:
38
+ """Construct the full endpoint URL for agency award summary.
39
+
40
+ Args:
41
+ resource_id: The toptier_code for the agency
42
+
43
+ Returns:
44
+ Full endpoint path including /awards/
45
+ """
46
+ return f"{self._endpoint}{resource_id}/awards/"
47
+
48
+ def find_by_id(self, toptier_code: str) -> Dict[str, Any]:
49
+ """Not used for award summary - use get_awards_summary instead.
50
+
51
+ Raises:
52
+ NotImplementedError: This method should not be used directly
53
+ """
54
+ raise NotImplementedError(
55
+ "Use get_awards_summary() method instead for award summary data"
56
+ )
57
+
58
+ def get_awards_summary(
59
+ self,
60
+ toptier_code: str,
61
+ fiscal_year: Optional[int] = None,
62
+ agency_type: str = "awarding",
63
+ award_type_codes: Optional[List[str]] = None,
64
+ ) -> Dict[str, Any]:
65
+ """Retrieve agency award summary with optional filters.
66
+
67
+ Args:
68
+ toptier_code: The toptier code of an agency (3-4 digit string)
69
+ fiscal_year: Optional fiscal year for the data (defaults to current)
70
+ agency_type: "awarding" or "funding" (defaults to "awarding")
71
+ award_type_codes: Optional list of award type codes to filter by
72
+
73
+ Returns:
74
+ Dictionary containing:
75
+ - toptier_code: Agency toptier code
76
+ - fiscal_year: Fiscal year of data
77
+ - latest_action_date: Latest transaction date
78
+ - transaction_count: Number of transactions
79
+ - obligations: Total obligations amount
80
+ - messages: Any API messages
81
+
82
+ Raises:
83
+ ValidationError: If toptier_code is invalid or agency_type is invalid
84
+ APIError: If API request fails
85
+ """
86
+ # Validate toptier_code
87
+ if not toptier_code:
88
+ raise ValidationError("toptier_code is required")
89
+
90
+ toptier_code = str(toptier_code).strip()
91
+ if not toptier_code.isdigit() or len(toptier_code) not in [3, 4]:
92
+ raise ValidationError(
93
+ f"Invalid toptier_code: {toptier_code}. "
94
+ "Must be a 3-4 digit numeric string"
95
+ )
96
+
97
+ # Validate agency_type
98
+ if agency_type not in ["awarding", "funding"]:
99
+ raise ValidationError(
100
+ f"Invalid agency_type: {agency_type}. Must be 'awarding' or 'funding'"
101
+ )
102
+
103
+ logger.debug(
104
+ "Fetching award summary for toptier_code: %s, fiscal_year: %s, "
105
+ "agency_type: %s, award_type_codes: %s",
106
+ toptier_code,
107
+ fiscal_year,
108
+ agency_type,
109
+ award_type_codes,
110
+ )
111
+
112
+ # Build params
113
+ params = {"agency_type": agency_type}
114
+
115
+ if fiscal_year is not None:
116
+ params["fiscal_year"] = fiscal_year
117
+
118
+ if award_type_codes:
119
+ # Convert to list if needed and filter out None/empty values
120
+ if isinstance(award_type_codes, (set, frozenset)):
121
+ award_type_codes = list(award_type_codes)
122
+ award_type_codes = [code for code in award_type_codes if code]
123
+
124
+ if award_type_codes:
125
+ # API expects award_type_codes as array parameter
126
+ params["award_type_codes"] = award_type_codes
127
+
128
+ # Construct endpoint
129
+ endpoint = self._construct_endpoint(toptier_code)
130
+
131
+ # Make API request with params
132
+ response = self._client._make_request("GET", endpoint, params=params)
133
+
134
+ return response
@@ -0,0 +1,102 @@
1
+ """Agency query implementation for retrieving single agency details."""
2
+
3
+ from typing import TYPE_CHECKING, Optional, Dict, Any
4
+ from .single_resource_base import SingleResourceBase
5
+ from ..exceptions import ValidationError
6
+ from ..client import USASpendingClient
7
+ from ..logging_config import USASpendingLogger
8
+
9
+ if TYPE_CHECKING:
10
+ from ..models.agency import Agency
11
+
12
+ logger = USASpendingLogger.get_logger(__name__)
13
+
14
+
15
+ class AgencyQuery(SingleResourceBase):
16
+ """Retrieve a single agency from the USAspending API.
17
+
18
+ This query class handles fetching agency overview information for a
19
+ specific fiscal year using the agency's toptier code.
20
+ """
21
+
22
+ def __init__(self, client: USASpendingClient):
23
+ """Initialize AgencyQuery with client.
24
+
25
+ Args:
26
+ client: USASpendingClient client instance
27
+ """
28
+ super().__init__(client)
29
+ logger.debug("AgencyQuery initialized with client: %s", client)
30
+
31
+ @property
32
+ def _endpoint(self) -> str:
33
+ """Base endpoint for single agency retrieval."""
34
+ return "/agency/"
35
+
36
+ def find_by_id(
37
+ self, toptier_code: str, fiscal_year: Optional[int] = None
38
+ ) -> "Agency":
39
+ """Retrieve agency by toptier code and optional fiscal year.
40
+
41
+ Args:
42
+ toptier_code: The toptier code of an agency (3-4 digit string)
43
+ fiscal_year: Optional fiscal year for the data (defaults to current)
44
+
45
+ Returns:
46
+ Agency model instance with full details
47
+
48
+ Raises:
49
+ ValidationError: If toptier_code is invalid
50
+ APIError: If agency not found
51
+ """
52
+ if not toptier_code:
53
+ raise ValidationError("toptier_code is required")
54
+
55
+ # Validate toptier_code format (3-4 digit numeric string)
56
+ toptier_code = str(toptier_code).strip()
57
+ if not toptier_code.isdigit() or len(toptier_code) not in [3, 4]:
58
+ raise ValidationError(
59
+ f"Invalid toptier_code: {toptier_code}. "
60
+ "Must be a 3-4 digit numeric string"
61
+ )
62
+
63
+ logger.debug(
64
+ "Fetching agency with toptier_code: %s, fiscal_year: %s",
65
+ toptier_code,
66
+ fiscal_year,
67
+ )
68
+
69
+ # Make API request with optional fiscal_year parameter
70
+ response = self._get_resource_with_params(toptier_code, fiscal_year)
71
+
72
+ # Create model instance
73
+ from ..models.agency import Agency
74
+
75
+ return Agency(response, client=self._client)
76
+
77
+ def _get_resource_with_params(
78
+ self, toptier_code: str, fiscal_year: Optional[int] = None
79
+ ) -> Dict[str, Any]:
80
+ """Retrieve agency with optional query parameters.
81
+
82
+ Args:
83
+ toptier_code: The agency's toptier code
84
+ fiscal_year: Optional fiscal year parameter
85
+
86
+ Returns:
87
+ API response dictionary
88
+ """
89
+ # Construct endpoint
90
+ endpoint = self._construct_endpoint(toptier_code)
91
+
92
+ # Build params dict if fiscal_year provided
93
+ params = {}
94
+ if fiscal_year is not None:
95
+ params["fiscal_year"] = fiscal_year
96
+
97
+ # Make API request with params
98
+ response = self._client._make_request(
99
+ "GET", endpoint, params=params if params else None
100
+ )
101
+
102
+ return response
@@ -0,0 +1,40 @@
1
+ from typing import TYPE_CHECKING
2
+ from .single_resource_base import SingleResourceBase
3
+ from ..exceptions import ValidationError
4
+ from ..client import USASpendingClient
5
+ from ..logging_config import USASpendingLogger
6
+
7
+ if TYPE_CHECKING:
8
+ from ..models.award import Award
9
+
10
+ logger = USASpendingLogger.get_logger(__name__)
11
+
12
+
13
+ class AwardQuery(SingleResourceBase):
14
+ """Retrieve a single-award from the USAspending API."""
15
+
16
+ def __init__(self, client: USASpendingClient):
17
+ super().__init__(client)
18
+ logger.debug("AwardQuery initialized with client: %s", client)
19
+
20
+ @property
21
+ def _endpoint(self) -> str:
22
+ """Base endpoint for single award retrieval."""
23
+ return "/awards/"
24
+
25
+ def find_by_id(self, award_id: str) -> "Award":
26
+ """Filter by unique award identifier."""
27
+ return self.find_by_generated_id(award_id)
28
+
29
+ def find_by_generated_id(self, award_id: str) -> "Award":
30
+ """Filter by USASpending's internally generated unique award identifier."""
31
+ if not award_id:
32
+ raise ValidationError("award_id is required")
33
+
34
+ # Make API request
35
+ response = self._get_resource(award_id)
36
+
37
+ # Create model instance using factory
38
+ from ..models.award_factory import create_award
39
+
40
+ return create_award(response, client=self._client)
@@ -0,0 +1,16 @@
1
+ """Agencies search query implementation for awarding agency/office autocomplete."""
2
+
3
+ from __future__ import annotations
4
+ from .agencies_search import AgenciesSearch
5
+ from ..logging_config import USASpendingLogger
6
+
7
+ logger = USASpendingLogger.get_logger(__name__)
8
+
9
+
10
+ class AwardingAgenciesSearch(AgenciesSearch):
11
+ """Search for awarding agencies and offices by name."""
12
+
13
+ @property
14
+ def _endpoint(self) -> str:
15
+ """API endpoint for agency autocomplete."""
16
+ return "/autocomplete/awarding_agency_office/"