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,631 @@
1
+ """Agency model for USASpending data."""
2
+
3
+ from __future__ import annotations
4
+ from typing import Dict, Any, Optional, List, TYPE_CHECKING
5
+ from dataclasses import dataclass
6
+ from decimal import Decimal
7
+ from ..utils.formatter import to_decimal, to_int, to_date
8
+ from datetime import date
9
+ from functools import cached_property
10
+ from .lazy_record import LazyRecord
11
+ from ..logging_config import USASpendingLogger
12
+ from .award_types import (
13
+ CONTRACT_CODES,
14
+ GRANT_CODES,
15
+ IDV_CODES,
16
+ LOAN_CODES,
17
+ DIRECT_PAYMENT_CODES,
18
+ OTHER_CODES,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from ..client import USASpendingClient
23
+ from ..queries.awards_search import AwardsSearch
24
+ from .subtier_agency import SubTierAgency
25
+
26
+ logger = USASpendingLogger.get_logger(__name__)
27
+
28
+
29
+ # Create data class for def_codes
30
+ @dataclass
31
+ class DefCode:
32
+ """Disaster Emergency Fund Code (DEFC) data structure.
33
+
34
+ Represents a disaster emergency fund code associated with an agency,
35
+ containing legislative and reference information.
36
+
37
+ Attributes:
38
+ code: The DEFC code identifier.
39
+ public_law: Associated public law reference.
40
+ title: Optional descriptive title of the fund.
41
+ urls: Optional list of related URLs for additional information.
42
+ disaster: Optional disaster category or description.
43
+ """
44
+
45
+ code: str
46
+ public_law: str
47
+ title: Optional[str] = None
48
+ urls: Optional[List[str]] = None
49
+ disaster: Optional[str] = None
50
+
51
+
52
+ class Agency(LazyRecord):
53
+ """Rich wrapper around a USAspending toptier agency record.
54
+
55
+ This model represents a toptier agency with its essential properties.
56
+ For subtier agency information, use the SubTierAgency model separately.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ data: Dict[str, Any],
62
+ client: USASpendingClient,
63
+ subtier_data: Optional[Dict[str, Any]] = None,
64
+ ):
65
+ """Initialize Agency instance.
66
+
67
+ Args:
68
+ data: Toptier agency data merged with top-level agency fields
69
+ client: USASpendingClient client instance
70
+ subtier_data: Optional subtier agency data for subtier_agency property
71
+ """
72
+ # Use the base validation method (dict-only)
73
+ raw = self.validate_init_data(data, "Agency", allow_string_id=False)
74
+ super().__init__(raw, client)
75
+
76
+ # Store subtier data separately
77
+ self._subtier_data = subtier_data
78
+
79
+ def _fetch_details(self) -> Optional[Dict[str, Any]]:
80
+ """Fetch full agency details if we have a toptier_code and client.
81
+
82
+ Returns:
83
+ Optional[Dict[str, Any]]: Full agency data from the API, or None
84
+ if unable to fetch due to missing toptier_code or API error.
85
+ """
86
+ # Try to get toptier_code from existing data
87
+ toptier_code = None
88
+ if "toptier_code" in self._data:
89
+ toptier_code = self._data["toptier_code"]
90
+ elif "code" in self._data:
91
+ toptier_code = self._data["code"]
92
+
93
+ try:
94
+ # Fetch full agency details using the toptier_code
95
+ from ..queries.agency_query import AgencyQuery
96
+
97
+ query = AgencyQuery(self._client)
98
+
99
+ # Get fiscal_year if available in current data
100
+ fiscal_year = self._data.get("fiscal_year")
101
+ full_agency = query._get_resource_with_params(toptier_code, fiscal_year)
102
+
103
+ return full_agency
104
+ except Exception as e:
105
+ # Log but don't raise - lazy loading should fail gracefully
106
+ logger.debug(f"Could not fetch agency details for {toptier_code}: {e}")
107
+ return None
108
+
109
+ def _get_award_summary(
110
+ self,
111
+ award_type_codes: Optional[List[str]] = None,
112
+ fiscal_year: Optional[int] = None,
113
+ agency_type: str = "awarding",
114
+ ) -> Optional[Dict[str, Any]]:
115
+ """Fetch award summary data for a given agency code.
116
+
117
+ Args:
118
+ award_type_codes (Optional[List[str]]): Optional list of award type codes
119
+ to filter by. If None, includes all award types.
120
+ fiscal_year (Optional[int]): Fiscal year to filter by. If None,
121
+ defaults to the current fiscal year.
122
+ agency_type (str): Type of agency relationship to filter by.
123
+ Must be "awarding" or "funding". Defaults to "awarding".
124
+
125
+ Returns:
126
+ Optional[Dict[str, Any]]: Award summary data dictionary containing
127
+ obligations, transaction counts, and other summary metrics,
128
+ or None if unable to fetch due to missing agency code or API error.
129
+ """
130
+ # Get toptier code
131
+ toptier_code = self.code
132
+ if not toptier_code:
133
+ logger.error("Cannot fetch agency award summaries without agency code.")
134
+ return None
135
+
136
+ try:
137
+ from ..queries.agency_award_summary import AgencyAwardSummary
138
+
139
+ query = AgencyAwardSummary(self._client)
140
+
141
+ return query.get_awards_summary(
142
+ toptier_code=toptier_code,
143
+ fiscal_year=fiscal_year,
144
+ agency_type=agency_type,
145
+ award_type_codes=award_type_codes,
146
+ )
147
+ except Exception as e:
148
+ logger.error(f"Could not fetch award summary for {toptier_code}: {e}")
149
+ return {}
150
+
151
+ # Properties from full agency API endpoint
152
+
153
+ @property
154
+ def fiscal_year(self) -> Optional[int]:
155
+ """Fiscal year for the agency data.
156
+
157
+ Returns:
158
+ Optional[int]: The fiscal year for this agency's data,
159
+ or None if not available.
160
+ """
161
+ fiscal_year = self._lazy_get("fiscal_year")
162
+ return to_int(fiscal_year)
163
+
164
+ @property
165
+ def toptier_code(self) -> Optional[str]:
166
+ """Agency toptier code (3-4 digit string).
167
+
168
+ This is the Treasury Account Fund Symbol (TAFS) used to identify
169
+ the agency at the top-tier level in the government hierarchy.
170
+
171
+ Returns:
172
+ Optional[str]: The toptier agency code, or None if not available.
173
+ """
174
+ return self._lazy_get("toptier_code", "code")
175
+
176
+ @property
177
+ def code(self) -> Optional[str]:
178
+ """Alias for toptier_code property.
179
+
180
+ Returns:
181
+ Optional[str]: The toptier agency code, or None if not available.
182
+ """
183
+ return self.toptier_code
184
+
185
+ @property
186
+ def name(self) -> Optional[str]:
187
+ """Primary agency name.
188
+
189
+ Returns:
190
+ Optional[str]: The official name of the agency,
191
+ or None if not available.
192
+ """
193
+ # Agency now contains toptier data directly
194
+ return self._lazy_get("name")
195
+
196
+ @property
197
+ def abbreviation(self) -> Optional[str]:
198
+ """Primary agency abbreviation.
199
+
200
+ Returns:
201
+ Optional[str]: The official abbreviation of the agency,
202
+ or None if not available.
203
+ """
204
+ # Agency now contains toptier data directly
205
+ return self._lazy_get("abbreviation")
206
+
207
+ @property
208
+ def id(self):
209
+ """Internal identifier from USASpending.gov.
210
+
211
+ Returns:
212
+ Optional[int]: The internal agency identifier,
213
+ or None if not available.
214
+ """
215
+ return self.agency_id
216
+
217
+ @property
218
+ def agency_id(self) -> Optional[int]:
219
+ """Internal identifier from USASpending.gov.
220
+
221
+ Returns:
222
+ Optional[int]: The internal agency identifier used by the API,
223
+ or None if not available.
224
+ """
225
+ agency_id = self._lazy_get("agency_id", "id")
226
+ return to_int(agency_id)
227
+
228
+ @property
229
+ def icon_filename(self) -> Optional[str]:
230
+ """Filename of the agency's icon/logo.
231
+
232
+ Returns:
233
+ Optional[str]: The filename of the agency's icon or logo image,
234
+ or None if not available.
235
+ """
236
+ return self._lazy_get("icon_filename")
237
+
238
+ @property
239
+ def mission(self) -> Optional[str]:
240
+ """Agency mission statement.
241
+
242
+ Returns:
243
+ Optional[str]: The official mission statement of the agency,
244
+ or None if not available.
245
+ """
246
+ return self._lazy_get("mission")
247
+
248
+ @property
249
+ def website(self) -> Optional[str]:
250
+ """Agency website URL.
251
+
252
+ Returns:
253
+ Optional[str]: The official website URL of the agency,
254
+ or None if not available.
255
+ """
256
+ return self._lazy_get("website")
257
+
258
+ @property
259
+ def congressional_justification_url(self) -> Optional[str]:
260
+ """URL to the agency's congressional justification.
261
+
262
+ Returns:
263
+ Optional[str]: The URL to the agency's congressional justification document,
264
+ or None if not available.
265
+ """
266
+ return self._lazy_get("congressional_justification_url")
267
+
268
+ @property
269
+ def about_agency_data(self) -> Optional[str]:
270
+ """Additional information about the agency's data.
271
+
272
+ Returns:
273
+ Optional[str]: Descriptive text about the agency's data quality,
274
+ coverage, or other relevant information, or None if not available.
275
+ """
276
+ return self._lazy_get("about_agency_data")
277
+
278
+ @property
279
+ def subtier_agency_count(self) -> Optional[int]:
280
+ """Number of subtier agencies under this agency.
281
+
282
+ Returns:
283
+ Optional[int]: The total count of subtier agencies that report
284
+ to this toptier agency, or None if not available.
285
+ """
286
+ count = self._lazy_get("subtier_agency_count")
287
+ return to_int(count)
288
+
289
+ @property
290
+ def messages(self) -> List[str]:
291
+ """API messages related to this agency data.
292
+
293
+ Returns:
294
+ List[str]: List of informational messages from the API
295
+ related to this agency's data. Empty list if no messages.
296
+ """
297
+ messages = self._lazy_get("messages", default=[])
298
+ if not isinstance(messages, list):
299
+ return []
300
+ return messages
301
+
302
+ @property
303
+ def def_codes(self) -> List[DefCode]:
304
+ """List of Disaster Emergency Fund Codes (DEFC) for this agency.
305
+
306
+ Returns:
307
+ List[DefCode]: List of DefCode dataclass instances containing
308
+ disaster emergency fund codes associated with this agency.
309
+ Empty list if no DEFCs are available.
310
+ """
311
+ def_codes_data = self._lazy_get("def_codes", default=[])
312
+ if not isinstance(def_codes_data, list):
313
+ return []
314
+
315
+ result = []
316
+ for code_data in def_codes_data:
317
+ if isinstance(code_data, dict):
318
+ # Handle the case where urls might be a string or list
319
+ urls = code_data.get("urls")
320
+ if isinstance(urls, str):
321
+ urls = [urls] if urls else None
322
+ elif urls and not isinstance(urls, list):
323
+ urls = None
324
+
325
+ def_code = DefCode(
326
+ code=code_data.get("code", ""),
327
+ public_law=code_data.get("public_law", ""),
328
+ title=code_data.get("title"),
329
+ urls=urls,
330
+ disaster=code_data.get("disaster"),
331
+ )
332
+ result.append(def_code)
333
+
334
+ return result
335
+
336
+ # Properties derived or related to the agency record
337
+ # These properties are not included in the agency detail API endpoint
338
+ # (generally, they come from a related agency properties in an award)
339
+ # so they cannot be lazy-loaded.
340
+
341
+ @property
342
+ def has_agency_page(self) -> bool:
343
+ """Whether this agency has a dedicated page on USASpending.gov.
344
+
345
+ Returns:
346
+ bool: True if the agency has a dedicated profile page on USASpending.gov,
347
+ False otherwise.
348
+ """
349
+ return bool(self.get_value(["has_agency_page"], default=False))
350
+
351
+ @property
352
+ def office_agency_name(self) -> Optional[str]:
353
+ """Name of the specific office within the agency.
354
+
355
+ Returns:
356
+ Optional[str]: The name of a specific office or division within
357
+ the agency, or None if not available.
358
+ """
359
+ return self.get_value("office_agency_name")
360
+
361
+ @property
362
+ def slug(self) -> Optional[str]:
363
+ """URL slug for this agency.
364
+
365
+ Returns:
366
+ Optional[str]: The URL-friendly slug identifier used in
367
+ USASpending.gov URLs for this agency, or None if not available.
368
+ """
369
+ return self.get_value("slug")
370
+
371
+ @property
372
+ def obligations(self) -> Optional[Decimal]:
373
+ """Alias for total_obligations property.
374
+
375
+ Returns:
376
+ Optional[Decimal]: The current fiscal year's total obligations
377
+ for this agency, or None if not available.
378
+ """
379
+ return self.total_obligations
380
+
381
+ # Related and derived resources.
382
+ # Some of these properties are provided by search query
383
+ # results, others are helper methods that provide quick access
384
+ # to related award and transaction data.
385
+
386
+ @cached_property
387
+ def total_obligations(self) -> Optional[Decimal]:
388
+ """Current fiscal year's total obligations made by this agency.
389
+
390
+ Returns:
391
+ Optional[Decimal]: The total dollar amount of obligations
392
+ for the current fiscal year, or None if not available.
393
+ """
394
+ obligations = self.get_value(["total_obligations", "obligations"])
395
+ if not obligations:
396
+ # If not present, fetch from award summary
397
+ obligations = self.get_obligations()
398
+ return obligations
399
+
400
+ @cached_property
401
+ def latest_action_date(self) -> Optional[date]:
402
+ """Date of the most recent action for this agency's awards.
403
+
404
+ Returns:
405
+ Optional[date]: The date of the most recent award action
406
+ associated with this agency, or None if not available.
407
+ """
408
+
409
+ # Check if value is present already (often provided in search results)
410
+ latest_action_date_string = self.get_value("latest_action_date")
411
+
412
+ # If not, fetch from agency award summary endpoint
413
+ if not latest_action_date_string:
414
+ summary = self._get_award_summary()
415
+ latest_action_date_string = summary.get("latest_action_date")
416
+
417
+ return to_date(latest_action_date_string)
418
+
419
+ @cached_property
420
+ def transaction_count(self) -> Optional[int]:
421
+ """Total transaction count for this agency across all awards.
422
+
423
+ Returns:
424
+ Optional[int]: The total number of transactions associated
425
+ with all awards for this agency, or None if not available.
426
+ """
427
+
428
+ # Check if value is present already (often provided in search results)
429
+ transaction_count = self.get_value("transaction_count")
430
+
431
+ # If not, fetch from agency award summary endpoint
432
+ if not transaction_count:
433
+ transaction_count = self.get_transaction_count()
434
+
435
+ return to_int(transaction_count)
436
+
437
+ @property
438
+ def awards(self) -> "AwardsSearch":
439
+ """Get an AwardsSearch instance pre-filtered to this agency as awarding agency.
440
+
441
+ Returns:
442
+ AwardsSearch: A query builder instance pre-filtered to show awards
443
+ where this agency is the top-tier awarding agency. Use this to
444
+ search and filter awards associated with this agency.
445
+ """
446
+ return self._client.awards.search().agency(self.name, "awarding", "toptier")
447
+
448
+ @property
449
+ def subagencies(self) -> List["SubTierAgency"]:
450
+ """Get list of subtier agencies under this toptier agency.
451
+
452
+ Returns:
453
+ List[SubTierAgency]: List of SubTierAgency model instances
454
+ representing all subtier agencies that report to this
455
+ toptier agency. Returns empty list if no subagencies
456
+ are found or if there's an API error.
457
+ """
458
+ # Get toptier code
459
+ toptier_code = self.code
460
+ if not toptier_code:
461
+ logger.error("Cannot fetch sub-agencies without agency code.")
462
+ return []
463
+
464
+ try:
465
+ from ..queries.sub_agency_query import SubAgencyQuery
466
+ from .subtier_agency import SubTierAgency
467
+
468
+ query = SubAgencyQuery(self._client)
469
+
470
+ # Use fiscal year from this agency if available
471
+ fiscal_year = self.fiscal_year
472
+
473
+ response = query.get_subagencies(
474
+ toptier_code=toptier_code,
475
+ fiscal_year=fiscal_year,
476
+ limit=100, # Default to maximum
477
+ )
478
+
479
+ # Transform results into SubTierAgency objects
480
+ subagencies = []
481
+ results = response.get("results", [])
482
+ for result in results:
483
+ if isinstance(result, dict):
484
+ subagency = SubTierAgency(result, self._client)
485
+ subagencies.append(subagency)
486
+
487
+ return subagencies
488
+
489
+ except Exception as e:
490
+ logger.debug(f"Could not fetch sub-agencies for {toptier_code}: {e}")
491
+ return []
492
+
493
+ def get_obligations(
494
+ self,
495
+ fiscal_year: Optional[int] = None,
496
+ agency_type: str = "awarding",
497
+ award_type_codes: Optional[List[str]] = None,
498
+ ) -> Optional[float]:
499
+ """Get obligations for this agency, optionally filtered.
500
+
501
+ Args:
502
+ fiscal_year (Optional[int]): Fiscal year to filter by.
503
+ If None, defaults to the current fiscal year.
504
+ agency_type (str): Type of agency relationship to filter by.
505
+ Must be "awarding" or "funding". Defaults to "awarding".
506
+ award_type_codes (Optional[List[str]]): Optional list of award type codes
507
+ to filter by. If None, includes all award types.
508
+
509
+ Returns:
510
+ Optional[float]: Total obligations amount as a float,
511
+ or None if unavailable due to missing data or API error.
512
+ """
513
+
514
+ # Fetch from award summary API
515
+ summary = self._get_award_summary(
516
+ award_type_codes=award_type_codes,
517
+ fiscal_year=fiscal_year,
518
+ agency_type=agency_type,
519
+ )
520
+ return to_decimal(summary.get("obligations")) if summary else None
521
+
522
+ @cached_property
523
+ def contract_obligations(self) -> Optional[Decimal]:
524
+ """Contract obligations for this agency in the current fiscal year.
525
+
526
+ Returns:
527
+ Optional[Decimal]: The total dollar amount of contract obligations
528
+ for this agency, or None if unavailable.
529
+ """
530
+ summary = self._get_award_summary(award_type_codes=list(CONTRACT_CODES))
531
+ return to_decimal(summary.get("obligations")) if summary else None
532
+
533
+ @cached_property
534
+ def grant_obligations(self) -> Optional[Decimal]:
535
+ """Grant obligations for this agency in the current fiscal year.
536
+
537
+ Returns:
538
+ Optional[Decimal]: The total dollar amount of grant obligations
539
+ for this agency, or None if unavailable.
540
+ """
541
+ summary = self._get_award_summary(award_type_codes=list(GRANT_CODES))
542
+ return to_decimal(summary.get("obligations")) if summary else None
543
+
544
+ @cached_property
545
+ def idv_obligations(self) -> Optional[Decimal]:
546
+ """Indefinite Delivery Vehicle (IDV) obligations for this agency.
547
+
548
+ Returns:
549
+ Optional[Decimal]: The total dollar amount of IDV obligations
550
+ for this agency in the current fiscal year, or None if unavailable.
551
+ """
552
+ summary = self._get_award_summary(award_type_codes=list(IDV_CODES))
553
+ return to_decimal(summary.get("obligations")) if summary else None
554
+
555
+ @cached_property
556
+ def loan_obligations(self) -> Optional[Decimal]:
557
+ """Loan obligations for this agency in the current fiscal year.
558
+
559
+ Returns:
560
+ Optional[Decimal]: The total dollar amount of loan obligations
561
+ for this agency, or None if unavailable.
562
+ """
563
+ summary = self._get_award_summary(award_type_codes=list(LOAN_CODES))
564
+ return to_decimal(summary.get("obligations")) if summary else None
565
+
566
+ @cached_property
567
+ def direct_payment_obligations(self) -> Optional[Decimal]:
568
+ """Direct payment obligations for this agency in the current fiscal year.
569
+
570
+ Returns:
571
+ Optional[Decimal]: The total dollar amount of direct payment obligations
572
+ for this agency, or None if unavailable.
573
+ """
574
+ summary = self._get_award_summary(award_type_codes=list(DIRECT_PAYMENT_CODES))
575
+ return to_decimal(summary.get("obligations")) if summary else None
576
+
577
+ @cached_property
578
+ def other_obligations(self) -> Optional[Decimal]:
579
+ """Other assistance obligations for this agency in the current fiscal year.
580
+
581
+ Returns:
582
+ Optional[Decimal]: The total dollar amount of other assistance obligations
583
+ for this agency, or None if unavailable.
584
+ """
585
+ summary = self._get_award_summary(award_type_codes=list(OTHER_CODES))
586
+ return to_decimal(summary.get("obligations")) if summary else None
587
+
588
+ def get_transaction_count(
589
+ self,
590
+ fiscal_year: Optional[int] = None,
591
+ agency_type: str = "awarding",
592
+ award_type_codes: Optional[List[str]] = None,
593
+ ) -> Optional[int]:
594
+ """Get transaction count for this agency, optionally filtered.
595
+
596
+ Args:
597
+ fiscal_year (Optional[int]): Fiscal year to filter by.
598
+ If None, uses the agency's fiscal year or current fiscal year.
599
+ agency_type (str): Type of agency relationship to filter by.
600
+ Must be "awarding" or "funding". Defaults to "awarding".
601
+ award_type_codes (Optional[List[str]]): Optional list of award type codes
602
+ to filter by. If None, includes all award types.
603
+
604
+ Returns:
605
+ Optional[int]: Total transaction count as an integer,
606
+ or None if unavailable due to missing data or API error.
607
+ """
608
+ # If no filters and we have existing data, return it
609
+ if not any([fiscal_year, award_type_codes]) and agency_type == "awarding":
610
+ existing = self._lazy_get("transaction_count")
611
+ if existing is not None:
612
+ return to_int(existing)
613
+
614
+ # Fetch from award summary API
615
+ summary = self._get_award_summary(
616
+ award_type_codes=award_type_codes,
617
+ fiscal_year=fiscal_year,
618
+ agency_type=agency_type,
619
+ )
620
+ return to_int(summary.get("transaction_count")) if summary else None
621
+
622
+ def __repr__(self) -> str:
623
+ """String representation of Agency.
624
+
625
+ Returns:
626
+ str: A string representation showing the agency code and name
627
+ in the format "<Agency CODE: NAME>".
628
+ """
629
+ name = self.name or "?"
630
+ code = self.code or "?"
631
+ return f"<Agency {code}: {name}>"