tango-python 1.17.0__tar.gz → 1.18.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 (209) hide show
  1. {tango_python-1.17.0 → tango_python-1.18.0}/CHANGELOG.md +12 -0
  2. {tango_python-1.17.0 → tango_python-1.18.0}/PKG-INFO +1 -1
  3. {tango_python-1.17.0 → tango_python-1.18.0}/docs/API_REFERENCE.md +17 -1
  4. {tango_python-1.17.0 → tango_python-1.18.0}/pyproject.toml +1 -1
  5. {tango_python-1.17.0 → tango_python-1.18.0}/tango/__init__.py +1 -1
  6. {tango_python-1.17.0 → tango_python-1.18.0}/tango/client.py +1 -1
  7. {tango_python-1.17.0 → tango_python-1.18.0}/tango/models.py +11 -0
  8. {tango_python-1.17.0 → tango_python-1.18.0}/tango/shapes/explicit_schemas.py +35 -0
  9. {tango_python-1.17.0 → tango_python-1.18.0}/tango/shapes/generated_overlay.py +18 -0
  10. tango_python-1.18.0/tests/test_obligations_by_fiscal_year.py +88 -0
  11. {tango_python-1.17.0 → tango_python-1.18.0}/.gitignore +0 -0
  12. {tango_python-1.17.0 → tango_python-1.18.0}/LICENSE +0 -0
  13. {tango_python-1.17.0 → tango_python-1.18.0}/README.md +0 -0
  14. {tango_python-1.17.0 → tango_python-1.18.0}/docs/CLIENT.md +0 -0
  15. {tango_python-1.17.0 → tango_python-1.18.0}/docs/DYNAMIC_MODELS.md +0 -0
  16. {tango_python-1.17.0 → tango_python-1.18.0}/docs/ERRORS.md +0 -0
  17. {tango_python-1.17.0 → tango_python-1.18.0}/docs/PAGINATION.md +0 -0
  18. {tango_python-1.17.0 → tango_python-1.18.0}/docs/SHAPES.md +0 -0
  19. {tango_python-1.17.0 → tango_python-1.18.0}/docs/WEBHOOKS.md +0 -0
  20. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/README.md +0 -0
  21. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/check_filter_shape_conformance.py +0 -0
  22. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/check_shape_coverage.py +0 -0
  23. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/fetch_api_schema.py +0 -0
  24. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/generate_schemas_from_api.py +0 -0
  25. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/generate_shape_overlay.py +0 -0
  26. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/pr_review.py +0 -0
  27. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/probe_shape_types.py +0 -0
  28. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/refresh_contract.py +0 -0
  29. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/smoke_api_parity.py +0 -0
  30. {tango_python-1.17.0 → tango_python-1.18.0}/scripts/test_production.py +0 -0
  31. {tango_python-1.17.0 → tango_python-1.18.0}/tango/exceptions.py +0 -0
  32. {tango_python-1.17.0 → tango_python-1.18.0}/tango/shapes/__init__.py +0 -0
  33. {tango_python-1.17.0 → tango_python-1.18.0}/tango/shapes/factory.py +0 -0
  34. {tango_python-1.17.0 → tango_python-1.18.0}/tango/shapes/generator.py +0 -0
  35. {tango_python-1.17.0 → tango_python-1.18.0}/tango/shapes/models.py +0 -0
  36. {tango_python-1.17.0 → tango_python-1.18.0}/tango/shapes/parser.py +0 -0
  37. {tango_python-1.17.0 → tango_python-1.18.0}/tango/shapes/schema.py +0 -0
  38. {tango_python-1.17.0 → tango_python-1.18.0}/tango/shapes/types.py +0 -0
  39. {tango_python-1.17.0 → tango_python-1.18.0}/tango/webhooks/__init__.py +0 -0
  40. {tango_python-1.17.0 → tango_python-1.18.0}/tango/webhooks/cli.py +0 -0
  41. {tango_python-1.17.0 → tango_python-1.18.0}/tango/webhooks/receiver.py +0 -0
  42. {tango_python-1.17.0 → tango_python-1.18.0}/tango/webhooks/signing.py +0 -0
  43. {tango_python-1.17.0 → tango_python-1.18.0}/tango/webhooks/simulate.py +0 -0
  44. {tango_python-1.17.0 → tango_python-1.18.0}/tests/__init__.py +0 -0
  45. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
  46. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
  47. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
  48. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
  49. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
  50. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
  51. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
  52. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
  53. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
  54. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
  55. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
  56. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
  57. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
  58. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
  59. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
  60. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[custom-key,piid,recipient(display_name),total_contract_value,award_date] +0 -0
  61. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
  62. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[detailed-key,piid,award_date,description,total_contract_value,obligated,fiscal_year,set_aside,recipient(display_name,uei),awarding_office(-),place_of_performa...ce114a3c47e2037aaa3c15d00b7031bd +0 -0
  63. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
  64. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
  65. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
  66. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
  67. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
  68. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
  69. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
  70. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
  71. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
  72. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
  73. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
  74. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
  75. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
  76. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
  77. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
  78. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
  79. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
  80. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
  81. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
  82. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
  83. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
  84. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
  85. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
  86. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_budget_flows +0 -0
  87. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
  88. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
  89. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
  90. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[comprehensive-uei,legal_business_name,dba_name,cage_code,business_types,primary_naics,naics_codes,psc_codes,email_address,entity_url,description,capabilities,ke...1603a7d52e211cf2b3bc7d32080238aa +0 -0
  91. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[custom-uei,legal_business_name,cage_code] +0 -0
  92. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
  93. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
  94. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
  95. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[custom-id,title,anticipated_award_date] +0 -0
  96. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
  97. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[detailed-id,source_system,external_id,title,description,anticipated_award_date,fiscal_year,naics_code,status,is_active] +0 -0
  98. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[minimal-id,title,anticipated_award_date,fiscal_year,naics_code,status] +0 -0
  99. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
  100. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
  101. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[custom-grant_id,title,opportunity_number] +0 -0
  102. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
  103. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[detailed-grant_id,opportunity_number,title,status(-),agency_code,description,last_updated,cfda_numbers(number,title),applicant_types(-),funding_categories(-)] +0 -0
  104. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[minimal-grant_id,opportunity_number,title,status(-),agency_code] +0 -0
  105. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
  106. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
  107. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
  108. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
  109. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
  110. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_code +0 -0
  111. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_name_text +0 -0
  112. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating +0 -0
  113. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating_max +0 -0
  114. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_performance_risk +0 -0
  115. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_type_of_investment +0 -0
  116. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_updated_time_range +0 -0
  117. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_funding_and_cio_evaluation_expansions +0 -0
  118. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_get_itdashboard_investment_by_uii +0 -0
  119. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_itdashboard_pagination +0 -0
  120. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_search +0 -0
  121. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[custom-uii,agency_name,investment_title,updated_time] +0 -0
  122. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[default-None] +0 -0
  123. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[minimal-uii,agency_name,bureau_name,investment_title,type_of_investment,part_of_it_portfolio,updated_time,url] +0 -0
  124. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
  125. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[custom-notice_id,title,solicitation_number] +0 -0
  126. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
  127. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[detailed-notice_id,title,description,solicitation_number,posted_date,naics_code,set_aside,office(-),place_of_performance(-)] +0 -0
  128. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
  129. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
  130. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
  131. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
  132. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
  133. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
  134. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
  135. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
  136. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
  137. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
  138. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[custom-opportunity_id,title,solicitation_number] +0 -0
  139. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
  140. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[detailed-opportunity_id,title,description,solicitation_number,response_deadline,first_notice_date,last_notice_date,active,naics_code,psc_code,set_asid...23b6b4502ddd665b7184afcff6c6d8d9 +0 -0
  141. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
  142. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
  143. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
  144. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
  145. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
  146. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
  147. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[custom-case_id,title,source_system,outcome] +0 -0
  148. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
  149. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[minimal-case_id,case_number,title,source_system,outcome,filed_date] +0 -0
  150. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[with_dockets-case_id,case_number,title,outcome,filed_date,dockets(docket_number,filed_date,outcome)] +0 -0
  151. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
  152. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
  153. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
  154. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
  155. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[ultra_minimal-key,piid,recipient(display_name),total_contract_value] +0 -0
  156. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
  157. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
  158. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[detailed-notice_id,title,description,solicitation_number,posted_date,naics_code,set_aside,office(-),place_of_performance(-)] +0 -0
  159. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
  160. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[detailed-opportunity_id,title,description,solicitation_number,response_deadline,first_notice_date,last_notice_date,active,naics_code,psc_code,set_aside,sam_url,office(-),place_of_performance(-)] +0 -0
  161. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
  162. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
  163. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_with_metrics_expansion +0 -0
  164. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
  165. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_orders_uses_default_shape +0 -0
  166. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
  167. {tango_python-1.17.0 → tango_python-1.18.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_with_ordering +0 -0
  168. {tango_python-1.17.0 → tango_python-1.18.0}/tests/conftest.py +0 -0
  169. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/README.md +0 -0
  170. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/__init__.py +0 -0
  171. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/conftest.py +0 -0
  172. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_agencies_integration.py +0 -0
  173. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_contracts_integration.py +0 -0
  174. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_edge_cases_integration.py +0 -0
  175. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_entities_integration.py +0 -0
  176. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_forecasts_integration.py +0 -0
  177. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_grants_integration.py +0 -0
  178. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_itdashboard_integration.py +0 -0
  179. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_naics_integration.py +0 -0
  180. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_notices_integration.py +0 -0
  181. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_offices_integration.py +0 -0
  182. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_opportunities_integration.py +0 -0
  183. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_organizations_integration.py +0 -0
  184. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_otas_otidvs_integration.py +0 -0
  185. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_protests_integration.py +0 -0
  186. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_reference_data_integration.py +0 -0
  187. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_subawards_integration.py +0 -0
  188. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/test_vehicles_idvs_integration.py +0 -0
  189. {tango_python-1.17.0 → tango_python-1.18.0}/tests/integration/validation.py +0 -0
  190. {tango_python-1.17.0 → tango_python-1.18.0}/tests/production/__init__.py +0 -0
  191. {tango_python-1.17.0 → tango_python-1.18.0}/tests/production/conftest.py +0 -0
  192. {tango_python-1.17.0 → tango_python-1.18.0}/tests/production/test_production_smoke.py +0 -0
  193. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_api_parity.py +0 -0
  194. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_client.py +0 -0
  195. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_contract_appeals.py +0 -0
  196. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_dibbs_exclusions_sbir.py +0 -0
  197. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_ebuy.py +0 -0
  198. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_federal_register.py +0 -0
  199. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_models.py +0 -0
  200. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_opportunity_attachments.py +0 -0
  201. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_opportunity_awards.py +0 -0
  202. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_organization_scope.py +0 -0
  203. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_shape_coverage_gate.py +0 -0
  204. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_shapes.py +0 -0
  205. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_sled.py +0 -0
  206. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_webhooks_cli.py +0 -0
  207. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_webhooks_receiver.py +0 -0
  208. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_webhooks_signing.py +0 -0
  209. {tango_python-1.17.0 → tango_python-1.18.0}/tests/test_webhooks_simulate.py +0 -0
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.18.0] - 2026-10-19
11
+
12
+ ### Added
13
+
14
+ - **`obligations_by_fiscal_year(fiscal_year, obligated)` shape expand on contracts, IDVs, OTAs and OTIDVs** (Tango API 5.11.0). Returns the award's obligations per federal fiscal year, each rounded to cents, with `fiscal_year` parsed as `int` and `obligated` as `Decimal`. It is always `None` on IDVs and OTIDVs, whose transactions report a running lifetime total.
15
+ - **`attachments(extraction_status)` shape field on opportunities and notices** (Tango API 5.11.0). Reports `pending`, `extracting`, `completed`, `failed` or `unsupported` for each attachment, so you can tell why `extracted_text` is missing.
16
+
17
+ ### Changed
18
+
19
+ - Re-vendored `contracts/filter_shape_contract.json` (Tango API 5.11.0) and regenerated the shape overlay.
20
+ - Documented that `recipient_uei` on `list_contracts()` (and `uei` on the IDV, OTA and OTIDV lists) accepts several UEIs joined by `|` (Tango API 5.11.0); the SDK already passed the value through unchanged.
21
+
10
22
  ## [1.17.0] - 2026-10-16
11
23
 
12
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tango-python
3
- Version: 1.17.0
3
+ Version: 1.18.0
4
4
  Summary: Python SDK for the Tango API
5
5
  Project-URL: Homepage, https://github.com/makegov/tango-python
6
6
  Project-URL: Documentation, https://docs.makegov.com/tango-python
@@ -285,7 +285,7 @@ contracts = client.list_contracts(
285
285
  - `awarding_agency` - Agency code (e.g., "4700" for GSA)
286
286
  - `funding_agency` - Funding agency code
287
287
  - `recipient_name` - Vendor/recipient name (mapped to 'recipient' API param)
288
- - `recipient_uei` - Vendor UEI (mapped to 'uei' API param)
288
+ - `recipient_uei` - Vendor UEI (mapped to 'uei' API param). Several UEIs joined by `|` match awards to any of them, up to 500 (Tango API 5.11.0).
289
289
 
290
290
  **Classification:**
291
291
  - `naics_code` - NAICS industry code (mapped to 'naics' API param)
@@ -381,6 +381,22 @@ contracts = client.list_contracts(
381
381
  - `psc` - Product/service code (nested)
382
382
  - `place_of_performance` - Location (nested)
383
383
 
384
+ **Obligations by fiscal year — `obligations_by_fiscal_year(fiscal_year, obligated)`** (Tango API 5.11.0):
385
+
386
+ ```python
387
+ contracts = client.list_contracts(
388
+ shape="key,piid,obligated,obligations_by_fiscal_year(fiscal_year,obligated)",
389
+ piid="SOME_PIID",
390
+ )
391
+ for year in contracts.results[0]["obligations_by_fiscal_year"] or []:
392
+ print(year["fiscal_year"], year["obligated"]) # int, Decimal
393
+ ```
394
+
395
+ - Sums the award's transactions per federal fiscal year (October 1 starts the next fiscal year), oldest year first, each rounded to cents. A de-obligation nets against its own year, so a year can be zero or negative.
396
+ - Returned only when named: no default shape includes it.
397
+ - The same expand works on `list_otas()`, `list_idvs()`, `list_otidvs()` and the `get_*` methods. On IDVs and OTIDVs it is always `None`, because their transactions report a running lifetime total rather than each action's obligation.
398
+ - `None` on a contract or OTA with no dated transactions.
399
+
384
400
  ---
385
401
 
386
402
  ## OTAs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tango-python"
7
- version = "1.17.0"
7
+ version = "1.18.0"
8
8
  description = "Python SDK for the Tango API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -62,7 +62,7 @@ from .webhooks import (
62
62
  )
63
63
  from .webhooks.receiver import Delivery, WebhookReceiver
64
64
 
65
- __version__ = "1.17.0"
65
+ __version__ = "1.18.0"
66
66
  __all__ = [
67
67
  "TangoClient",
68
68
  "TangoAPIError",
@@ -695,7 +695,7 @@ class TangoClient:
695
695
  naics_code: NAICS code (mapped to 'naics' API param)
696
696
  psc_code: PSC code (mapped to 'psc' API param)
697
697
  recipient_name: Vendor/recipient name (mapped to 'recipient' API param)
698
- recipient_uei: Vendor UEI (mapped to 'uei' API param)
698
+ recipient_uei: Vendor UEI (mapped to 'uei' API param); join several with `|` for OR, up to 500
699
699
  set_aside_type: Set-aside type (mapped to 'set_aside' API param)
700
700
  sort: Field to sort by (combined with 'order' to produce 'ordering')
701
701
  order: Sort order ('asc' or 'desc', default 'asc')
@@ -281,6 +281,17 @@ class AwardTransaction:
281
281
  obligated: Decimal | None = None
282
282
 
283
283
 
284
+ @dataclass
285
+ class FiscalYearObligation:
286
+ """Schema definition for FiscalYearObligation (not used for instances).
287
+
288
+ One entry of an award's ``obligations_by_fiscal_year``: the obligations dated in one federal fiscal year, rounded to cents.
289
+ """
290
+
291
+ fiscal_year: int
292
+ obligated: Decimal
293
+
294
+
284
295
  @dataclass
285
296
  class OrganizationOfficePayload:
286
297
  """Schema definition for OrganizationOfficePayload (not used for instances).
@@ -197,6 +197,12 @@ TRANSACTION_SCHEMA: dict[str, FieldSchema] = {
197
197
  }
198
198
 
199
199
 
200
+ FISCAL_YEAR_OBLIGATION_SCHEMA: dict[str, FieldSchema] = {
201
+ "fiscal_year": FieldSchema(name="fiscal_year", type=int, is_optional=False, is_list=False),
202
+ "obligated": FieldSchema(name="obligated", type=Decimal, is_optional=False, is_list=False),
203
+ }
204
+
205
+
200
206
  DEPARTMENT_SCHEMA: dict[str, FieldSchema] = {
201
207
  "abbreviation": FieldSchema(name="abbreviation", type=str, is_optional=True, is_list=False),
202
208
  "code": FieldSchema(name="code", type=int, is_optional=True, is_list=False),
@@ -432,6 +438,13 @@ CONTRACT_SCHEMA: dict[str, FieldSchema] = {
432
438
  is_list=False,
433
439
  nested_model="Vehicle",
434
440
  ),
441
+ "obligations_by_fiscal_year": FieldSchema(
442
+ name="obligations_by_fiscal_year",
443
+ type=dict,
444
+ is_optional=True,
445
+ is_list=True,
446
+ nested_model="FiscalYearObligation",
447
+ ),
435
448
  }
436
449
 
437
450
 
@@ -1290,6 +1303,13 @@ IDV_SCHEMA: dict[str, FieldSchema] = {
1290
1303
  "idv_contracts_value": FieldSchema(
1291
1304
  name="idv_contracts_value", type=Decimal, is_optional=True, is_list=False
1292
1305
  ),
1306
+ "obligations_by_fiscal_year": FieldSchema(
1307
+ name="obligations_by_fiscal_year",
1308
+ type=dict,
1309
+ is_optional=True,
1310
+ is_list=True,
1311
+ nested_model="FiscalYearObligation",
1312
+ ),
1293
1313
  }
1294
1314
 
1295
1315
 
@@ -1439,6 +1459,13 @@ OTA_SCHEMA: dict[str, FieldSchema] = {
1439
1459
  is_list=False,
1440
1460
  nested_model="RecipientProfile",
1441
1461
  ),
1462
+ "obligations_by_fiscal_year": FieldSchema(
1463
+ name="obligations_by_fiscal_year",
1464
+ type=dict,
1465
+ is_optional=True,
1466
+ is_list=True,
1467
+ nested_model="FiscalYearObligation",
1468
+ ),
1442
1469
  }
1443
1470
 
1444
1471
  # OTIDV (Other Transaction IDV) - IDV-like
@@ -1459,6 +1486,13 @@ OTIDV_SCHEMA: dict[str, FieldSchema] = {
1459
1486
  is_list=False,
1460
1487
  nested_model="RecipientProfile",
1461
1488
  ),
1489
+ "obligations_by_fiscal_year": FieldSchema(
1490
+ name="obligations_by_fiscal_year",
1491
+ type=dict,
1492
+ is_optional=True,
1493
+ is_list=True,
1494
+ nested_model="FiscalYearObligation",
1495
+ ),
1462
1496
  }
1463
1497
 
1464
1498
  # Subaward (prime/sub awards)
@@ -1707,6 +1741,7 @@ EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
1707
1741
  "LegislativeMandates": LEGISLATIVE_MANDATES_SCHEMA,
1708
1742
  "SubawardsSummary": SUBAWARDS_SUMMARY_SCHEMA,
1709
1743
  "Transaction": TRANSACTION_SCHEMA,
1744
+ "FiscalYearObligation": FISCAL_YEAR_OBLIGATION_SCHEMA,
1710
1745
  "Department": DEPARTMENT_SCHEMA,
1711
1746
  "Contact": CONTACT_SCHEMA,
1712
1747
  "RecipientProfile": RECIPIENT_PROFILE_SCHEMA,
@@ -85,6 +85,9 @@ ATTACHMENTS_SCHEMA: dict[str, FieldSchema] = {
85
85
  "doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False),
86
86
  "doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False),
87
87
  "extracted_text": FieldSchema(name="extracted_text", type=str, is_optional=True, is_list=False),
88
+ "extraction_status": FieldSchema(
89
+ name="extraction_status", type=str, is_optional=True, is_list=False
90
+ ),
88
91
  "file_size": FieldSchema(name="file_size", type=int, is_optional=True, is_list=False),
89
92
  "mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False),
90
93
  "name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
@@ -99,6 +102,9 @@ ATTACHMENTS2_SCHEMA: dict[str, FieldSchema] = {
99
102
  "doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False),
100
103
  "doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False),
101
104
  "extracted_text": FieldSchema(name="extracted_text", type=str, is_optional=True, is_list=False),
105
+ "extraction_status": FieldSchema(
106
+ name="extraction_status", type=str, is_optional=True, is_list=False
107
+ ),
102
108
  "file_size": FieldSchema(name="file_size", type=int, is_optional=True, is_list=False),
103
109
  "mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False),
104
110
  "name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
@@ -135,6 +141,9 @@ ATTACHMENTS3_SCHEMA: dict[str, FieldSchema] = {
135
141
 
136
142
  ATTACHMENTS4_SCHEMA: dict[str, FieldSchema] = {
137
143
  "attachment_id": FieldSchema(name="attachment_id", type=str, is_optional=True, is_list=False),
144
+ "extraction_status": FieldSchema(
145
+ name="extraction_status", type=str, is_optional=True, is_list=False
146
+ ),
138
147
  "file_size": FieldSchema(name="file_size", type=str, is_optional=True, is_list=False),
139
148
  "mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False),
140
149
  "name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
@@ -149,6 +158,9 @@ ATTACHMENTS5_SCHEMA: dict[str, FieldSchema] = {
149
158
  "doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False),
150
159
  "doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False),
151
160
  "extracted_text": FieldSchema(name="extracted_text", type=str, is_optional=True, is_list=False),
161
+ "extraction_status": FieldSchema(
162
+ name="extraction_status", type=str, is_optional=True, is_list=False
163
+ ),
152
164
  "file_size": FieldSchema(name="file_size", type=str, is_optional=True, is_list=False),
153
165
  "mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False),
154
166
  "name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
@@ -758,6 +770,11 @@ NOTICE_TYPE_SCHEMA: dict[str, FieldSchema] = {
758
770
  "type": FieldSchema(name="type", type=str, is_optional=True, is_list=False),
759
771
  }
760
772
 
773
+ OBLIGATIONS_BY_FISCAL_YEAR_SCHEMA: dict[str, FieldSchema] = {
774
+ "fiscal_year": FieldSchema(name="fiscal_year", type=str, is_optional=True, is_list=False),
775
+ "obligated": FieldSchema(name="obligated", type=str, is_optional=True, is_list=False),
776
+ }
777
+
761
778
  OFFICERS_SCHEMA: dict[str, FieldSchema] = {
762
779
  "highly_compensated_officer_1_amount": FieldSchema(
763
780
  name="highly_compensated_officer_1_amount", type=Decimal, is_optional=True, is_list=False
@@ -1161,6 +1178,7 @@ GENERATED_NESTED: dict[str, dict[str, FieldSchema]] = {
1161
1178
  "NoticeHistory": NOTICE_HISTORY_SCHEMA,
1162
1179
  "NoticeHistory2": NOTICE_HISTORY2_SCHEMA,
1163
1180
  "NoticeType": NOTICE_TYPE_SCHEMA,
1181
+ "ObligationsByFiscalYear": OBLIGATIONS_BY_FISCAL_YEAR_SCHEMA,
1164
1182
  "Officers": OFFICERS_SCHEMA,
1165
1183
  "Opportunity": OPPORTUNITY_SCHEMA,
1166
1184
  "Opportunity2": OPPORTUNITY2_SCHEMA,
@@ -0,0 +1,88 @@
1
+ """The `obligations_by_fiscal_year(...)` expand on contracts, IDVs, OTAs and OT IDVs."""
2
+
3
+ from decimal import Decimal
4
+ from unittest.mock import Mock, patch
5
+
6
+ import pytest
7
+
8
+ from tango import TangoClient
9
+ from tango.models import IDV, OTA, OTIDV, Contract, Notice, Opportunity
10
+ from tango.shapes.parser import ShapeParser
11
+
12
+ SHAPE = "key,obligations_by_fiscal_year(fiscal_year,obligated)"
13
+
14
+
15
+ def _mock(mock_request, payload):
16
+ response = Mock()
17
+ response.is_success = True
18
+ response.json.return_value = payload
19
+ response.content = b"{}"
20
+ mock_request.return_value = response
21
+
22
+
23
+ def _page(rows):
24
+ return {"count": len(rows), "next": None, "previous": None, "results": rows}
25
+
26
+
27
+ class TestObligationsByFiscalYearShape:
28
+ @pytest.mark.parametrize("model", [Contract, IDV, OTA, OTIDV])
29
+ @pytest.mark.parametrize(
30
+ "shape",
31
+ [
32
+ SHAPE,
33
+ "key,obligations_by_fiscal_year(*)",
34
+ "key,obligations_by_fiscal_year",
35
+ ],
36
+ )
37
+ def test_shape_validates(self, model, shape):
38
+ parser = ShapeParser(cache_enabled=False)
39
+ parser.validate(parser.parse(shape), model)
40
+
41
+ @pytest.mark.parametrize("model", [Contract, OTA])
42
+ def test_unknown_sub_field_is_rejected(self, model):
43
+ parser = ShapeParser(cache_enabled=False)
44
+ with pytest.raises(Exception, match="amount"):
45
+ parser.validate(parser.parse("key,obligations_by_fiscal_year(amount)"), model)
46
+
47
+
48
+ class TestObligationsByFiscalYearResponses:
49
+ @pytest.mark.parametrize("method", ["list_contracts", "list_otas"])
50
+ @patch("tango.client.httpx.Client.request")
51
+ def test_list_parses_each_year(self, mock_request, method):
52
+ _mock(
53
+ mock_request,
54
+ _page(
55
+ [
56
+ {
57
+ "key": "K1",
58
+ "obligations_by_fiscal_year": [
59
+ {"fiscal_year": 2023, "obligated": 1000.0},
60
+ {"fiscal_year": 2024, "obligated": -250.5},
61
+ ],
62
+ }
63
+ ]
64
+ ),
65
+ )
66
+
67
+ (row,) = getattr(TangoClient(api_key="k"), method)(shape=SHAPE).results
68
+
69
+ years = row["obligations_by_fiscal_year"]
70
+ assert [y["fiscal_year"] for y in years] == [2023, 2024]
71
+ assert [y["obligated"] for y in years] == [Decimal("1000.0"), Decimal("-250.5")]
72
+ assert all(isinstance(y["obligated"], Decimal) for y in years)
73
+
74
+ @pytest.mark.parametrize("method", ["list_idvs", "list_otidvs"])
75
+ @patch("tango.client.httpx.Client.request")
76
+ def test_cumulative_award_types_return_none(self, mock_request, method):
77
+ _mock(mock_request, _page([{"key": "K1", "obligations_by_fiscal_year": None}]))
78
+
79
+ (row,) = getattr(TangoClient(api_key="k"), method)(shape=SHAPE).results
80
+
81
+ assert row["obligations_by_fiscal_year"] is None
82
+
83
+
84
+ class TestAttachmentExtractionStatusShape:
85
+ @pytest.mark.parametrize("model", [Opportunity, Notice])
86
+ def test_shape_validates(self, model):
87
+ parser = ShapeParser(cache_enabled=False)
88
+ parser.validate(parser.parse("title,attachments(name,extraction_status)"), model)
File without changes
File without changes
File without changes