tango-python 1.6.0__tar.gz → 1.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 (206) hide show
  1. {tango_python-1.6.0 → tango_python-1.8.0}/CHANGELOG.md +39 -0
  2. {tango_python-1.6.0 → tango_python-1.8.0}/PKG-INFO +1 -1
  3. {tango_python-1.6.0 → tango_python-1.8.0}/docs/API_REFERENCE.md +200 -5
  4. {tango_python-1.6.0 → tango_python-1.8.0}/docs/WEBHOOKS.md +2 -0
  5. {tango_python-1.6.0 → tango_python-1.8.0}/pyproject.toml +1 -1
  6. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/check_filter_shape_conformance.py +2 -0
  7. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/check_shape_coverage.py +2 -0
  8. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/generate_shape_overlay.py +18 -3
  9. {tango_python-1.6.0 → tango_python-1.8.0}/tango/__init__.py +5 -1
  10. {tango_python-1.6.0 → tango_python-1.8.0}/tango/client.py +299 -9
  11. {tango_python-1.6.0 → tango_python-1.8.0}/tango/models.py +161 -6
  12. {tango_python-1.6.0 → tango_python-1.8.0}/tango/shapes/explicit_schemas.py +116 -0
  13. {tango_python-1.6.0 → tango_python-1.8.0}/tango/shapes/generated_overlay.py +95 -23
  14. tango_python-1.8.0/tests/test_contract_appeals.py +213 -0
  15. tango_python-1.8.0/tests/test_federal_register.py +233 -0
  16. tango_python-1.8.0/tests/test_opportunity_attachments.py +72 -0
  17. tango_python-1.8.0/tests/test_organization_scope.py +107 -0
  18. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_sled.py +61 -0
  19. {tango_python-1.6.0 → tango_python-1.8.0}/.gitignore +0 -0
  20. {tango_python-1.6.0 → tango_python-1.8.0}/LICENSE +0 -0
  21. {tango_python-1.6.0 → tango_python-1.8.0}/README.md +0 -0
  22. {tango_python-1.6.0 → tango_python-1.8.0}/docs/CLIENT.md +0 -0
  23. {tango_python-1.6.0 → tango_python-1.8.0}/docs/DYNAMIC_MODELS.md +0 -0
  24. {tango_python-1.6.0 → tango_python-1.8.0}/docs/ERRORS.md +0 -0
  25. {tango_python-1.6.0 → tango_python-1.8.0}/docs/PAGINATION.md +0 -0
  26. {tango_python-1.6.0 → tango_python-1.8.0}/docs/SHAPES.md +0 -0
  27. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/README.md +0 -0
  28. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/fetch_api_schema.py +0 -0
  29. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/generate_schemas_from_api.py +0 -0
  30. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/pr_review.py +0 -0
  31. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/probe_shape_types.py +0 -0
  32. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/refresh_contract.py +0 -0
  33. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/smoke_api_parity.py +0 -0
  34. {tango_python-1.6.0 → tango_python-1.8.0}/scripts/test_production.py +0 -0
  35. {tango_python-1.6.0 → tango_python-1.8.0}/tango/exceptions.py +0 -0
  36. {tango_python-1.6.0 → tango_python-1.8.0}/tango/shapes/__init__.py +0 -0
  37. {tango_python-1.6.0 → tango_python-1.8.0}/tango/shapes/factory.py +0 -0
  38. {tango_python-1.6.0 → tango_python-1.8.0}/tango/shapes/generator.py +0 -0
  39. {tango_python-1.6.0 → tango_python-1.8.0}/tango/shapes/models.py +0 -0
  40. {tango_python-1.6.0 → tango_python-1.8.0}/tango/shapes/parser.py +0 -0
  41. {tango_python-1.6.0 → tango_python-1.8.0}/tango/shapes/schema.py +0 -0
  42. {tango_python-1.6.0 → tango_python-1.8.0}/tango/shapes/types.py +0 -0
  43. {tango_python-1.6.0 → tango_python-1.8.0}/tango/webhooks/__init__.py +0 -0
  44. {tango_python-1.6.0 → tango_python-1.8.0}/tango/webhooks/cli.py +0 -0
  45. {tango_python-1.6.0 → tango_python-1.8.0}/tango/webhooks/receiver.py +0 -0
  46. {tango_python-1.6.0 → tango_python-1.8.0}/tango/webhooks/signing.py +0 -0
  47. {tango_python-1.6.0 → tango_python-1.8.0}/tango/webhooks/simulate.py +0 -0
  48. {tango_python-1.6.0 → tango_python-1.8.0}/tests/__init__.py +0 -0
  49. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
  50. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
  51. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
  52. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
  53. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
  54. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
  55. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
  56. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
  57. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
  58. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
  59. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
  60. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
  61. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
  62. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
  63. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
  64. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[custom-key,piid,recipient(display_name),total_contract_value,award_date] +0 -0
  65. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
  66. {tango_python-1.6.0 → tango_python-1.8.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
  67. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
  68. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
  69. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
  70. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
  71. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
  72. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
  73. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
  74. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
  75. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
  76. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
  77. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
  78. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
  79. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
  80. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
  81. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
  82. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
  83. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
  84. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
  85. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
  86. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
  87. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
  88. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
  89. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
  90. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_budget_flows +0 -0
  91. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
  92. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
  93. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
  94. {tango_python-1.6.0 → tango_python-1.8.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
  95. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[custom-uei,legal_business_name,cage_code] +0 -0
  96. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
  97. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
  98. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
  99. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[custom-id,title,anticipated_award_date] +0 -0
  100. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
  101. {tango_python-1.6.0 → tango_python-1.8.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
  102. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[minimal-id,title,anticipated_award_date,fiscal_year,naics_code,status] +0 -0
  103. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
  104. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
  105. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[custom-grant_id,title,opportunity_number] +0 -0
  106. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
  107. {tango_python-1.6.0 → tango_python-1.8.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
  108. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[minimal-grant_id,opportunity_number,title,status(-),agency_code] +0 -0
  109. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
  110. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
  111. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
  112. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
  113. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
  114. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_code +0 -0
  115. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_name_text +0 -0
  116. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating +0 -0
  117. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating_max +0 -0
  118. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_performance_risk +0 -0
  119. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_type_of_investment +0 -0
  120. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_updated_time_range +0 -0
  121. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_funding_and_cio_evaluation_expansions +0 -0
  122. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_get_itdashboard_investment_by_uii +0 -0
  123. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_itdashboard_pagination +0 -0
  124. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_search +0 -0
  125. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[custom-uii,agency_name,investment_title,updated_time] +0 -0
  126. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[default-None] +0 -0
  127. {tango_python-1.6.0 → tango_python-1.8.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
  128. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
  129. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[custom-notice_id,title,solicitation_number] +0 -0
  130. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
  131. {tango_python-1.6.0 → tango_python-1.8.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
  132. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
  133. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
  134. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
  135. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
  136. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
  137. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
  138. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
  139. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
  140. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
  141. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
  142. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[custom-opportunity_id,title,solicitation_number] +0 -0
  143. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
  144. {tango_python-1.6.0 → tango_python-1.8.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
  145. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
  146. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
  147. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
  148. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
  149. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
  150. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
  151. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[custom-case_id,title,source_system,outcome] +0 -0
  152. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
  153. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[minimal-case_id,case_number,title,source_system,outcome,filed_date] +0 -0
  154. {tango_python-1.6.0 → tango_python-1.8.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
  155. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
  156. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
  157. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
  158. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
  159. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[ultra_minimal-key,piid,recipient(display_name),total_contract_value] +0 -0
  160. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
  161. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
  162. {tango_python-1.6.0 → tango_python-1.8.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
  163. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
  164. {tango_python-1.6.0 → tango_python-1.8.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
  165. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
  166. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
  167. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_with_metrics_expansion +0 -0
  168. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
  169. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_orders_uses_default_shape +0 -0
  170. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
  171. {tango_python-1.6.0 → tango_python-1.8.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_with_ordering +0 -0
  172. {tango_python-1.6.0 → tango_python-1.8.0}/tests/conftest.py +0 -0
  173. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/README.md +0 -0
  174. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/__init__.py +0 -0
  175. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/conftest.py +0 -0
  176. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_agencies_integration.py +0 -0
  177. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_contracts_integration.py +0 -0
  178. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_edge_cases_integration.py +0 -0
  179. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_entities_integration.py +0 -0
  180. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_forecasts_integration.py +0 -0
  181. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_grants_integration.py +0 -0
  182. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_itdashboard_integration.py +0 -0
  183. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_naics_integration.py +0 -0
  184. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_notices_integration.py +0 -0
  185. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_offices_integration.py +0 -0
  186. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_opportunities_integration.py +0 -0
  187. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_organizations_integration.py +0 -0
  188. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_otas_otidvs_integration.py +0 -0
  189. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_protests_integration.py +0 -0
  190. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_reference_data_integration.py +0 -0
  191. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_subawards_integration.py +0 -0
  192. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/test_vehicles_idvs_integration.py +0 -0
  193. {tango_python-1.6.0 → tango_python-1.8.0}/tests/integration/validation.py +0 -0
  194. {tango_python-1.6.0 → tango_python-1.8.0}/tests/production/__init__.py +0 -0
  195. {tango_python-1.6.0 → tango_python-1.8.0}/tests/production/conftest.py +0 -0
  196. {tango_python-1.6.0 → tango_python-1.8.0}/tests/production/test_production_smoke.py +0 -0
  197. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_api_parity.py +0 -0
  198. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_client.py +0 -0
  199. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_dibbs_exclusions_sbir.py +0 -0
  200. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_models.py +0 -0
  201. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_shape_coverage_gate.py +0 -0
  202. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_shapes.py +0 -0
  203. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_webhooks_cli.py +0 -0
  204. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_webhooks_receiver.py +0 -0
  205. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_webhooks_signing.py +0 -0
  206. {tango_python-1.6.0 → tango_python-1.8.0}/tests/test_webhooks_simulate.py +0 -0
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.8.0] - 2026-09-28
11
+
12
+ ### Added
13
+ - **Federal Register documents** (Tango API 5.3.0). Two methods over the new `/api/federal_register/` resource: `list_federal_register_documents()` and `get_federal_register_document()`, plus a `FederalRegisterDocument` schema and the `FEDERAL_REGISTER_MINIMAL` / `FEDERAL_REGISTER_COMPREHENSIVE` defaults. These are the rules, proposed rules, notices and presidential documents published in the Federal Register since 1994. All seventeen of the API's filters are explicit named parameters, including `comments_open`, `cfr_title` / `cfr_part`, `rin`, and both `agency` (a Tango organization, including its sub-agencies) and `fr_agency` (the Federal Register's own agency slug).
14
+
15
+ `get_federal_register_document()` takes the document's `uuid`, not its `document_number`, because a document number is not unique on its own: the Federal Register reused some before 2016. Look a document up by number with `list_federal_register_documents(document_number=...)`. `full_text` is available on the detail method only when named in `shape`, and neither default shape includes it, since it can run to several MB. `ordering="rank"` requires a non-empty `search`, matching the API.
16
+ - **`alerts.federal_register.match` is an alertable event type.** `create_webhook_alert(query_type="federal_register", ...)` works.
17
+ - **`agency` filter on `list_budget_accounts()`, `list_exclusions()` and `list_itdashboard_investments()`** (Tango API 5.3.0). It takes a Tango agency name, abbreviation, code or organization key (e.g. `EPA`) and matches the whole organization subtree, so a department includes its sub-agencies; OR several with `|`. On exclusions it scopes by the excluding agency. On IT Dashboard it is available at every plan.
18
+ - **The `organization` expand is accepted on budget accounts, exclusions, SBIR topics and SBIR solicitations**, along with the `organization_id` field on budget accounts and exclusions. `organization(...)` carries `agency_code`, `agency_name`, `department_code`, `department_name`, `office_code`, `office_name` and `organization_id`. The SDK rejected these shapes client-side before. The default shapes are unchanged.
19
+
20
+ ### Fixed
21
+ - **`list_itdashboard_investments()` documents the API's current filter tiers.** `agency_name` is available at every plan, not Business+ only.
22
+
23
+ ## [1.7.0] - 2026-09-23
24
+
25
+ ### Added
26
+ - **Contract Disputes Act appeals** (Tango API 4.26.0). Two methods over the new `/api/contract_appeals/` resource: `list_contract_appeals()` and `get_contract_appeal()`, plus a `ContractAppeal` schema and the `CONTRACT_APPEALS_MINIMAL` / `CONTRACT_APPEALS_COMPREHENSIVE` defaults. These are decisions of the Civilian Board of Contract Appeals (CBCA) and the Armed Services Board of Contract Appeals (ASBCA) — appeals of a contracting officer's final decision under an existing contract, which is a different dispute from a bid protest and shares no identifiers with `list_protests()`. All ten of the API's filters are explicit named parameters.
27
+
28
+ Three behaviors are documented on the methods and the model, because each is otherwise a support question. **`decision_text` needs an Enterprise plan, and below that tier the key is absent rather than null** — read it with `.get()`. **Searching that text is free at every plan**: `search=` matches inside the decision body and returns no fragment of it, so buying the body does not change what you can find. And **`docket` accepts a docket as it is cited** — a leading `CBCA` / `ASBCA` and `No.` / `Nos.` are ignored, while commas stay part of a docket (`7092-C(6682, 6765, 6767)`) rather than separating two.
29
+
30
+ Neither default shape names `decision_text`: the body runs to roughly 100K characters, so putting it in a default would make every fetch ask for something most callers cannot read. `ordering="rank"` requires a non-empty `search`, matching the API.
31
+ - **`alerts.contract_appeal.match` is an alertable event type.** `create_webhook_alert(query_type="contract_appeal", ...)` works. That method's `query_type` docstring now names the full current set and points at `list_webhook_event_types()` as the authority, since a list written into a docstring can only go stale.
32
+
33
+ ### Fixed
34
+ - **`attachments(doc_role, doc_role_alt)` on opportunities and notices no longer raises `ShapeValidationError`.** The Tango API serves both fields on a Pro plan or above, but the SDK rejected them client-side after the request had already been made. `doc_role` says what an attachment is for (`requirement`, `instructions`, `pricing`, `terms`, `reference` or `unknown`) and `doc_role_alt` is a runner-up role that is usually null. Both must be named in the shape, since `attachments(*)` does not include them, and an attachment that has not been classified omits both keys, so read them with `.get()`.
35
+ - **The shape overlay generator no longer lets one resource's view of a model hide fields from another's.** An opportunity is reachable both from `list_opportunities()` and as the `opportunity` expand on vehicles, and the vehicles embed exposes fewer attachment fields. Whichever tree was generated last replaced the other, so the opportunity schema lost fields its own endpoint serves. The generator now adds the missing fields instead of replacing the schema, and keeps every existing field's type as it was.
36
+ - **SLED solicitations accept `delisted_at` and `meta(jurisdiction_declared)`.** Both were rejected client-side even though the API returns them. `delisted_at` is when a portal dropped a solicitation from its listing before its deadline, which is what `status_reason="delisted"` means. `meta.jurisdiction_declared` says whether the source stated the jurisdiction level itself or Tango classified it from the issuer's name. `delisted_at` is now in both `SLED_OPPORTUNITIES_MINIMAL` and `SLED_OPPORTUNITIES_COMPREHENSIVE`, matching the API's own list default.
37
+
38
+ ### Changed
39
+ - **SLED response values now carry the types the API returns.** The SDK's SLED schema had guessed several types from field names. `posted_date`, `response_deadline`, `response_deadline_original` and `bid_opening_date` on `SledOpportunity` now parse to timezone-aware `datetime` values (before, `posted_date` and `bid_opening_date` were truncated to a `date` and the two deadlines stayed strings), as does `estimated_advertisement_date` on `SledForecast`. `estimated_value(min, max)` on forecasts now parses to `Decimal`. `category_codes` is typed as a list of `{scheme, code}` objects, `attachments` and `revisions` as lists, `meta(last_change_source_declared)` and `revisions(source_declared)` as booleans, and `attachments(size_bytes, pages)` and `revisions(sequence)` as integers. If you compared any of the date fields as strings, compare them as `datetime` values instead.
40
+ - Re-vendored `contracts/filter_shape_contract.json` (Tango API 5.1.0) and regenerated `tango/shapes/generated_overlay.py`. The one resource the SDK does not wrap yet, `ebuy/requests`, is recorded in `contracts/shape_coverage_baseline.json` as a known gap.
41
+ - The `ContractAppeal` shape schema is hand-curated in `tango/shapes/explicit_schemas.py` rather than left to the generated overlay. The generator falls back to name heuristics for a resource the live-API type probe has not sampled yet, and those heuristics get four of this resource's fields wrong: `docket_numbers` is a list, `listed` and `decision_date_repaired` are booleans, and `listing_year` is an integer.
42
+
43
+ ### Documentation
44
+ - `list_protests()` and `docs/API_REFERENCE.md` now describe all three protest venues: GAO, the U.S. Court of Federal Claims (COFC) and the SBA Office of Hearings and Appeals (SBA OHA). `naics_code` is documented as the NAICS code at issue in an SBA OHA size or NAICS appeal, which GAO and COFC protests never carry, rather than the solicitation's code. `case_number` examples now cover each venue's format, and `naics_code` is listed among the filter parameters.
45
+ - `docs/API_REFERENCE.md` documents the document-role fields on opportunity and notice attachments, and `delisted_at` and `meta.jurisdiction_declared` on SLED solicitations.
46
+ - New **Contract Appeals** section in `docs/API_REFERENCE.md`, leading with what separates an appeal from a protest, plus the two new `ShapeConfig` constants in the shapes table.
47
+ - Added `alerts.contract_appeal.match` to the event-type listing in `docs/WEBHOOKS.md`.
48
+
10
49
  ## [1.6.0] - 2026-09-10
11
50
 
12
51
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tango-python
3
- Version: 1.6.0
3
+ Version: 1.8.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
@@ -22,6 +22,8 @@ Complete reference for all Tango Python SDK methods and functionality.
22
22
  - [Grants](#grants)
23
23
  - [GSA eLibrary Contracts](#gsa-elibrary-contracts)
24
24
  - [Protests](#protests)
25
+ - [Contract Appeals](#contract-appeals)
26
+ - [Federal Register](#federal-register)
25
27
  - [Budget](#budget)
26
28
  - [Business Types](#business-types)
27
29
  - [NAICS](#naics)
@@ -925,6 +927,25 @@ for opp in opportunities.results:
925
927
  - `naics_code` - Industry code
926
928
  - `psc_code` - Product/service code
927
929
 
930
+ **Document roles — `attachments(doc_role, doc_role_alt)`:**
931
+
932
+ On a **Pro plan or above**, each attachment can say what the document is for.
933
+ `doc_role` is one of `requirement`, `instructions`, `pricing`, `terms`, `reference` or `unknown`; `doc_role_alt` is a runner-up role, and is null on nearly every attachment.
934
+
935
+ ```python
936
+ opp = client.get_opportunity(
937
+ opportunity_id,
938
+ shape="opportunity_id,title,attachments(name,url,doc_role,doc_role_alt)",
939
+ )
940
+ for attachment in opp["attachments"]:
941
+ print(attachment["name"], attachment.get("doc_role"))
942
+ ```
943
+
944
+ - **You have to name them.** `attachments(*)` does not include either field, and no default shape does.
945
+ - **The keys are absent, not null**, on an attachment Tango has not classified. Use `attachment.get("doc_role")`.
946
+ - A Free-plan request that names them gets the response without them, plus an entry in `meta.upgrade_hints`.
947
+ - There is no filter on document role.
948
+
928
949
  ---
929
950
 
930
951
  ## Notices
@@ -1143,7 +1164,7 @@ contract = client.get_gsa_elibrary_contract("UUID_HERE")
1143
1164
 
1144
1165
  ## Protests
1145
1166
 
1146
- Bid protest records (GAO, COFC, etc.).
1167
+ Bid protest records from three venues: GAO, the U.S. Court of Federal Claims (COFC), and the SBA Office of Hearings and Appeals (SBA OHA).
1147
1168
 
1148
1169
  ### list_protests()
1149
1170
 
@@ -1166,20 +1187,22 @@ protests = client.list_protests(
1166
1187
  filed_date_before=None,
1167
1188
  decision_date_after=None,
1168
1189
  decision_date_before=None,
1190
+ naics_code=None,
1169
1191
  search=None,
1170
1192
  )
1171
1193
  ```
1172
1194
 
1173
1195
  **Filter Parameters:**
1174
- - `source_system` - Filter by source system (e.g., `"gao"`)
1196
+ - `source_system` - Filter by source system: `"gao"`, `"cofc"`, or `"sba_oha"`
1175
1197
  - `outcome` - Filter by outcome (e.g., `"Denied"`, `"Dismissed"`, `"Withdrawn"`, `"Sustained"`)
1176
1198
  - `case_type` - Filter by case type
1177
1199
  - `agency` - Filter by protested agency
1178
- - `case_number` - Filter by case number (e.g., `"b-423274"`)
1200
+ - `case_number` - Filter by base case number, matched case-insensitively (e.g., `"b-423274"` for GAO, `"26-1391"` for COFC, `"SIZ-6100"` for SBA OHA)
1179
1201
  - `solicitation_number` - Filter by solicitation number
1180
1202
  - `protester` - Search by protester name
1181
1203
  - `filed_date_after` / `filed_date_before` - Filed date range
1182
1204
  - `decision_date_after` / `decision_date_before` - Decision date range
1205
+ - `naics_code` - The NAICS code at issue in an SBA OHA size or NAICS appeal (e.g., `"541519"`). GAO and COFC protests carry no NAICS code, so this filter returns SBA OHA records only
1183
1206
  - `search` - Full-text search
1184
1207
 
1185
1208
  **Returns:** [PaginatedResponse](#paginatedresponse) with protest dictionaries
@@ -1214,6 +1237,172 @@ protest = client.get_protest(
1214
1237
 
1215
1238
  ---
1216
1239
 
1240
+ ## Contract Appeals
1241
+
1242
+ Contract Disputes Act appeal decisions from the Civilian Board of Contract Appeals (CBCA) and the Armed Services Board of Contract Appeals (ASBCA).
1243
+
1244
+ **These are not bid protests.** A protest challenges an award or a solicitation before performance; an appeal here challenges a contracting officer's final decision under an existing contract — a claim, a termination, a differing-site-conditions dispute. Protests live at [Protests](#protests) and share no identifiers with this resource.
1245
+
1246
+ One row is one decision as the board's own listing publishes it, so several fields describe the listing rather than the dispute. `listed` goes false once the board's newest listing stops carrying the decision; the row is kept, never dropped.
1247
+
1248
+ ### list_contract_appeals()
1249
+
1250
+ List appeal decisions with filtering and shaping.
1251
+
1252
+ ```python
1253
+ appeals = client.list_contract_appeals(
1254
+ page=1,
1255
+ limit=25,
1256
+ shape=ShapeConfig.CONTRACT_APPEALS_MINIMAL,
1257
+ # Filter parameters (all optional)
1258
+ board=None,
1259
+ docket=None,
1260
+ appellant=None,
1261
+ judge=None,
1262
+ decision_type=None,
1263
+ decision_date_after=None,
1264
+ decision_date_before=None,
1265
+ listed=None,
1266
+ document_id=None,
1267
+ search=None,
1268
+ ordering=None,
1269
+ )
1270
+ ```
1271
+
1272
+ **Filter Parameters:**
1273
+ - `board` - `"cbca"` or `"asbca"`; OR both with `"cbca|asbca"`
1274
+ - `docket` - Exact docket match, e.g. `"3288-R"`, `"59116"` or `"7092-C(6682, 6765, 6767)"`. A leading `CBCA` / `ASBCA` and `No.` / `Nos.` are ignored, so a docket can be pasted as it is cited. Commas are part of a docket, never a separator; OR several with `|`
1275
+ - `appellant` - Case-insensitive substring match on the appellant's name (min 2 characters)
1276
+ - `judge` - Case-insensitive exact match on the judge as the listing names them
1277
+ - `decision_type` - CBCA only: `"Decision"`, `"Dismissal"`, `"Order"`, `"Full Board Order"`, or the listing's own text. ASBCA rows carry no type
1278
+ - `decision_date_after` / `decision_date_before` - Decision date range (`YYYY-MM-DD`)
1279
+ - `listed` - Whether the board's newest listing still carries the decision
1280
+ - `document_id` - The board's own document id
1281
+ - `search` - Ranked full-text search over the appellant and the full decision text (min 2 characters); wrap in double quotes for a phrase
1282
+ - `ordering` - `decision_date` (the default, as `-decision_date`), `appellant`, `first_listed_at` or `rank`. `rank` requires a non-empty `search`
1283
+
1284
+ **Returns:** [PaginatedResponse](#paginatedresponse) with appeal-decision dictionaries
1285
+
1286
+ **Example:**
1287
+ ```python
1288
+ appeals = client.list_contract_appeals(
1289
+ board="asbca",
1290
+ search="differing site conditions",
1291
+ decision_date_after="2025-01-01",
1292
+ ordering="rank",
1293
+ limit=25,
1294
+ )
1295
+
1296
+ for appeal in appeals.results:
1297
+ dockets = ", ".join(appeal["docket_numbers"])
1298
+ print(f"{appeal['board'].upper()} {dockets}: {appeal['appellant']} ({appeal['decision_date']})")
1299
+ ```
1300
+
1301
+ ### get_contract_appeal()
1302
+
1303
+ Get a single decision by `uuid`.
1304
+
1305
+ ```python
1306
+ appeal = client.get_contract_appeal(
1307
+ "DECISION_UUID",
1308
+ shape=ShapeConfig.CONTRACT_APPEALS_COMPREHENSIVE,
1309
+ )
1310
+ ```
1311
+
1312
+ **Notes:**
1313
+ - `docket_numbers` is a list — the dockets with the board prefix stripped (`3288-R`, `59116`), and a consolidated appeal carries several. `docket_raw` keeps the listing's own text, and `docket_source` says where the dockets came from.
1314
+ - **Reading `decision_text` requires an Enterprise plan, and below that tier the key is absent rather than null** — read it with `.get()`, not `[...]`. Neither default shape names it: the body runs to roughly 100K characters, so asking for it on every row is rarely what you want.
1315
+ - **Searching that text is free at every plan.** `search=` matches inside the decision body and returns no fragment of it, so buying the body does not change what you can find.
1316
+
1317
+ ---
1318
+
1319
+ ## Federal Register
1320
+
1321
+ Federal Register documents — rules, proposed rules, notices and presidential documents published since 1994.
1322
+
1323
+ A document is identified by `uuid`. `document_number` is not unique on its own: the Federal Register reused some numbers before 2016, so `document_number=` can return more than one document.
1324
+
1325
+ ### list_federal_register_documents()
1326
+
1327
+ List documents with filtering and shaping.
1328
+
1329
+ ```python
1330
+ documents = client.list_federal_register_documents(
1331
+ page=1,
1332
+ limit=25,
1333
+ shape=ShapeConfig.FEDERAL_REGISTER_MINIMAL,
1334
+ # Filter parameters (all optional)
1335
+ search=None,
1336
+ document_number=None,
1337
+ type=None,
1338
+ agency=None,
1339
+ fr_agency=None,
1340
+ publication_date_after=None,
1341
+ publication_date_before=None,
1342
+ effective_on_after=None,
1343
+ effective_on_before=None,
1344
+ comments_close_on_after=None,
1345
+ comments_close_on_before=None,
1346
+ comments_open=None,
1347
+ cfr_title=None,
1348
+ cfr_part=None,
1349
+ significant=None,
1350
+ rin=None,
1351
+ executive_order_number=None,
1352
+ ordering=None,
1353
+ )
1354
+ ```
1355
+
1356
+ **Filter Parameters:**
1357
+ - `search` - Ranked full-text search over the title, abstract and action; wrap in double quotes for a phrase
1358
+ - `document_number` - Exact FR document number, e.g. `"2016-31922"`; OR several with `|`
1359
+ - `type` - `"Notice"`, `"Rule"`, `"Proposed Rule"`, `"Presidential Document"`, `"Correction"`, `"Sunshine Act Document"` or `"Uncategorized Document"` (case-insensitive); OR several with `|`. An unknown type is an error, not an empty page. Documents published before 2008 are mostly `Uncategorized Document`, so a type filter undercounts that era
1360
+ - `agency` - A Tango agency name, abbreviation, code or organization key, e.g. `"EPA"`. Matches a document when any agency it lists falls within that organization, so a department includes its sub-agencies; OR several with `|`
1361
+ - `fr_agency` - The Federal Register's own agency slug, e.g. `"environmental-protection-agency"`; OR several with `|`
1362
+ - `publication_date_after` / `publication_date_before` - Publication date range (`YYYY-MM-DD`)
1363
+ - `effective_on_after` / `effective_on_before` - Effective date range (`YYYY-MM-DD`)
1364
+ - `comments_close_on_after` / `comments_close_on_before` - Comment-deadline range (`YYYY-MM-DD`)
1365
+ - `comments_open` - `True` for documents whose comment period closes today or later, `False` for those already closed; documents with no comment deadline match neither
1366
+ - `cfr_title` - A CFR title number, e.g. `"40"`
1367
+ - `cfr_part` - A CFR part number, e.g. `"52"`. Requires `cfr_title`, and both must match the same CFR reference, so `cfr_title="40", cfr_part="52"` finds 40 CFR 52
1368
+ - `significant` - Significant under Executive Order 12866
1369
+ - `rin` - A Regulation Identifier Number, e.g. `"2060-AV16"`; OR several with `|`
1370
+ - `executive_order_number` - Exact executive order number
1371
+ - `ordering` - `publication_date` (the default, as `-publication_date`), `effective_on`, `comments_close_on`, `document_number` or `rank`. `rank` requires a non-empty `search`
1372
+
1373
+ **Returns:** [PaginatedResponse](#paginatedresponse) with document dictionaries
1374
+
1375
+ **Example:**
1376
+ ```python
1377
+ documents = client.list_federal_register_documents(
1378
+ type="Proposed Rule",
1379
+ comments_open=True,
1380
+ agency="EPA",
1381
+ limit=25,
1382
+ )
1383
+
1384
+ for doc in documents.results:
1385
+ print(f"{doc['document_number']} ({doc['publication_date']}): {doc['title']}")
1386
+ print(f" comments close {doc['comments_close_on']}")
1387
+ ```
1388
+
1389
+ ### get_federal_register_document()
1390
+
1391
+ Get a single document by `uuid`. To look one up by its document number, use `list_federal_register_documents(document_number=...)`.
1392
+
1393
+ ```python
1394
+ document = client.get_federal_register_document(
1395
+ "DOCUMENT_UUID",
1396
+ shape=ShapeConfig.FEDERAL_REGISTER_COMPREHENSIVE,
1397
+ )
1398
+ ```
1399
+
1400
+ **Notes:**
1401
+ - `agencies`, `cfr_references`, `dockets` and `topics` are the Federal Register's own structures, served as published. `agencies` carries Federal Register agency slugs, not Tango organization keys.
1402
+ - `full_text`, the document's plain text, is available on this method only and only when named in `shape` (e.g. `shape="uuid,title,full_text"`). It can run to several MB, so neither default shape includes it.
1403
+
1404
+ ---
1405
+
1217
1406
  ## State & Local (SLED)
1218
1407
 
1219
1408
  State, local and education procurement — solicitations that never appear on SAM.gov because they were never federal. **Beta**: coverage is partial and grows one jurisdiction at a time.
@@ -1323,6 +1512,8 @@ row = client.get_sled_opportunity(
1323
1512
  - `meta.attachment_count` can be **lower** than `len(row["attachments"])`. Some portals auto-generate a cover sheet alongside the real documents; it is listed and flagged `is_generated_summary`, but excluded from the count and from `has_documents`. The count answers "does this hold its solicitation package"; the array answers "what files exist".
1324
1513
  - `size_bytes` and `char_count` only mean something as a pair — 3 MB that yielded no characters is a scan awaiting OCR.
1325
1514
  - `raw(*)` needs a Small plan or above, and is explicitly unstable: its shape varies by portal platform.
1515
+ - `delisted_at` is when a portal dropped the solicitation from its listing before its deadline, and is what `status_reason="delisted"` means. It is null when the solicitation was never delisted or has been seen again since.
1516
+ - `meta.jurisdiction_declared` says whether the source stated the jurisdiction level itself. `False` means Tango classified it from the issuer's name, which is the common case on a state's central portal.
1326
1517
 
1327
1518
  **Reading a document body — `attachments(extracted_text)`:**
1328
1519
 
@@ -2206,12 +2397,16 @@ entity = client.get_entity("UEI_KEY", shape=ShapeConfig.ENTITIES_COMPREHENSIVE)
2206
2397
  | `SUBAWARDS_MINIMAL` | `list_subawards` | award_key, prime_recipient(uei,display_name), subaward_recipient(uei,display_name) |
2207
2398
  | `GSA_ELIBRARY_CONTRACTS_MINIMAL` | `list_gsa_elibrary_contracts` | uuid, contract_number, schedule, recipient(display_name,uei), idv(key,award_date) |
2208
2399
  | `PROTESTS_MINIMAL` | `list_protests` | case_id, case_number, title, source_system, outcome, filed_date |
2400
+ | `CONTRACT_APPEALS_MINIMAL` | `list_contract_appeals` | uuid, board, docket_numbers, decision_date, appellant, judge, decision_type, url |
2401
+ | `CONTRACT_APPEALS_COMPREHENSIVE` | `get_contract_appeal` | The list fields plus docket_raw, docket_source, decision_date_repaired, decision_type_raw, listing_year, first_listed_at, listed, text_status, text_char_count (omits `decision_text`, which needs an Enterprise plan) |
2402
+ | `FEDERAL_REGISTER_MINIMAL` | `list_federal_register_documents` | uuid, document_number, publication_date, type, subtype, title, abstract, action, agencies, cfr_references, citation, significant, comments_close_on, effective_on, html_url, pdf_url |
2403
+ | `FEDERAL_REGISTER_COMPREHENSIVE` | `get_federal_register_document` | The list fields plus dates, signing_date, start_page, end_page, volume, docket_ids, dockets, regulation_id_numbers, topics, correction_of, corrections, executive_order_number, presidential_document_number, proclamation_number, comment_url, regulations_dot_gov_url, raw_text_url, body_html_url (omits `full_text`) |
2209
2404
  | `BUDGET_ACCOUNTS_MINIMAL` | `list_budget_accounts`, `get_budget_account` | id, federal_account_symbol, fiscal_year, agency_code/name, bureau_name, account_title, bea_category, on_off_budget, subfunction_code, lifecycle (requested/enacted/apportioned/obligated/outlayed/unobligated), contract & assistance rollups, key ratios, next-year growth |
2210
2405
  | `VEHICLE_ORDERS_MINIMAL` | `list_vehicle_orders` | key, piid, award_date, recipient(display_name,uei), total_contract_value, obligated |
2211
2406
  | `ITDASHBOARD_INVESTMENTS_MINIMAL` | `list_itdashboard_investments` | Minimal IT Dashboard investment fields |
2212
2407
  | `ITDASHBOARD_INVESTMENTS_COMPREHENSIVE` | `get_itdashboard_investment` | Full investment fields: uii, agency_code, agency_name, bureau_code, bureau_name, investment_title, type_of_investment, part_of_it_portfolio, updated_time, url |
2213
- | `SLED_OPPORTUNITIES_MINIMAL` | `list_sled_opportunities` | opportunity_id, solicitation_number, solicitation_type, title, state, jurisdiction, agency, status, status_reason, posted_date, response_deadline, source_url, has_documents, first_seen_at, last_change_seen_at (no `description` — detail-only on the API) |
2214
- | `SLED_OPPORTUNITIES_COMPREHENSIVE` | `get_sled_opportunity` | Full solicitation with description, the raw portal status, both deadlines, bid opening, category codes, and organization / contact / meta / attachments / revisions |
2408
+ | `SLED_OPPORTUNITIES_MINIMAL` | `list_sled_opportunities` | opportunity_id, solicitation_number, solicitation_type, title, state, jurisdiction, agency, status, status_reason, delisted_at, posted_date, response_deadline, source_url, has_documents, first_seen_at, last_change_seen_at (no `description` — detail-only on the API) |
2409
+ | `SLED_OPPORTUNITIES_COMPREHENSIVE` | `get_sled_opportunity` | Full solicitation with description, the raw portal status, the delisting timestamp, both deadlines, bid opening, category codes, and organization / contact / meta / attachments / revisions |
2215
2410
  | `SLED_REVISIONS_MINIMAL` | `list_sled_opportunity_revisions` | observed_at, sequence, kind, changed_fields, source_declared (omits `changes`, which needs a Small plan) |
2216
2411
  | `SLED_FORECASTS_MINIMAL` | `list_sled_forecasts` | forecast_id, state, agency, title, estimated_advertisement_date, estimated_advertisement_raw, procurement_category, procurement_method, contract_number, incumbent_name, source_url, estimated_value(*) |
2217
2412
  | `SLED_FORECASTS_COMPREHENSIVE` | `get_sled_forecast` | Full forecast with description, contract_term, mbe_dbe_goal, delivery_location, and organization / contact / estimated_value |
@@ -88,6 +88,8 @@ tango webhooks list-event-types
88
88
  # alerts.dibbs_rfq.match DLA DIBBS RFQ matched a saved alert
89
89
  # alerts.dibbs_rfp.match DLA DIBBS RFP matched a saved alert
90
90
  # alerts.sled_opportunity.match State/local/education solicitation matched a saved alert
91
+ # alerts.contract_appeal.match CBCA/ASBCA appeal decision matched a saved alert
92
+ # alerts.federal_register.match Federal Register document matched a saved alert
91
93
  ```
92
94
 
93
95
  This list is served by the API, so it is always current — the SDK does not hardcode
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tango-python"
7
- version = "1.6.0"
7
+ version = "1.8.0"
8
8
  description = "Python SDK for the Tango API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -77,6 +77,8 @@ RESOURCE_TO_METHOD: dict[str, str | None] = {
77
77
  "sled/opportunities": "list_sled_opportunities",
78
78
  "sled/forecasts": "list_sled_forecasts",
79
79
  "protests": "list_protests",
80
+ "contract_appeals": "list_contract_appeals",
81
+ "federal_register": "list_federal_register_documents",
80
82
  "offices": "list_offices",
81
83
  "psc": "list_psc",
82
84
  "mas_sins": "list_mas_sins",
@@ -68,6 +68,8 @@ RESOURCE_TO_MODEL: dict[str, str] = {
68
68
  "budget/accounts": "BudgetAccount",
69
69
  "budget_accounts": "BudgetAccount",
70
70
  "protests": "Protest",
71
+ "contract_appeals": "ContractAppeal",
72
+ "federal_register": "FederalRegisterDocument",
71
73
  "offices": "Office",
72
74
  "assistance_listings": "AssistanceListing",
73
75
  "business_types": "BusinessType",
@@ -174,6 +174,20 @@ overlay: dict[str, dict[str, dict]] = {}
174
174
  report_rows: list[str] = []
175
175
 
176
176
 
177
+ def put(container: str, name: str, e: dict) -> None:
178
+ """Record an overlay entry, unioning nested schemas when two resources reach the same container.
179
+
180
+ A model can be reached from its own resource and as another resource's expand (`opportunities` vs `vehicles.opportunity`), and those trees can expose different sub-fields.
181
+ Overwriting let a narrower embed hide fields the resource itself serves; the union only adds the missing ones, and the later entry still decides every field both trees define, so no existing type moves.
182
+ """
183
+ cur = overlay.setdefault(container, {}).get(name)
184
+ nested = {cur.get("nested") if cur else None, e.get("nested")}
185
+ if cur and None not in nested and "CodeDescription" not in nested and len(nested) == 2:
186
+ merged = {**nested_schemas[cur["nested"]], **nested_schemas[e["nested"]]}
187
+ e = {**e, "nested": intern_nested(re.sub(r"\d+$", "", e["nested"]), merged)}
188
+ overlay[container][name] = e
189
+
190
+
177
191
  def walk(res: str, path: str, node: dict, schema, container: str) -> None:
178
192
  if schema is None:
179
193
  return
@@ -182,7 +196,7 @@ def walk(res: str, path: str, node: dict, schema, container: str) -> None:
182
196
  for f in fields:
183
197
  if f != "*" and f not in schema:
184
198
  t, lst = resolve_scalar(res, path, f)
185
- overlay.setdefault(container, {})[f] = entry(t, True, lst)
199
+ put(container, f, entry(t, True, lst))
186
200
  report_rows.append(f"{res}:{path or '(root)'}.{f} -> {t}{'[]' if lst else ''}")
187
201
  for ename, enode in (node.get("expands") or {}).items():
188
202
  fs = schema.get(ename)
@@ -192,7 +206,7 @@ def walk(res: str, path: str, node: dict, schema, container: str) -> None:
192
206
  if fs is None or child_schema is None:
193
207
  if is_wildcard(enode) and fs is not None:
194
208
  continue
195
- overlay.setdefault(container, {})[ename] = expand_entry(res, path, ename, enode)
209
+ put(container, ename, expand_entry(res, path, ename, enode))
196
210
  report_rows.append(f"{res}:{path or '(root)'}.{ename} -> expand")
197
211
  else:
198
212
  walk(res, child_path, enode, child_schema, container=nested_name)
@@ -214,7 +228,8 @@ for rkey, r in contract["resources"].items():
214
228
  for cname, cnode in (shape.get("expands") or {}).items():
215
229
  s[cname] = expand_entry(rkey, "", cname, cnode)
216
230
  if model_name:
217
- overlay[model_name] = {**overlay.get(model_name, {}), **s}
231
+ for name, e in s.items():
232
+ put(model_name, name, e)
218
233
  continue
219
234
  walk(rkey, "", shape, schema, container=model_name)
220
235
 
@@ -10,10 +10,12 @@ from .exceptions import (
10
10
  )
11
11
  from .models import (
12
12
  BudgetAccount,
13
+ ContractAppeal,
13
14
  DibbsAward,
14
15
  DibbsRfp,
15
16
  DibbsRfq,
16
17
  Exclusion,
18
+ FederalRegisterDocument,
17
19
  GsaElibraryContract,
18
20
  ITDashboardInvestment,
19
21
  PaginatedResponse,
@@ -53,7 +55,7 @@ from .webhooks import (
53
55
  )
54
56
  from .webhooks.receiver import Delivery, WebhookReceiver
55
57
 
56
- __version__ = "1.6.0"
58
+ __version__ = "1.8.0"
57
59
  __all__ = [
58
60
  "TangoClient",
59
61
  "TangoAPIError",
@@ -65,6 +67,8 @@ __all__ = [
65
67
  "ResolveCandidate",
66
68
  "ResolveResult",
67
69
  "BudgetAccount",
70
+ "ContractAppeal",
71
+ "FederalRegisterDocument",
68
72
  "DibbsAward",
69
73
  "DibbsRfp",
70
74
  "DibbsRfq",