usaspending-orm 0.7.3__tar.gz → 0.8.0__tar.gz

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 (270) hide show
  1. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/.gitignore +3 -0
  2. usaspending_orm-0.8.0/CHANGELOG.md +468 -0
  3. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/PKG-INFO +16 -9
  4. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/README.md +13 -8
  5. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/pyproject.toml +18 -1
  6. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/client.py +2 -2
  7. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/logging_config.py +0 -21
  8. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/agency.py +41 -29
  9. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/award.py +181 -238
  10. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/award_account.py +10 -9
  11. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/award_factory.py +46 -13
  12. usaspending_orm-0.8.0/src/usaspending/models/award_identifier.py +73 -0
  13. usaspending_orm-0.8.0/src/usaspending/models/award_types.py +356 -0
  14. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/base_model.py +79 -46
  15. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/contract.py +13 -80
  16. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/district_spending.py +2 -22
  17. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/download.py +1 -1
  18. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/federal_account.py +37 -9
  19. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/funding.py +12 -15
  20. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/grant.py +2 -2
  21. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/idv.py +14 -105
  22. usaspending_orm-0.8.0/src/usaspending/models/lazy_record.py +151 -0
  23. usaspending_orm-0.8.0/src/usaspending/models/loan.py +83 -0
  24. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/location.py +7 -6
  25. usaspending_orm-0.8.0/src/usaspending/models/period_of_performance.py +121 -0
  26. usaspending_orm-0.8.0/src/usaspending/models/procurement_award.py +117 -0
  27. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/recipient.py +79 -49
  28. usaspending_orm-0.8.0/src/usaspending/models/recipient_spending.py +72 -0
  29. usaspending_orm-0.8.0/src/usaspending/models/spending.py +143 -0
  30. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/state_spending.py +2 -22
  31. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/subaward.py +11 -17
  32. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/subtier_agency.py +10 -3
  33. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/transaction.py +37 -41
  34. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/treasury_account_symbol.py +3 -4
  35. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/__init__.py +3 -1
  36. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/agencies_search.py +12 -35
  37. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/agency_award_summary.py +17 -51
  38. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/agency_query.py +3 -11
  39. usaspending_orm-0.8.0/src/usaspending/queries/award_accounts_query.py +85 -0
  40. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/award_query.py +1 -1
  41. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/awarding_agencies_search.py +7 -14
  42. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/awards_search.py +62 -130
  43. usaspending_orm-0.8.0/src/usaspending/queries/base_query.py +239 -0
  44. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/client_side_query_builder.py +51 -28
  45. usaspending_orm-0.8.0/src/usaspending/queries/federal_accounts_query.py +112 -0
  46. usaspending_orm-0.8.0/src/usaspending/queries/filter_tree_query.py +263 -0
  47. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/filters.py +80 -6
  48. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/funding_agencies_search.py +7 -14
  49. usaspending_orm-0.8.0/src/usaspending/queries/funding_search.py +66 -0
  50. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/idv_child_awards.py +14 -61
  51. usaspending_orm-0.8.0/src/usaspending/queries/mixins.py +154 -0
  52. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/query_builder.py +308 -156
  53. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/recipient_query.py +3 -31
  54. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/recipients_search.py +7 -6
  55. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/single_resource_base.py +4 -2
  56. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/spending_search.py +15 -62
  57. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/sub_agency_query.py +19 -57
  58. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/subawards_search.py +7 -27
  59. usaspending_orm-0.8.0/src/usaspending/queries/tas_agencies_query.py +72 -0
  60. usaspending_orm-0.8.0/src/usaspending/queries/tas_codes_query.py +122 -0
  61. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/queries/transactions_search.py +90 -106
  62. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/agency_resource.py +30 -22
  63. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/award_accounts_resource.py +1 -1
  64. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/award_resource.py +12 -22
  65. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/funding_resource.py +4 -4
  66. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/recipients_resource.py +48 -21
  67. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/spending_resource.py +7 -3
  68. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/subawards_resource.py +5 -4
  69. usaspending_orm-0.8.0/src/usaspending/resources/tas_resource.py +65 -0
  70. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/transactions_resource.py +2 -3
  71. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/utils/__init__.py +2 -0
  72. usaspending_orm-0.8.0/src/usaspending/utils/dates.py +139 -0
  73. usaspending_orm-0.8.0/src/usaspending/utils/numbers.py +99 -0
  74. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/utils/retry.py +4 -9
  75. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/utils/special_cases.yaml +1 -2
  76. usaspending_orm-0.8.0/src/usaspending/utils/textcase.py +379 -0
  77. usaspending_orm-0.8.0/src/usaspending/utils/validations.py +388 -0
  78. usaspending_orm-0.8.0/tests/fixtures/golden/agency.json +74 -0
  79. usaspending_orm-0.8.0/tests/fixtures/golden/award_account_first_result.json +29 -0
  80. usaspending_orm-0.8.0/tests/fixtures/golden/award_contract.json +190 -0
  81. usaspending_orm-0.8.0/tests/fixtures/golden/award_contract_awarding_agency.json +74 -0
  82. usaspending_orm-0.8.0/tests/fixtures/golden/award_contract_period_of_performance.json +15 -0
  83. usaspending_orm-0.8.0/tests/fixtures/golden/award_contract_place_of_performance.json +23 -0
  84. usaspending_orm-0.8.0/tests/fixtures/golden/award_contract_recipient.json +51 -0
  85. usaspending_orm-0.8.0/tests/fixtures/golden/award_grant.json +198 -0
  86. usaspending_orm-0.8.0/tests/fixtures/golden/award_idv.json +190 -0
  87. usaspending_orm-0.8.0/tests/fixtures/golden/award_loan.json +191 -0
  88. usaspending_orm-0.8.0/tests/fixtures/golden/award_parent_of_contract.json +136 -0
  89. usaspending_orm-0.8.0/tests/fixtures/golden/funding_first_result.json +35 -0
  90. usaspending_orm-0.8.0/tests/fixtures/golden/idv_child_award_first_result.json +192 -0
  91. usaspending_orm-0.8.0/tests/fixtures/golden/recipient.json +60 -0
  92. usaspending_orm-0.8.0/tests/fixtures/golden/search_contract_first_result.json +190 -0
  93. usaspending_orm-0.8.0/tests/fixtures/golden/search_subaward_first_result.json +91 -0
  94. usaspending_orm-0.8.0/tests/fixtures/golden/spending_by_recipient_first_result.json +68 -0
  95. usaspending_orm-0.8.0/tests/fixtures/golden/spending_by_state_first_result.json +17 -0
  96. usaspending_orm-0.8.0/tests/fixtures/golden/transaction_first_result.json +18 -0
  97. usaspending_orm-0.8.0/tests/fixtures/public_api_surface.json +1411 -0
  98. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/mocks/README.md +29 -3
  99. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/mocks/mock_client.py +70 -9
  100. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/mocks/response_builder.py +5 -0
  101. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_agency.py +11 -38
  102. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_agency_subagencies.py +5 -5
  103. usaspending_orm-0.8.0/tests/models/test_agency_tas_integration.py +364 -0
  104. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_award_account.py +16 -2
  105. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_award_agency_integration.py +63 -0
  106. usaspending_orm-0.8.0/tests/models/test_award_category_registry.py +159 -0
  107. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_award_common.py +14 -62
  108. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_award_decimal_precision.py +39 -12
  109. usaspending_orm-0.8.0/tests/models/test_award_identifier.py +81 -0
  110. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_award_types.py +10 -4
  111. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_base_model.py +60 -2
  112. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_federal_account.py +130 -0
  113. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_funding.py +22 -8
  114. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_lazy_load.py +161 -1
  115. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_location.py +9 -11
  116. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_period_of_performance.py +113 -1
  117. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_recipient.py +188 -37
  118. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_recipient_spending.py +6 -4
  119. usaspending_orm-0.8.0/tests/models/test_search_projection.py +175 -0
  120. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_subaward.py +16 -23
  121. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_subtier_agency.py +4 -4
  122. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_transaction.py +30 -0
  123. usaspending_orm-0.8.0/tests/models/test_volatile_field_mapping.py +177 -0
  124. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_agency_award_summary.py +17 -17
  125. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_award_accounts_query.py +28 -4
  126. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_client_side_query_builder.py +94 -2
  127. usaspending_orm-0.8.0/tests/queries/test_count_respects_limits.py +217 -0
  128. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_federal_accounts_query.py +8 -1
  129. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_query_builder.py +52 -0
  130. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_query_builder_list_behavior.py +98 -49
  131. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_query_builder_pagination.py +3 -2
  132. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_recipient_query.py +6 -2
  133. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_search_query_builder.py +38 -0
  134. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_spending_search.py +12 -0
  135. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_sub_agency_query.py +2 -2
  136. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_subawards_search.py +2 -2
  137. usaspending_orm-0.8.0/tests/queries/test_tas_agencies_query.py +84 -0
  138. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_tas_codes_query.py +61 -1
  139. usaspending_orm-0.8.0/tests/queries/test_transactions_search.py +475 -0
  140. usaspending_orm-0.8.0/tests/resources/test_recipients_resource.py +71 -0
  141. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/resources/test_tas_resource.py +56 -8
  142. usaspending_orm-0.8.0/tests/snapshot_support.py +70 -0
  143. usaspending_orm-0.8.0/tests/test_characterization.py +282 -0
  144. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_download_resource.py +97 -0
  145. usaspending_orm-0.8.0/tests/test_golden_master_integration.py +707 -0
  146. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_logging_config.py +0 -20
  147. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_mock_client_example.py +11 -8
  148. usaspending_orm-0.8.0/tests/test_public_api_surface.py +388 -0
  149. usaspending_orm-0.8.0/tests/utils/test_dates.py +288 -0
  150. usaspending_orm-0.8.0/tests/utils/test_doctests.py +66 -0
  151. usaspending_orm-0.8.0/tests/utils/test_numbers.py +73 -0
  152. usaspending_orm-0.8.0/tests/utils/test_special_cases_data.py +118 -0
  153. usaspending_orm-0.7.3/tests/utils/test_text_formatter.py → usaspending_orm-0.8.0/tests/utils/test_textcase.py +267 -40
  154. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/utils/test_validations.py +153 -0
  155. usaspending_orm-0.8.0/tests/volatile_fields.py +293 -0
  156. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/uv.lock +244 -2
  157. usaspending_orm-0.7.3/CHANGELOG.md +0 -81
  158. usaspending_orm-0.7.3/CLAUDE.md +0 -243
  159. usaspending_orm-0.7.3/poetry.lock +0 -585
  160. usaspending_orm-0.7.3/src/usaspending/models/award_types.py +0 -145
  161. usaspending_orm-0.7.3/src/usaspending/models/lazy_record.py +0 -90
  162. usaspending_orm-0.7.3/src/usaspending/models/loan.py +0 -115
  163. usaspending_orm-0.7.3/src/usaspending/models/period_of_performance.py +0 -75
  164. usaspending_orm-0.7.3/src/usaspending/models/recipient_spending.py +0 -84
  165. usaspending_orm-0.7.3/src/usaspending/models/spending.py +0 -103
  166. usaspending_orm-0.7.3/src/usaspending/queries/award_accounts_query.py +0 -194
  167. usaspending_orm-0.7.3/src/usaspending/queries/base_query.py +0 -141
  168. usaspending_orm-0.7.3/src/usaspending/queries/federal_accounts_query.py +0 -205
  169. usaspending_orm-0.7.3/src/usaspending/queries/funding_search.py +0 -174
  170. usaspending_orm-0.7.3/src/usaspending/queries/tas_codes_query.py +0 -213
  171. usaspending_orm-0.7.3/src/usaspending/resources/tas_resource.py +0 -77
  172. usaspending_orm-0.7.3/src/usaspending/utils/formatter.py +0 -449
  173. usaspending_orm-0.7.3/src/usaspending/utils/validations.py +0 -148
  174. usaspending_orm-0.7.3/tests/models/test_agency_tas_integration.py +0 -172
  175. usaspending_orm-0.7.3/tests/queries/test_transactions_search.py +0 -126
  176. usaspending_orm-0.7.3/tests/resources/test_recipients_resource.py +0 -29
  177. usaspending_orm-0.7.3/tests/utils/test_formatter.py +0 -301
  178. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/LICENSE +0 -0
  179. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/api-docs-links.md +0 -0
  180. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/__init__.py +0 -0
  181. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/cli/__init__.py +0 -0
  182. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/cli/download_award.py +0 -0
  183. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/config.py +0 -0
  184. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/download/__init__.py +0 -0
  185. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/download/job.py +0 -0
  186. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/download/manager.py +0 -0
  187. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/exceptions.py +0 -0
  188. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/__init__.py +0 -0
  189. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/models/recipient_business_categories.py +0 -0
  190. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/__init__.py +0 -0
  191. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/base_resource.py +0 -0
  192. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/resources/download_resource.py +0 -0
  193. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/src/usaspending/utils/rate_limit.py +0 -0
  194. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/__init__.py +0 -0
  195. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/conftest.py +0 -0
  196. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/agency.json +0 -0
  197. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/agency_autocomplete.json +0 -0
  198. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/agency_award_summary.json +0 -0
  199. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/agency_subagencies.json +0 -0
  200. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/accounts.json +0 -0
  201. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/award_funding_grant.json +0 -0
  202. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/contract.json +0 -0
  203. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/grant.json +0 -0
  204. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/idv.json +0 -0
  205. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/idv_awards.json +0 -0
  206. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/loan.json +0 -0
  207. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/search_results_contracts.json +0 -0
  208. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/search_results_grants.json +0 -0
  209. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/search_results_idvs.json +0 -0
  210. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/search_results_subawards.json +0 -0
  211. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/awards/transactions.json +0 -0
  212. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/download_assistance.json +0 -0
  213. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/download_search.json +0 -0
  214. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/download_status.json +0 -0
  215. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/recipient_university.json +0 -0
  216. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/recipients_search.json +0 -0
  217. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/spending_by_award_contracts.json +0 -0
  218. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/spending_by_district.json +0 -0
  219. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/spending_by_recipient.json +0 -0
  220. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/spending_by_state.json +0 -0
  221. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/tas_agencies.json +0 -0
  222. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/tas_codes.json +0 -0
  223. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/tas_federal_accounts.json +0 -0
  224. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/top_recipients_params.json +0 -0
  225. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/fixtures/top_recipients_response.json +0 -0
  226. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/mocks/__init__.py +0 -0
  227. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/mocks/test_mock_client.py +0 -0
  228. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/__init__.py +0 -0
  229. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_award_factory.py +0 -0
  230. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_award_lazy_loading.py +0 -0
  231. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_award_office_integration.py +0 -0
  232. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_award_type_constants.py +0 -0
  233. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_district_spending.py +0 -0
  234. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_recipient_business_categories.py +0 -0
  235. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_spending.py +0 -0
  236. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_state_spending.py +0 -0
  237. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/models/test_treasury_account_symbol.py +0 -0
  238. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_agencies_search.py +0 -0
  239. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_agency_query.py +0 -0
  240. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_awards_search.py +0 -0
  241. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_filters.py +0 -0
  242. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_funding_search.py +0 -0
  243. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_idv_child_awards.py +0 -0
  244. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_recipients_search.py +0 -0
  245. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_single_resource_base.py +0 -0
  246. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/queries/test_spending_search_count.py +0 -0
  247. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/resources/test_agency_resource.py +0 -0
  248. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/resources/test_award_resource.py +0 -0
  249. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/resources/test_funding_resource.py +0 -0
  250. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/resources/test_spending_resource.py +0 -0
  251. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/resources/test_subawards_resource.py +0 -0
  252. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/resources/test_transactions_resource.py +0 -0
  253. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_award_accounts_integration.py +0 -0
  254. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_cache_fallback.py +0 -0
  255. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_client.py +0 -0
  256. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_client_caching.py +0 -0
  257. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_client_funding.py +0 -0
  258. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_client_rate_limit.py +0 -0
  259. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_client_security.py +0 -0
  260. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_client_transactions.py +0 -0
  261. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_config.py +0 -0
  262. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_exceptions.py +0 -0
  263. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_integration.py +0 -0
  264. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_readme_examples.py +0 -0
  265. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/test_spending_integration.py +0 -0
  266. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/utils/__init__.py +0 -0
  267. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/utils/decimal_helpers.py +0 -0
  268. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/utils/test_rate_limit.py +0 -0
  269. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/utils/test_retry.py +0 -0
  270. {usaspending_orm-0.7.3 → usaspending_orm-0.8.0}/tests/utils/test_retry_session_reset.py +0 -0
@@ -4,3 +4,6 @@ __pycache__
4
4
  .*_cache
5
5
  repomix*
6
6
  dist/
7
+ .claude/
8
+ /~/
9
+ .coverage
@@ -0,0 +1,468 @@
1
+ # Changelog
2
+
3
+ All notable changes to the USASpending ORM library are documented here.
4
+ The format loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and the project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.8.0] - 2026-07-28
8
+
9
+ Mostly refactoring internals to DRY the codebase and standardize the query interface.
10
+ It now keeps one set of promises everywhere: `count()`, `len()`, iteration, indexing, slicing and
11
+ truthiness agree under the bounds you set. Adding CI tests to verify compatability for Pythons 3.9 - 3.14.
12
+
13
+ **Breaking changes**
14
+
15
+ - Optional money and string getters return `None` when the API reports no
16
+ value; a reported zero is still `Decimal("0.00")`. Format with a fallback:
17
+ `f"{award.total_obligation or 0:,.2f}"`.
18
+ - `count()` honors `limit()` and `max_pages()` on every query. Read the server
19
+ total by counting before bounding (i.e. it will never return a count greater
20
+ that your set limit).
21
+ - A lazy load that fails now raises instead of silently answering `None`
22
+ forever. Only the API's "no such record" answers still read as absent data.
23
+ - `since()` and `until()` raise `ValidationError` for a pre-FY2008 bound (the lower
24
+ limit for USASpending data) or given an inverted range instead of silently matching
25
+ nothing useful.
26
+ - `order_by()` raises `ValidationError` on every builder for a direction other
27
+ than `"asc"` or `"desc"`.
28
+ - `client.tas.agencies` returns a query object rather than a list. So `== []` no
29
+ longer works as an emptiness check.
30
+ - A multi-level recipient ID now resolves away from the often-empty `R` level,
31
+ changing which record `find_by_recipient_id()` returns for it.
32
+ - Removed without alias: the internal `usaspending.utils.formatter` module,
33
+ module-level `logging_config.get_logger`, `Agency.__init__`'s unused
34
+ `subtier_data` parameter, and `AgencyAwardSummary.find_by_id`.
35
+ - `Award.start_date` and `end_date` changed precedence for hand-built payloads
36
+ carrying several date spellings; live API data is unaffected.
37
+
38
+ ### Added
39
+
40
+ - `Recipient.recipient_level`: which level of the recipient hierarchy a record
41
+ describes, as `"C"` (child), `"P"` (parent) or `"R"` (no parent).
42
+
43
+ - `usaspending.utils.current_fiscal_year()`: the federal fiscal year today
44
+ falls in, as an int. October 1 opens the fiscal year named for the calendar
45
+ year it ends in, so it reports 2026 on 2026-09-30 and 2027 on 2026-10-01.
46
+ Relocated from the removed `usaspending.utils.formatter.current_fiscal_year`.
47
+
48
+ ### Changed
49
+
50
+ - Optional money and string getters return `None` when the API reports no
51
+ value, instead of a fabricated `Decimal("0.00")` or `""`. A value the API
52
+ actually reports as zero still returns `Decimal("0.00")`, so callers can
53
+ finally tell "this award has no such figure" from "this figure is zero";
54
+ previously both produced `Decimal("0.00")` and the distinction was
55
+ unrecoverable.
56
+
57
+ Affected properties:
58
+
59
+ - `Award.total_obligation`, `Award.award_amount`
60
+ - `Award.covid19_obligations`, `Award.covid19_outlays`
61
+ - `Award.infrastructure_obligations`, `Award.infrastructure_outlays`
62
+ - `AwardAccount.total_transaction_obligated_amount` and its alias
63
+ `AwardAccount.obligated_amount`
64
+ - `Funding.transaction_obligated_amount` and `Funding.gross_outlay_amount`
65
+ - `Location.zip5` and `Location.district`
66
+ - `Award.type`, its alias `Award.award_type_code`, `Award.type_description`,
67
+ and `Contract.contract_award_type`, which delegates to it. Award-type
68
+ dispatch is unaffected: `create_award` reads the raw payload, not these
69
+ properties.
70
+
71
+ **Migrating.** Any f-string or arithmetic on these needs a fallback:
72
+
73
+ ```python
74
+ # Before
75
+ print(f"${award.total_obligation:,.2f}")
76
+ # After
77
+ print(f"${award.total_obligation or 0:,.2f}")
78
+ ```
79
+
80
+ **This can break working code.** Comparisons against zero change meaning
81
+ deliberately: `award.covid19_obligations == 0` was true for every award
82
+ without COVID-19 funding and is now false, while `is None` identifies
83
+ exactly those awards. On live golden-master data the COVID-19 and
84
+ infrastructure properties and `Funding.transaction_obligated_amount` are the
85
+ ones that move in practice; `total_obligation`, `award_amount`, `zip5` and
86
+ `district` were unchanged there, because the API does report them.
87
+ (`Grant.transaction_obligated_amount` already returned `None` for the same
88
+ property name.) `Award.award_identifier` deliberately still
89
+ returns `""`: it is annotated `-> str`, so there was no `Optional` contract
90
+ to honor. Documentation examples that formatted money were given the same
91
+ `or 0` fallback, and three examples referencing nonexistent properties
92
+ (`award.amount`, `award.recipient_name`, `child.obligated_amount`) were
93
+ corrected along the way.
94
+
95
+ - `count()` now honors `limit()` and `max_pages()` on every query, so
96
+ `count()`, `len(query)` and `len(query.all())` agree under the bounds a
97
+ caller sets. Which answer
98
+ you got used to depend on how the endpoint reports a total: builders backed
99
+ by a count endpoint or page metadata returned the server's figure whatever
100
+ the caller asked for (`limit(3).count()` said 510 while the query yielded
101
+ three rows), while builders that count by walking pages already stopped at
102
+ the bound.
103
+
104
+ **This can break working code** that used `count()` on a bounded query to
105
+ read the server's total. Count before bounding: `search.count()` is the
106
+ total under those filters, `search.limit(3).count()` is what the bounded
107
+ query yields.
108
+
109
+ `config.default_result_limit` is deliberately excluded from counts; it
110
+ still caps iteration, so an unbounded query over more than 10,000 rows
111
+ counts higher than it yields. That exclusion is itself a change for the
112
+ builders whose count pages the result set: `len(award.funding)` on a result
113
+ set larger than the default used to report the cap and now reports the true
114
+ total. Bounds that forbid every result are answered without a request:
115
+ `limit(0)` and `max_pages(0)` count 0 without touching the API, and for an
116
+ in-memory query `bool(query.limit(0))` was True and is now False.
117
+
118
+ - A lazy load that fails no longer turns a model into a permanent source of
119
+ `None`. `Agency` and `Recipient` reported every failure as absent data and
120
+ latched, so one 500, one dropped connection or one read after
121
+ `client.close()` left every lazy property answering `None` for the rest of
122
+ the model's life, with no way to ask again.
123
+
124
+ **This can break working code**, since a failure that used to be silent now
125
+ raises. Only the API's answer that there is no such record is still reported
126
+ as absent data: HTTP 400, 404 and 422 leave the model fetched and answering
127
+ `None`, as does a model built with no id. Everything else now reaches the
128
+ caller, including server errors, rate limits, connection errors, timeouts,
129
+ and the `DetachedInstanceError` from a closed session. The model is left
130
+ unfetched, so the next access retries, and a model whose load failed while
131
+ detached now loads after `reattach()`. `Award` already raised, so it is
132
+ unchanged.
133
+
134
+ **Migrating.** Force the load once rather than guarding each property read,
135
+ which would pay the full retry ladder per read (up to about 88 seconds of
136
+ backoff at default settings):
137
+
138
+ ```python
139
+ try:
140
+ agency.fetch_all_details()
141
+ except (USASpendingError, requests.RequestException):
142
+ logger.warning("Agency details unavailable")
143
+ return None
144
+ print(agency.mission)
145
+ ```
146
+
147
+ - `award.transactions.since()` and `until()` now reject a bound the API cannot
148
+ answer, matching what `time_period()` already enforced. A bound before
149
+ FY2008 begins (2007-10-01) matched every row, so `since("1999-01-01")`
150
+ looked like a filter and did nothing; an inverted range yielded no rows,
151
+ indistinguishable from an award with no transactions in range. Both now
152
+ raise `ValidationError`, checked by whichever of the two calls comes second.
153
+
154
+ **This can break working code**: if you relied on an inverted range to
155
+ select nothing, use `limit(0)` or skip the query; if you relied on a
156
+ pre-FY2008 bound as a harmless no-op, clamp it to `2007-10-01` or drop it.
157
+ Equal bounds still select that single day, and 2007-10-01 itself is valid.
158
+
159
+ - `order_by()` now validates its direction on every builder. Some builders
160
+ already raised, the client-side queries accepted any string and silently
161
+ sorted ascending, and the paginated searches passed the caller's spelling
162
+ through to the API, so `order_by("amount", "sideways")` put
163
+ `"order": "sideways"` on the wire. Every builder now raises
164
+ `ValidationError` for a direction other than `"asc"` or `"desc"`.
165
+
166
+ **This can break working code** that passed an unrecognized direction and
167
+ relied on whatever came back: pass `"asc"` or `"desc"`. Which fields each
168
+ builder accepts is unchanged; a few validator messages were unified in
169
+ wording only, and `time_period()`'s error messages are unchanged.
170
+
171
+ - `client.tas.agencies` returns a `TASAgenciesQuery` instead of
172
+ `list[Agency]`, matching `Agency.federal_accounts` and
173
+ `FederalAccount.tas_codes`. Iteration, `len()`, indexing and slicing work
174
+ unchanged, so most code needs no edit.
175
+
176
+ **This can break working code** that requires a real list: call `.all()`,
177
+ and use `if not query:` rather than `== []` as the emptiness check.
178
+
179
+ In exchange the level is fetched once, filtering costs no further requests,
180
+ and there is no shared list for one caller to corrupt for the next.
181
+ `TASAgenciesQuery` is exported from `usaspending.queries`, with `code()`,
182
+ `codes()` and `description()` filters that were previously unreachable.
183
+
184
+ - `Agency.federal_accounts` and `FederalAccount.tas_codes` fetch their level
185
+ of the TAS tree once per model instead of once per read. No call site
186
+ changes; the request count does. Walking an agency's 16 accounts and
187
+ reading each one's TAS codes cost 33 requests and now costs 17, the floor
188
+ set by the API's one-request-per-level shape; the same walk with in-memory
189
+ `fiscal_year()` filters fell from 49 or 99 requests to the same 17.
190
+
191
+ The tradeoff is staleness: a long-lived `Agency` keeps reporting the
192
+ accounts it first saw. Construct a fresh model to pick up changes, or
193
+ `del agency._federal_accounts_level` to drop the cache; a non-recursive
194
+ `reattach()` also discards it, while a recursive one rebinds the cached
195
+ models in place. This cache is not the one `config` describes:
196
+ `cache_enabled` governs HTTP responses with a TTL, while this one is always
197
+ on, per model instance, without one, so turning response caching off does
198
+ not make these properties re-read. The retained level costs about 0.2 MB
199
+ for a 16-account agency, so a walk over many agencies should drop each
200
+ model as it goes.
201
+
202
+ - `Award.start_date` and `end_date` delegate to `period_of_performance`
203
+ rather than keeping their own copy of the key list, which had drifted from
204
+ the period model's. Three behaviors moved: where a payload carries both the
205
+ flat and the nested spelling, the nested `period_of_performance` object now
206
+ wins; the `"Period of Performance End Date"` alias, which no recorded API
207
+ response carries, is no longer read; and `"Base Obligation Date"` now
208
+ yields to `"Period of Performance Start Date"` where a payload carries
209
+ both.
210
+
211
+ **This can break working code** that feeds `Award` hand-built payloads and
212
+ relies on any old precedence; read `award.raw` directly where the payload's
213
+ spelling matters. Live API responses never carry disagreeing spellings.
214
+ Cost moved rather than vanished: dates parse once on construction, so the
215
+ first read of a single date is slower, repeat reads are roughly four times
216
+ faster, and the crossover lands around six reads; against 0.7.3 every
217
+ measured pattern is still faster.
218
+
219
+ - Date parsing reads the shape the API actually sends about 11 times faster.
220
+ The converter tried seven `strptime` formats in order; plain `YYYY-MM-DD`
221
+ accounts for nearly all API date values and now reads through
222
+ `date.fromisoformat` behind a shape guard, taking 0.18 us rather than
223
+ 2.06 us. Reading `action_date` across a 5000-transaction page falls from
224
+ 11.0 ms to 1.7 ms. CPU only: no request or value changes, and every format
225
+ accepted before is still accepted, verified on Python 3.9 through 3.14.
226
+ The guard exists because 3.11 widened `fromisoformat` to accept forms the
227
+ 3.9 floor refuses, such as `20250829` and ISO week dates; without it the
228
+ accepted set would depend on the interpreter.
229
+
230
+ - Title casing now emits a `UserWarning` when `special_cases.yaml` exists but
231
+ cannot be read as a list, and another naming colliding entries (two
232
+ spellings that lowercase to the same key), where before the problem went
233
+ only to the log and names were silently mis-cased. It still degrades to no
234
+ special casing rather than raising, because casing runs inside `__repr__`
235
+ and a cosmetic problem must not break debuggers or logging; escalate with
236
+ `-W error::UserWarning` if wanted. A merely absent file still warns only in
237
+ the log, since an installation can legitimately lack it. The one duplicate
238
+ entry in the shipped data file was removed.
239
+
240
+ - `to_int` reads numeric API fields leniently: a non-numeric value now yields
241
+ `None` where the integer counts on `Funding`, `SubAward`,
242
+ `TreasuryAccountSymbol` and `Award.subaward_count` previously raised
243
+ `ValueError`.
244
+
245
+ - `SubAward.amount`'s return annotation is corrected from `float | None` to
246
+ `Decimal | None` to match its actual runtime type; no runtime change.
247
+
248
+ ### Deprecated
249
+
250
+ - `parse_date_string`'s `format_str` parameter, scheduled for removal in a
251
+ future release. A non-default pattern still parses with the pattern the
252
+ caller supplied, still raises `ValidationError` for a value it cannot read,
253
+ and now emits a `DeprecationWarning`. Omitting the parameter, or passing
254
+ its default, is unchanged and warns about nothing.
255
+
256
+ ### Fixed
257
+
258
+ - Building a query's payload no longer mutates the filters that built it.
259
+ When two filters serialized under one key, aggregation extended the first
260
+ filter's own list in place, and since clones share filter objects, a
261
+ derived query's payload grew on every build and corrupted its parent:
262
+ `q1 = search().keywords("alpha")` followed by `q2 = q1.keywords("beta")`
263
+ left `q1` sending `["alpha", "beta", "beta"]`. Affected chained `keywords`,
264
+ `award_type_codes`, simple `psc_codes` and `treasury_account_components`
265
+ filters.
266
+
267
+ - `client.recipients.find_by_recipient_id()` no longer addresses the wrong
268
+ record for a recipient ID carrying several levels, such as
269
+ `"<hash>-['C', 'R']"`. Normalization was implemented twice with algorithms
270
+ that disagreed, and the finder's choice, `R`, is the record that reports
271
+ little or no spending for most such IDs, so it could report a recipient
272
+ with real spending as having none and no parent. Both paths now avoid the
273
+ `R` level whenever another is available; to address a level explicitly,
274
+ pass the suffix yourself (see `Recipient.recipient_level` under Added).
275
+ `client.spending.search().recipient_id()` also normalizes its argument now,
276
+ so a multi-level ID copied out of `raw` produces a filter the API can
277
+ match.
278
+
279
+ - `award.transactions` no longer reports a count that disagrees with what it
280
+ yields. `since()` and `until()` filter in memory, but the count came from
281
+ the API, so it included transactions the caller would never see: `count()`
282
+ and `len()` said three where iteration yielded two, slices disagreed with
283
+ `len()`, and `query[-1]` raised `IndexError`. All of them now agree, and
284
+ `limit(1)` means one matching row rather than one row that may then be
285
+ discarded.
286
+
287
+ - A `Recipient` built directly from an award search result now reports its
288
+ `uei` and `location`, which read only the nested spellings a detail
289
+ response sends. `award.recipient.uei` worked because `Award` repaired the
290
+ keys on the way past; `Recipient(row, client).uei` for the same row did
291
+ not. `PeriodOfPerformance` had the same gap for `"Base Obligation Date"`.
292
+ Both models now read every spelling of their own fields, and `Award` hands
293
+ its payload over rather than reproducing the mapping; nested spellings
294
+ still take precedence where a payload carries both.
295
+
296
+ - Indexing and slicing an in-memory query now read the limited collection:
297
+ `agency.federal_accounts.limit(3)[:]` returned every account and
298
+ `limit(3)[5]` returned a row; the slice now holds three rows and the index
299
+ raises `IndexError`, agreeing with `len()` and `.all()`. Present in 0.7.3
300
+ for every in-memory query; the new `tas.agencies` query made it visible.
301
+
302
+ - `Transaction` instances no longer compare equal regardless of their data. A
303
+ fieldless `@dataclass` declaration generated an `__eq__` comparing empty
304
+ tuples, so any two transactions were equal and none was hashable. They now
305
+ compare by identity, like every other model, and can be used in sets and as
306
+ dict keys.
307
+
308
+ - `Recipient.parents` returns a fresh list on each read, so sorting or
309
+ popping the result no longer corrupts the model.
310
+
311
+ - Iterating `idv.child_awards` no longer raises
312
+ `AttributeError: 'str' object has no attribute 'get'` when reading the four
313
+ agency properties. The `/idvs/awards/` endpoint reuses those keys for a
314
+ plain agency-name string, which the model accepted as a record; all four
315
+ now return `None` there, with the name still available via `raw`. Present
316
+ in 0.7.3.
317
+
318
+ - `reattach(recursive=True)` now reaches every nested model that holds the
319
+ client. It recognized only lazy-loading models, so `AwardAccount`,
320
+ `FederalAccount`, `Funding`, `SubAward` and `TreasuryAccountSymbol` were
321
+ stepped over and could raise `DetachedInstanceError` after the old client
322
+ was collected. The walk now tests for holding a client, and a recursive
323
+ reattach rebinds cached TAS levels in place: a 16-account agency costs no
324
+ requests after a reattach, where discarding the caches cost 17.
325
+
326
+ - `.all()` no longer spends an API request on a count it discards. `list()`
327
+ asks for a length hint, which called `count()` purely to size the list, so
328
+ every `.all()` paid one extra request, and on the five builders whose count
329
+ pages the result set (award funding, spending search, IDV child awards,
330
+ unscoped subaward search, and transactions with a date bound set), a second
331
+ complete pass: 250 date-bounded transactions cost 6 requests, now 3.
332
+ `len(query)` still counts, as must everything that consults the hint
333
+ (`list(query)`, `tuple(query)`, `sorted(query)`, `[*query]`); `set(query)`,
334
+ `sum(...)`, `in` and comprehensions were never affected. Prefer `.all()` or
335
+ a plain loop. The 13 docstring examples that taught `list(...)` now show
336
+ `.all()`.
337
+
338
+ - `if query:` no longer costs a full pagination. Truthiness fell through to
339
+ `__len__`, which on the five paging-count builders walked every page to
340
+ settle a single bit (250 date-bounded transactions: 3 requests, now 1). It
341
+ reads one row via `first()`; in-memory queries answer from their own count
342
+ instead.
343
+
344
+ - `FederalAccount.count` no longer fires an API request when the count is
345
+ already in the response; the fallback was an eagerly evaluated default
346
+ argument, so every access paid for a request whose result was discarded.
347
+
348
+
349
+ - `query.limit(0).first()` returned a row, contradicting `.all()` and
350
+ `len()`. It now respects the zero, which is also what makes `__bool__`
351
+ correct for a zero-limit query.
352
+
353
+ - The two date parsers now honor their documented contracts for unusable
354
+ values. The strict filter parser raised `TypeError` from inside `strptime`
355
+ for a non-string (most often a threaded-through `None`) rather than the
356
+ promised `ValidationError` naming the field; the lenient API-payload
357
+ converter could leak the same `TypeError` rather than answering `None`.
358
+ The error now quotes the documented `YYYY-MM-DD` form rather than a
359
+ strftime pattern, and shows the offending value with `repr`.
360
+
361
+ - Passing a `datetime` where a date filter documents accepting a `date` no
362
+ longer raises `TypeError` from the FY2008 floor check. `time_period()`,
363
+ `since()` and `until()` narrow a datetime to its date portion; the wire
364
+ payload was never affected.
365
+
366
+ - A `TransactionsSearch` with no award set raises `ValidationError` from
367
+ `count()` instead of requesting `/awards/count/transaction/None/`.
368
+
369
+ - `Award._load_agency_data` raises `ValidationError` rather than a bare
370
+ `ValueError` for an invalid `agency_type`; `ValidationError` subclasses
371
+ `ValueError`, so existing handlers are unaffected.
372
+
373
+ ### Removed
374
+
375
+ - The internal `usaspending.utils.formatter` module, split into
376
+ `usaspending.utils.dates`, `usaspending.utils.numbers` and
377
+ `usaspending.utils.textcase`. Every symbol it held was absent from
378
+ `__all__` and the README, so there is no compatibility shim. Only
379
+ `current_fiscal_year` gets a documented new home, in `usaspending.utils`,
380
+ as noted under Added.
381
+
382
+ - `Agency.__init__`'s third parameter, `subtier_data`. It was stored and
383
+ never read; drop the argument, and use `Award.funding_subtier_agency` or
384
+ `Award.awarding_subtier_agency` to reach subtier data.
385
+
386
+ - Module-level `usaspending.logging_config.get_logger`; the classmethod
387
+ `USASpendingLogger.get_logger` is the one entry point.
388
+
389
+ - `AgencyAwardSummary.find_by_id`, which only ever raised
390
+ `NotImplementedError` pointing at `get_awards_summary()`. The class is no
391
+ longer a `SingleResourceBase`; that base class remains, unchanged, under
392
+ the query classes that use it.
393
+
394
+ ## [0.7.3] - 2026-07-05
395
+
396
+ ### Added
397
+
398
+ - `AwardsSearch.object_classes(*codes)`: filter awards by federal object class codes (per Office of Management and Budget Circular A-11) on the `/search/spending_by_award/` and `/search/spending_by_award_count/` endpoints. Pass codes such as `"10"` or `"252"`, not names; multiple codes use OR logic.
399
+ - `SubAwardsSearch.object_classes()` raises `ValidationError`. The USASpending API accepts the `object_classes` filter for award searches only and returns HTTP 422 when it is combined with `spending_level=subawards`.
400
+ - `client.downloads.search()` emits a `UserWarning` when the query contains an `object_classes` filter, because the `/api/v2/download/search/` endpoint drops the key. The filter is still forwarded unmodified.
401
+ - Known upstream limitation as of 2026-07-05: the production API accepts `object_classes` but the rollout is incomplete. The search endpoint returns zero results for any object class, the count endpoint does not narrow, and the subawards rejection is not yet enforced server side. The filter should begin returning data once USAspending finishes deploying the feature and populating its award index.
402
+ - `tests/test_readme_examples.py`: integration tests that extract the README's Python code blocks and execute the runnable ones verbatim against the live API, so broken or rotted examples fail mechanically. A network-free guard in the default suite catches extraction drift. The live `client` fixture moved to `tests/conftest.py` and is now shared with `tests/test_integration.py`.
403
+
404
+ ### Documentation
405
+
406
+ - Corrected the `api-docs-links.md` subawards entry: `SubAwardsSearch` POSTs `/api/v2/search/spending_by_award/` with `subawards=true` and `spending_level=subawards` (inheriting `AwardsSearch`) rather than calling `/api/v2/subawards/`.
407
+ - Added missing `api-docs-links.md` entries for `/api/v2/idvs/awards/` (IDV child awards) and the TAS filter-tree endpoints (`/api/v2/references/filter_tree/tas/` and its `{toptier_code}/` and `{toptier_code}/{federal_account}/` variants).
408
+ - An upstream API compatibility review of USASpending API releases from April through June 2026 found no breaking changes for this library. The 0.7.2 release already handles the `spending_by_award` Pydantic migration (HTTP 422 validation errors), the `program_activities` filter rename, and the new F001 through F010 assistance type codes.
409
+ - Revalidated all README examples against live USAspending data. Fixed two broken chains (`location.full_address` is actually `location.formatted_address`; a subaward's place of performance is on the subaward, not its recipient), refreshed stale sample outputs, and added examples for the `client.spending` category rollups and the list-style `len(query)` interface.
410
+
411
+ ## [0.7.2] - 2026-06-05
412
+
413
+ ### Added
414
+
415
+ - `client.downloads.search(query, ...)` backed by the new `/api/v2/download/search/` endpoint, which bundles award, transaction, and subaward data into a single download built from an awards search query. Supports `file_format`, `spending_level`, `columns`, and `limit`.
416
+ - `QueryBuilder.to_filters_payload()`: public accessor returning the assembled API `filters` object for a query without executing a search.
417
+
418
+ ### Changed
419
+
420
+ - HTTP 422 (Unprocessable Entity) validation responses are now surfaced as `APIError` with the response detail instead of a generic `HTTPError`. This aligns with the USASpending API's move to Pydantic request validation on `/search/spending_by_award/`.
421
+
422
+ ### Deprecated
423
+
424
+ - `program_activity()` is deprecated and now forwards to `program_activities()`, emitting a `DeprecationWarning`. The USASpending API removed the `program_activity` filter from `/search/spending_by_award/` in favor of `program_activities`. Existing calls continue to work; integer codes are forwarded as string `code` values.
425
+
426
+ ## [0.7.1] - 2026-04-16
427
+
428
+ ### Added
429
+
430
+ - Public export of `DetachedInstanceError` so callers can `from usaspending import DetachedInstanceError` as documented in the README.
431
+ - `config.allowed_download_hosts`: an allow-list of hostnames the library will fetch binary downloads from. Defaults to `{"api.usaspending.gov", "files.usaspending.gov"}`.
432
+ - `config.validate()` now rejects malformed `base_url`, CR/LF in `user_agent`, and empty `cache_dir`.
433
+
434
+ ### Changed
435
+
436
+ - `ValidationError` now inherits from both `USASpendingError` and the built-in `ValueError`. Callers that previously caught `ValueError` for invalid parameters continue to work unchanged; new code can catch the library-specific `ValidationError`.
437
+ - `RateLimiter` now raises `ValidationError` (rather than a bare `ValueError`) for invalid `max_calls`/`period` arguments. Non-breaking due to the dual inheritance above.
438
+ - `RecipientsResource.find_by_recipient_id` is now annotated as non-Optional. This matches its long-standing behavior of raising on miss via the direct-ID lookup path; runtime behavior is unchanged. `find_by_duns` and `find_by_uei` remain Optional because they use the search-delegation path.
439
+ - `Agency.get_obligations` return annotation corrected from `float | None` to `Decimal | None` to match actual runtime type.
440
+ - `Agency.total_obligations` now applies `Decimal` coercion to stored values to match its declared return type.
441
+ - `TransactionsSearch._award_id` and `FundingSearch._award_id` annotated as `str | None` rather than `str` (they are always `None` until set).
442
+ - `time_period()` now rejects date ranges where `end_date` is earlier than `start_date`.
443
+
444
+ ### Fixed
445
+
446
+ - `to_date()` now extracts the `date` portion from `datetime` inputs instead of passing them through unchanged.
447
+ - `IDVChildAwardsSearch._clone()` no longer silently drops `_filter_objects`, `_max_pages`, `_order_by`, and `_order_direction` when chaining.
448
+ - `SubAwardsSearch._clone()` now delegates to `super()._clone()` so base-class state changes propagate.
449
+ - `AwardAccountsQuery._clone()` no longer copies a stale `_cached_count` across filter changes.
450
+ - `Agency.latest_action_date` no longer crashes when the award-summary endpoint returns no data.
451
+ - `round_to_millions()` cleaned up redundant disjunct in threshold check.
452
+ - Removed unused `DEFAULT_KEEP_UPPERCASE` set, which also contained a latent missing-comma bug that concatenated two entries.
453
+ - Fixed malformed `Raises:` block in `AwardResource.find_by_generated_id` docstring.
454
+ - `Agency.id` was missing its return-type annotation.
455
+
456
+ ### Security
457
+
458
+ - Binary downloads (`_download_binary_file`) now require `https` and a hostname on `config.allowed_download_hosts`, mitigating SSRF if API responses are tampered with.
459
+ - File-backed cache directory is now created with `0700` and tightened to `0700` if it already exists with looser bits. Protects the pickle-backed cache on shared hosts.
460
+ - ZIP extraction creates the extract directory with `0700`, detects absolute paths with stdlib `posixpath.isabs` / `ntpath.isabs` (covers Windows drive letters and UNC prefixes), and uses `normpath`-based prefix comparison for traversal detection.
461
+
462
+ ### Removed
463
+
464
+ - Dead code: `get_past_fiscal_years`, `custom_titlecase_callback`, `parse_agency_type`, `parse_agency_tier`, `QueryBuilder._fetch_page`, empty `AwardsSearch.__init__`, redundant `AwardsSearch._clone` override, unreachable `try/except` in `_derived_award_identifier`.
465
+
466
+ ## [0.7.0] - 2026-04-12
467
+
468
+ Previous release. See git history for details.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: usaspending-orm
3
- Version: 0.7.3
3
+ Version: 0.8.0
4
4
  Summary: A Python ORM wrapper for the USAspending.gov API with intuitive queries, automatic pagination, and smart caching
5
5
  Project-URL: Homepage, https://github.com/planetary-society/usaspending-orm
6
6
  Project-URL: Repository, https://github.com/planetary-society/usaspending-orm
@@ -17,6 +17,8 @@ Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
20
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
23
  Requires-Python: >=3.9
22
24
  Requires-Dist: cachier<5.0.0,>=4.1.0
@@ -106,7 +108,7 @@ awards_query = client.awards.search() \
106
108
  ```
107
109
 
108
110
  This returns a query object that can be further refined or executed to return results.
109
- The methods `.all()`, `.first()`, `.count()` will trigger a query to the API, as will iterating over the query object. Query objects also support the standard Python list interface, so `len(awards_query)` fires the corresponding count endpoint:
111
+ The methods `.all()`, `.first()`, `.count()` will trigger a query to the API, as will iterating over the query object and testing it with `if query:` (which reads a single row). Query objects also support the standard Python list interface, so `len(awards_query)` fires the corresponding count endpoint:
110
112
 
111
113
  ```python
112
114
  len(client.awards.search()
@@ -239,7 +241,8 @@ with USASpendingClient() as client:
239
241
  awards = client.awards.search().agency("National Aeronautics and Space Administration").all()
240
242
  for award in awards:
241
243
  # Access lazy-loaded properties inside the context
242
- print(f"{award.recipient.name}: ${award.total_obligation:,.2f}")
244
+ # Money getters return None when the award reports no figure
245
+ print(f"{award.recipient.name}: ${award.total_obligation or 0:,.2f}")
243
246
  print(f"Subawards: {award.subaward_count}")
244
247
  # Session automatically closed here
245
248
 
@@ -270,14 +273,18 @@ with USASpendingClient() as client:
270
273
 
271
274
  # Reattach to a new session to access related properties
272
275
  with USASpendingClient() as new_client:
273
- award.reattach(new_client)
274
- print(f"Subawards: {award.subawards.count()}") # Works!
275
-
276
- # Recursive reattach for nested objects
276
+ # recursive=True is normally what you want: it also rebinds models already
277
+ # loaded from this one, such as award.recipient
277
278
  award.reattach(new_client, recursive=True)
278
- print(f"Recipient: {award.recipient.name}") # Recipient also reattached
279
+ print(f"Subawards: {award.subawards.count()}")
280
+ print(f"Recipient: {award.recipient.name}")
279
281
  ```
280
282
 
283
+ Without `recursive=True` only the award itself is rebound. That is enough for
284
+ properties it loads afresh, but a nested model already loaded, such as
285
+ `award.recipient`, keeps pointing at the old session and raises
286
+ `DetachedInstanceError` once that session is gone.
287
+
281
288
  ### Performance Considerations
282
289
 
283
290
  #### Lazy Loading and N+1 Queries
@@ -421,7 +428,7 @@ usaspending_config.configure(
421
428
  # Set HTTP request parameters (default: max_retries=3, timeout=30)
422
429
  # Set number of retries for failed requests (default: 3)
423
430
  max_retries=5,
424
- # Set delay between retries in seconds (default: 1.0)
431
+ # Set delay between retries in seconds (default: 10.0)
425
432
  retry_delay=10.0,
426
433
  # Set exponential backoff factor for retries (default: 2.0)
427
434
  retry_backoff=2.0,
@@ -79,7 +79,7 @@ awards_query = client.awards.search() \
79
79
  ```
80
80
 
81
81
  This returns a query object that can be further refined or executed to return results.
82
- The methods `.all()`, `.first()`, `.count()` will trigger a query to the API, as will iterating over the query object. Query objects also support the standard Python list interface, so `len(awards_query)` fires the corresponding count endpoint:
82
+ The methods `.all()`, `.first()`, `.count()` will trigger a query to the API, as will iterating over the query object and testing it with `if query:` (which reads a single row). Query objects also support the standard Python list interface, so `len(awards_query)` fires the corresponding count endpoint:
83
83
 
84
84
  ```python
85
85
  len(client.awards.search()
@@ -212,7 +212,8 @@ with USASpendingClient() as client:
212
212
  awards = client.awards.search().agency("National Aeronautics and Space Administration").all()
213
213
  for award in awards:
214
214
  # Access lazy-loaded properties inside the context
215
- print(f"{award.recipient.name}: ${award.total_obligation:,.2f}")
215
+ # Money getters return None when the award reports no figure
216
+ print(f"{award.recipient.name}: ${award.total_obligation or 0:,.2f}")
216
217
  print(f"Subawards: {award.subaward_count}")
217
218
  # Session automatically closed here
218
219
 
@@ -243,14 +244,18 @@ with USASpendingClient() as client:
243
244
 
244
245
  # Reattach to a new session to access related properties
245
246
  with USASpendingClient() as new_client:
246
- award.reattach(new_client)
247
- print(f"Subawards: {award.subawards.count()}") # Works!
248
-
249
- # Recursive reattach for nested objects
247
+ # recursive=True is normally what you want: it also rebinds models already
248
+ # loaded from this one, such as award.recipient
250
249
  award.reattach(new_client, recursive=True)
251
- print(f"Recipient: {award.recipient.name}") # Recipient also reattached
250
+ print(f"Subawards: {award.subawards.count()}")
251
+ print(f"Recipient: {award.recipient.name}")
252
252
  ```
253
253
 
254
+ Without `recursive=True` only the award itself is rebound. That is enough for
255
+ properties it loads afresh, but a nested model already loaded, such as
256
+ `award.recipient`, keeps pointing at the old session and raises
257
+ `DetachedInstanceError` once that session is gone.
258
+
254
259
  ### Performance Considerations
255
260
 
256
261
  #### Lazy Loading and N+1 Queries
@@ -394,7 +399,7 @@ usaspending_config.configure(
394
399
  # Set HTTP request parameters (default: max_retries=3, timeout=30)
395
400
  # Set number of retries for failed requests (default: 3)
396
401
  max_retries=5,
397
- # Set delay between retries in seconds (default: 1.0)
402
+ # Set delay between retries in seconds (default: 10.0)
398
403
  retry_delay=10.0,
399
404
  # Set exponential backoff factor for retries (default: 2.0)
400
405
  retry_backoff=2.0,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "usaspending-orm"
3
- version = "0.7.3"
3
+ version = "0.8.0"
4
4
  description = "A Python ORM wrapper for the USAspending.gov API with intuitive queries, automatic pagination, and smart caching"
5
5
  authors = [
6
6
  {name = "Casey Dreier", email = "casey.dreier@planetary.org"}
@@ -21,6 +21,8 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.10",
22
22
  "Programming Language :: Python :: 3.11",
23
23
  "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
24
26
  "Topic :: Software Development :: Libraries :: Python Modules",
25
27
  ]
26
28
  dependencies = [
@@ -39,10 +41,25 @@ Documentation = "https://github.com/planetary-society/usaspending-orm/blob/main/
39
41
  [dependency-groups]
40
42
  dev = [
41
43
  "pytest>=8.4.1,<9",
44
+ "pytest-cov>=5,<8",
42
45
  "requests-mock>=1.12.1,<2",
43
46
  "ruff>=0.12.7,<1",
44
47
  ]
45
48
 
49
+ [tool.hatch.build.targets.sdist]
50
+ # An allowlist so the sdist does not depend on working-tree cleanliness:
51
+ # hatchling's fallback includes any file git does not ignore, untracked ones
52
+ # included.
53
+ include = [
54
+ "src/usaspending",
55
+ "tests",
56
+ "CHANGELOG.md",
57
+ "README.md",
58
+ "LICENSE",
59
+ "api-docs-links.md",
60
+ "uv.lock",
61
+ ]
62
+
46
63
  [build-system]
47
64
  requires = ["hatchling"]
48
65
  build-backend = "hatchling.build"
@@ -85,7 +85,7 @@ class USASpendingClient:
85
85
  ... .limit(10)
86
86
  ... )
87
87
  >>> for award in awards:
88
- ... print(f"{award.recipient_name}: ${award.amount:,.2f}")
88
+ ... print(f"{award.recipient.name}: ${award.total_obligation or 0:,.2f}")
89
89
  """
90
90
 
91
91
  _make_cached_request: Any
@@ -261,7 +261,7 @@ class USASpendingClient:
261
261
  >>> # Get accounts for an award
262
262
  >>> award = client.awards.find_by_generated_id("CONT_AWD_123...")
263
263
  >>> for account in award.accounts:
264
- ... print(f"{account.code}: ${account.obligated_amount:,.2f}")
264
+ ... print(f"{account.code}: ${account.obligated_amount or 0:,.2f}")
265
265
  >>>
266
266
  >>> # Or access directly
267
267
  >>> accounts = client.award_accounts.award_id("CONT_AWD_123...")