tango-python 1.5.1__tar.gz → 1.7.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 (204) hide show
  1. {tango_python-1.5.1 → tango_python-1.7.0}/CHANGELOG.md +45 -0
  2. {tango_python-1.5.1 → tango_python-1.7.0}/PKG-INFO +2 -2
  3. {tango_python-1.5.1 → tango_python-1.7.0}/docs/API_REFERENCE.md +311 -3
  4. {tango_python-1.5.1 → tango_python-1.7.0}/docs/WEBHOOKS.md +16 -5
  5. {tango_python-1.5.1 → tango_python-1.7.0}/pyproject.toml +1 -1
  6. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/check_filter_shape_conformance.py +3 -0
  7. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/check_shape_coverage.py +3 -0
  8. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/generate_shape_overlay.py +18 -3
  9. {tango_python-1.5.1 → tango_python-1.7.0}/tango/__init__.py +9 -1
  10. {tango_python-1.5.1 → tango_python-1.7.0}/tango/client.py +595 -5
  11. {tango_python-1.5.1 → tango_python-1.7.0}/tango/models.py +244 -2
  12. {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/explicit_schemas.py +38 -0
  13. {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/generated_overlay.py +547 -48
  14. tango_python-1.7.0/tests/test_contract_appeals.py +213 -0
  15. tango_python-1.7.0/tests/test_opportunity_attachments.py +72 -0
  16. tango_python-1.7.0/tests/test_sled.py +342 -0
  17. {tango_python-1.5.1 → tango_python-1.7.0}/.gitignore +0 -0
  18. {tango_python-1.5.1 → tango_python-1.7.0}/LICENSE +0 -0
  19. {tango_python-1.5.1 → tango_python-1.7.0}/README.md +0 -0
  20. {tango_python-1.5.1 → tango_python-1.7.0}/docs/CLIENT.md +0 -0
  21. {tango_python-1.5.1 → tango_python-1.7.0}/docs/DYNAMIC_MODELS.md +0 -0
  22. {tango_python-1.5.1 → tango_python-1.7.0}/docs/ERRORS.md +0 -0
  23. {tango_python-1.5.1 → tango_python-1.7.0}/docs/PAGINATION.md +0 -0
  24. {tango_python-1.5.1 → tango_python-1.7.0}/docs/SHAPES.md +0 -0
  25. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/README.md +0 -0
  26. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/fetch_api_schema.py +0 -0
  27. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/generate_schemas_from_api.py +0 -0
  28. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/pr_review.py +0 -0
  29. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/probe_shape_types.py +0 -0
  30. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/refresh_contract.py +0 -0
  31. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/smoke_api_parity.py +0 -0
  32. {tango_python-1.5.1 → tango_python-1.7.0}/scripts/test_production.py +0 -0
  33. {tango_python-1.5.1 → tango_python-1.7.0}/tango/exceptions.py +0 -0
  34. {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/__init__.py +0 -0
  35. {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/factory.py +0 -0
  36. {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/generator.py +0 -0
  37. {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/models.py +0 -0
  38. {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/parser.py +0 -0
  39. {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/schema.py +0 -0
  40. {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/types.py +0 -0
  41. {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/__init__.py +0 -0
  42. {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/cli.py +0 -0
  43. {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/receiver.py +0 -0
  44. {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/signing.py +0 -0
  45. {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/simulate.py +0 -0
  46. {tango_python-1.5.1 → tango_python-1.7.0}/tests/__init__.py +0 -0
  47. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
  48. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
  49. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
  50. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
  51. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
  52. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
  53. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
  54. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
  55. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
  56. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
  57. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
  58. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
  59. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
  60. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
  61. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
  62. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[custom-key,piid,recipient(display_name),total_contract_value,award_date] +0 -0
  63. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
  64. {tango_python-1.5.1 → tango_python-1.7.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
  65. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
  66. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
  67. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
  68. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
  69. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
  70. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
  71. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
  72. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
  73. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
  74. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
  75. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
  76. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
  77. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
  78. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
  79. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
  80. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
  81. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
  82. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
  83. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
  84. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
  85. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
  86. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
  87. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
  88. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_budget_flows +0 -0
  89. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
  90. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
  91. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
  92. {tango_python-1.5.1 → tango_python-1.7.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
  93. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[custom-uei,legal_business_name,cage_code] +0 -0
  94. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
  95. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
  96. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
  97. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[custom-id,title,anticipated_award_date] +0 -0
  98. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
  99. {tango_python-1.5.1 → tango_python-1.7.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
  100. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[minimal-id,title,anticipated_award_date,fiscal_year,naics_code,status] +0 -0
  101. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
  102. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
  103. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[custom-grant_id,title,opportunity_number] +0 -0
  104. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
  105. {tango_python-1.5.1 → tango_python-1.7.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
  106. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[minimal-grant_id,opportunity_number,title,status(-),agency_code] +0 -0
  107. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
  108. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
  109. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
  110. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
  111. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
  112. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_code +0 -0
  113. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_name_text +0 -0
  114. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating +0 -0
  115. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating_max +0 -0
  116. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_performance_risk +0 -0
  117. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_type_of_investment +0 -0
  118. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_updated_time_range +0 -0
  119. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_funding_and_cio_evaluation_expansions +0 -0
  120. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_get_itdashboard_investment_by_uii +0 -0
  121. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_itdashboard_pagination +0 -0
  122. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_search +0 -0
  123. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[custom-uii,agency_name,investment_title,updated_time] +0 -0
  124. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[default-None] +0 -0
  125. {tango_python-1.5.1 → tango_python-1.7.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
  126. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
  127. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[custom-notice_id,title,solicitation_number] +0 -0
  128. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
  129. {tango_python-1.5.1 → tango_python-1.7.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
  130. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
  131. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
  132. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
  133. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
  134. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
  135. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
  136. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
  137. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
  138. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
  139. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
  140. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[custom-opportunity_id,title,solicitation_number] +0 -0
  141. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
  142. {tango_python-1.5.1 → tango_python-1.7.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
  143. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
  144. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
  145. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
  146. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
  147. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
  148. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
  149. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[custom-case_id,title,source_system,outcome] +0 -0
  150. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
  151. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[minimal-case_id,case_number,title,source_system,outcome,filed_date] +0 -0
  152. {tango_python-1.5.1 → tango_python-1.7.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
  153. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
  154. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
  155. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
  156. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
  157. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[ultra_minimal-key,piid,recipient(display_name),total_contract_value] +0 -0
  158. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
  159. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
  160. {tango_python-1.5.1 → tango_python-1.7.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
  161. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
  162. {tango_python-1.5.1 → tango_python-1.7.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
  163. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
  164. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
  165. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_with_metrics_expansion +0 -0
  166. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
  167. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_orders_uses_default_shape +0 -0
  168. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
  169. {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_with_ordering +0 -0
  170. {tango_python-1.5.1 → tango_python-1.7.0}/tests/conftest.py +0 -0
  171. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/README.md +0 -0
  172. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/__init__.py +0 -0
  173. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/conftest.py +0 -0
  174. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_agencies_integration.py +0 -0
  175. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_contracts_integration.py +0 -0
  176. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_edge_cases_integration.py +0 -0
  177. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_entities_integration.py +0 -0
  178. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_forecasts_integration.py +0 -0
  179. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_grants_integration.py +0 -0
  180. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_itdashboard_integration.py +0 -0
  181. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_naics_integration.py +0 -0
  182. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_notices_integration.py +0 -0
  183. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_offices_integration.py +0 -0
  184. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_opportunities_integration.py +0 -0
  185. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_organizations_integration.py +0 -0
  186. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_otas_otidvs_integration.py +0 -0
  187. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_protests_integration.py +0 -0
  188. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_reference_data_integration.py +0 -0
  189. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_subawards_integration.py +0 -0
  190. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_vehicles_idvs_integration.py +0 -0
  191. {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/validation.py +0 -0
  192. {tango_python-1.5.1 → tango_python-1.7.0}/tests/production/__init__.py +0 -0
  193. {tango_python-1.5.1 → tango_python-1.7.0}/tests/production/conftest.py +0 -0
  194. {tango_python-1.5.1 → tango_python-1.7.0}/tests/production/test_production_smoke.py +0 -0
  195. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_api_parity.py +0 -0
  196. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_client.py +0 -0
  197. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_dibbs_exclusions_sbir.py +0 -0
  198. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_models.py +0 -0
  199. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_shape_coverage_gate.py +0 -0
  200. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_shapes.py +0 -0
  201. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_webhooks_cli.py +0 -0
  202. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_webhooks_receiver.py +0 -0
  203. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_webhooks_signing.py +0 -0
  204. {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_webhooks_simulate.py +0 -0
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.7.0] - 2026-09-23
11
+
12
+ ### Added
13
+ - **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.
14
+
15
+ 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.
16
+
17
+ 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.
18
+ - **`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.
19
+
20
+ ### Fixed
21
+ - **`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()`.
22
+ - **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.
23
+ - **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.
24
+
25
+ ### Changed
26
+ - **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.
27
+ - 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.
28
+ - 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.
29
+
30
+ ### Documentation
31
+ - `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.
32
+ - `docs/API_REFERENCE.md` documents the document-role fields on opportunity and notice attachments, and `delisted_at` and `meta.jurisdiction_declared` on SLED solicitations.
33
+ - 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.
34
+ - Added `alerts.contract_appeal.match` to the event-type listing in `docs/WEBHOOKS.md`.
35
+
36
+ ## [1.6.0] - 2026-09-10
37
+
38
+ ### Added
39
+ - **`attachments(extracted_text)` — SLED document bodies on the Small plan and above** (Tango API 4.25.1). Re-vendored the contract and regenerated the shape overlay, so `shape="...,attachments(name,extracted_text)"` now validates instead of being rejected client-side. Three properties are documented on the model and in `docs/API_REFERENCE.md`, because each is otherwise a support question: the leaf must be **named** (neither `SLED_OPPORTUNITIES_MINIMAL` nor `SLED_OPPORTUNITIES_COMPREHENSIVE` includes it, and `attachments(*)` does not carry it, because the API only resolves the body for a caller who asked); the **key is absent rather than null** when the text is not being served, so read it with `.get()`; and a **contested document never returns text at any plan**. Searching document text stays ungated on every plan and returns no fragment of it — buying the body does not change search.
40
+ - **State, local and education (SLED) procurement** (Tango API 4.25.0). Six methods over the new `/api/sled/` namespace: `list_sled_opportunities()`, `get_sled_opportunity()`, `list_sled_opportunity_revisions()`, `get_sled_coverage()`, `list_sled_forecasts()`, `get_sled_forecast()`, plus `SledOpportunity` / `SledOpportunityRevision` / `SledForecast` schemas and five `ShapeConfig` defaults. Every one of the API's 27 solicitation filters and 13 forecast filters is an explicit named parameter.
41
+
42
+ Four behaviors are documented on the methods themselves because each misleads a caller who assumes federal semantics. **Passing neither `status` nor `active` returns open solicitations only** — that default is the API's, and the SDK deliberately does not synthesize a `status` param, since doing so would make `active=False` unreachable (pinned by a test). **`status` is Tango-derived and refreshed every fifteen minutes**; the portal's own word is served as `source_status`, is frozen at last capture, and is not a liveness filter. **Category scheme tagging is mid-migration**, so `naics=` matches only the small tagged share and `category_code=` is the escape hatch. And **`meta.attachment_count` can be lower than `len(attachments)`**, because an auto-generated portal cover sheet is listed and flagged `is_generated_summary` but excluded from the count.
43
+
44
+ `SLED_REVISIONS_MINIMAL` omits `changes` on purpose: the per-field before/after needs a Small plan, so naming it by default would 403 a Free caller. `changed_fields` is in the default and available at every plan.
45
+ - **`previous_uii` on `list_itdashboard_investments()`.** The Tango API has accepted this filter for a while — it resolves a retired UII forward to whatever superseded it — and the SDK never picked it up, so it was a silent no-op for anyone who passed it through. Ungated, unlike its analytical neighbours on that endpoint. Found by the SDK's own filter-conformance check, not by SLED work.
46
+
47
+ ### Changed
48
+ - Re-vendored `contracts/filter_shape_contract.json` (schema_version 2, 48 resources) and regenerated `tango/shapes/generated_overlay.py` from it — 535 fields across 33 containers, 84 nested schemas. Besides the SLED expands this closes three entity gaps the older contract could not see: `relationships(confidence, verification_method)` and the whole `workforce(*)` expand were reachable on the API and rejected client-side by the SDK's schema.
49
+ - Baselined 14 reverse-shape-coverage gaps in `contracts/shape_coverage_baseline.json`. All 14 are nested sub-resource routes the newer contract publishes for the first time (`entities_contracts`, `idvs_awards`, `vehicles_orders`, and similar); none is SLED, and none is a regression — they are pre-existing SDK gaps that only became visible with the re-vendored contract. The gate reports 0 new drift.
50
+
51
+ ### Documentation
52
+ - New **State & Local (SLED)** section in `docs/API_REFERENCE.md` covering all six methods, both defaults that surprise people, and the five new `ShapeConfig` constants.
53
+ - Corrected `docs/WEBHOOKS.md`. It stated flatly that "nothing fires when a record merely lapses"; that is now false for exactly one query type. `alerts.sled_opportunity.match` **does** fire on a closing, because SLED liveness is a stored column a fifteen-minute sweep writes rather than a query-time derivation. Also documented that there is no `sled_forecast` query type and that SLED revisions and attachments are not separately alertable.
54
+
10
55
  ## [1.5.1] - 2026-08-10
11
56
 
12
57
  ### Documentation
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: tango-python
3
- Version: 1.5.1
3
+ Version: 1.7.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
@@ -18,9 +18,11 @@ Complete reference for all Tango Python SDK methods and functionality.
18
18
  - [Forecasts](#forecasts)
19
19
  - [Opportunities](#opportunities)
20
20
  - [Notices](#notices)
21
+ - [State \& Local (SLED)](#state--local-sled)
21
22
  - [Grants](#grants)
22
23
  - [GSA eLibrary Contracts](#gsa-elibrary-contracts)
23
24
  - [Protests](#protests)
25
+ - [Contract Appeals](#contract-appeals)
24
26
  - [Budget](#budget)
25
27
  - [Business Types](#business-types)
26
28
  - [NAICS](#naics)
@@ -924,6 +926,25 @@ for opp in opportunities.results:
924
926
  - `naics_code` - Industry code
925
927
  - `psc_code` - Product/service code
926
928
 
929
+ **Document roles — `attachments(doc_role, doc_role_alt)`:**
930
+
931
+ On a **Pro plan or above**, each attachment can say what the document is for.
932
+ `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.
933
+
934
+ ```python
935
+ opp = client.get_opportunity(
936
+ opportunity_id,
937
+ shape="opportunity_id,title,attachments(name,url,doc_role,doc_role_alt)",
938
+ )
939
+ for attachment in opp["attachments"]:
940
+ print(attachment["name"], attachment.get("doc_role"))
941
+ ```
942
+
943
+ - **You have to name them.** `attachments(*)` does not include either field, and no default shape does.
944
+ - **The keys are absent, not null**, on an attachment Tango has not classified. Use `attachment.get("doc_role")`.
945
+ - A Free-plan request that names them gets the response without them, plus an entry in `meta.upgrade_hints`.
946
+ - There is no filter on document role.
947
+
927
948
  ---
928
949
 
929
950
  ## Notices
@@ -1142,7 +1163,7 @@ contract = client.get_gsa_elibrary_contract("UUID_HERE")
1142
1163
 
1143
1164
  ## Protests
1144
1165
 
1145
- Bid protest records (GAO, COFC, etc.).
1166
+ 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).
1146
1167
 
1147
1168
  ### list_protests()
1148
1169
 
@@ -1165,20 +1186,22 @@ protests = client.list_protests(
1165
1186
  filed_date_before=None,
1166
1187
  decision_date_after=None,
1167
1188
  decision_date_before=None,
1189
+ naics_code=None,
1168
1190
  search=None,
1169
1191
  )
1170
1192
  ```
1171
1193
 
1172
1194
  **Filter Parameters:**
1173
- - `source_system` - Filter by source system (e.g., `"gao"`)
1195
+ - `source_system` - Filter by source system: `"gao"`, `"cofc"`, or `"sba_oha"`
1174
1196
  - `outcome` - Filter by outcome (e.g., `"Denied"`, `"Dismissed"`, `"Withdrawn"`, `"Sustained"`)
1175
1197
  - `case_type` - Filter by case type
1176
1198
  - `agency` - Filter by protested agency
1177
- - `case_number` - Filter by case number (e.g., `"b-423274"`)
1199
+ - `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)
1178
1200
  - `solicitation_number` - Filter by solicitation number
1179
1201
  - `protester` - Search by protester name
1180
1202
  - `filed_date_after` / `filed_date_before` - Filed date range
1181
1203
  - `decision_date_after` / `decision_date_before` - Decision date range
1204
+ - `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
1182
1205
  - `search` - Full-text search
1183
1206
 
1184
1207
  **Returns:** [PaginatedResponse](#paginatedresponse) with protest dictionaries
@@ -1213,6 +1236,284 @@ protest = client.get_protest(
1213
1236
 
1214
1237
  ---
1215
1238
 
1239
+ ## Contract Appeals
1240
+
1241
+ Contract Disputes Act appeal decisions from the Civilian Board of Contract Appeals (CBCA) and the Armed Services Board of Contract Appeals (ASBCA).
1242
+
1243
+ **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.
1244
+
1245
+ 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.
1246
+
1247
+ ### list_contract_appeals()
1248
+
1249
+ List appeal decisions with filtering and shaping.
1250
+
1251
+ ```python
1252
+ appeals = client.list_contract_appeals(
1253
+ page=1,
1254
+ limit=25,
1255
+ shape=ShapeConfig.CONTRACT_APPEALS_MINIMAL,
1256
+ # Filter parameters (all optional)
1257
+ board=None,
1258
+ docket=None,
1259
+ appellant=None,
1260
+ judge=None,
1261
+ decision_type=None,
1262
+ decision_date_after=None,
1263
+ decision_date_before=None,
1264
+ listed=None,
1265
+ document_id=None,
1266
+ search=None,
1267
+ ordering=None,
1268
+ )
1269
+ ```
1270
+
1271
+ **Filter Parameters:**
1272
+ - `board` - `"cbca"` or `"asbca"`; OR both with `"cbca|asbca"`
1273
+ - `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 `|`
1274
+ - `appellant` - Case-insensitive substring match on the appellant's name (min 2 characters)
1275
+ - `judge` - Case-insensitive exact match on the judge as the listing names them
1276
+ - `decision_type` - CBCA only: `"Decision"`, `"Dismissal"`, `"Order"`, `"Full Board Order"`, or the listing's own text. ASBCA rows carry no type
1277
+ - `decision_date_after` / `decision_date_before` - Decision date range (`YYYY-MM-DD`)
1278
+ - `listed` - Whether the board's newest listing still carries the decision
1279
+ - `document_id` - The board's own document id
1280
+ - `search` - Ranked full-text search over the appellant and the full decision text (min 2 characters); wrap in double quotes for a phrase
1281
+ - `ordering` - `decision_date` (the default, as `-decision_date`), `appellant`, `first_listed_at` or `rank`. `rank` requires a non-empty `search`
1282
+
1283
+ **Returns:** [PaginatedResponse](#paginatedresponse) with appeal-decision dictionaries
1284
+
1285
+ **Example:**
1286
+ ```python
1287
+ appeals = client.list_contract_appeals(
1288
+ board="asbca",
1289
+ search="differing site conditions",
1290
+ decision_date_after="2025-01-01",
1291
+ ordering="rank",
1292
+ limit=25,
1293
+ )
1294
+
1295
+ for appeal in appeals.results:
1296
+ dockets = ", ".join(appeal["docket_numbers"])
1297
+ print(f"{appeal['board'].upper()} {dockets}: {appeal['appellant']} ({appeal['decision_date']})")
1298
+ ```
1299
+
1300
+ ### get_contract_appeal()
1301
+
1302
+ Get a single decision by `uuid`.
1303
+
1304
+ ```python
1305
+ appeal = client.get_contract_appeal(
1306
+ "DECISION_UUID",
1307
+ shape=ShapeConfig.CONTRACT_APPEALS_COMPREHENSIVE,
1308
+ )
1309
+ ```
1310
+
1311
+ **Notes:**
1312
+ - `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.
1313
+ - **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.
1314
+ - **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.
1315
+
1316
+ ---
1317
+
1318
+ ## State & Local (SLED)
1319
+
1320
+ 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.
1321
+
1322
+ This data does not join to the federal data. There is no UEI, no PIID, no agency-hierarchy key and no NAICS/PSC crosswalk; `organization(*)` here is three strings, not the federal 7-key office payload.
1323
+
1324
+ ### list_sled_opportunities()
1325
+
1326
+ List SLED solicitations with filtering and shaping.
1327
+
1328
+ ```python
1329
+ solicitations = client.list_sled_opportunities(
1330
+ page=1,
1331
+ limit=25,
1332
+ shape=ShapeConfig.SLED_OPPORTUNITIES_MINIMAL,
1333
+ # Filter parameters (all optional)
1334
+ state=None,
1335
+ jurisdiction=None,
1336
+ status=None,
1337
+ active=None,
1338
+ agency=None,
1339
+ solicitation_number=None,
1340
+ solicitation_type=None,
1341
+ has_documents=None,
1342
+ revision_kind=None,
1343
+ naics=None,
1344
+ nigp=None,
1345
+ unspsc=None,
1346
+ category=None,
1347
+ category_code=None,
1348
+ posted_after=None,
1349
+ posted_before=None,
1350
+ response_deadline_after=None,
1351
+ response_deadline_before=None,
1352
+ first_seen_after=None,
1353
+ first_seen_before=None,
1354
+ change_seen_after=None,
1355
+ modified_after=None,
1356
+ modified_before=None,
1357
+ search=None,
1358
+ ordering=None,
1359
+ )
1360
+ ```
1361
+
1362
+ **Two defaults worth knowing before your first call:**
1363
+
1364
+ - **Passing neither `status` nor `active` returns open solicitations only.** Only about a fifth of the corpus is open, and a portal drops a closed solicitation rather than restating it, so the API defaults the list to `status=open`. Pass an explicit `status` to page the whole corpus. `status="unknown"` (standing rosters, dateless RFIs) is hidden by that default — reach it with `status="open|unknown"`. `get_sled_opportunity()` returns the solicitation whatever its status.
1365
+ - **`status` is Tango's answer, not the portal's.** It is derived from the portal's word, the deadline and the clock, and refreshed every fifteen minutes. The portal's own word is served as `source_status` and is frozen at last capture — most of what it calls open already has a passed deadline. Never filter liveness on it.
1366
+
1367
+ **Filter Parameters:**
1368
+ - `state` - Two-letter state or territory code. Multi-value: `"TX|OK"`
1369
+ - `jurisdiction` - `"state"`, `"local"`, `"education"`, or `"unknown"` for aggregator rows that cannot tell state from local
1370
+ - `status` - `"open"`, `"closed"`, `"awarded"`, `"cancelled"`, `"unknown"`
1371
+ - `active` - Sugar for federal-shaped callers: `True` is `status="open"`; `False` is its complement, so it includes `unknown`
1372
+ - `agency` - Substring match on the buyer's published text (min 2 characters); no code resolution behind it
1373
+ - `solicitation_number` - The number a human would quote; null on roughly a third of the corpus
1374
+ - `solicitation_type` - `"rfp"`, `"ifb"`, `"rfq"`, `"rfi"`, `"itb"`, `"sole_source"`, `"grant"`, `"other"`. `"null"` (portal states no type) is a distinct answer from `"other"`
1375
+ - `has_documents` - Whether the solicitation advertises at least one document
1376
+ - `revision_kind` - Kind of the most recent substantive revision
1377
+ - `naics` / `nigp` / `unspsc` / `category` - Exact match within one category scheme. **`naics` is thin on purpose**: scheme tagging is mid-migration, so only a small share of entries are tagged NAICS
1378
+ - `category_code` - Match a code under ANY scheme, including the untagged pre-migration strings. The escape hatch when a scheme-specific filter returns less than expected
1379
+ - `posted_after` / `posted_before` - Posted-date range
1380
+ - `response_deadline_after` / `response_deadline_before` - Deadline range
1381
+ - `first_seen_after` / `first_seen_before` - When Tango first observed it. The polling primitive
1382
+ - `change_seen_after` - When Tango *observed* the last substantive change. A scrape date, not an amendment date
1383
+ - `modified_after` / `modified_before` - When the Tango row last changed
1384
+ - `platform` / `native_id` / `external_id` - Support filters for reproducing a record with us. Not in any response shape and not stable values
1385
+ - `search` - Ranked full-text search over title, agency, identifiers, category labels and description, widened by the solicitations whose *attachment text* matched (min 2 characters)
1386
+ - `ordering` - `rank`, `response_deadline`, `posted_date`, `first_seen_at`, `last_seen_at`, `last_change_seen_at`, `modified`. `rank` requires a non-empty `search`
1387
+
1388
+ **Returns:** [PaginatedResponse](#paginatedresponse) with solicitation dictionaries
1389
+
1390
+ **Example:**
1391
+ ```python
1392
+ # Open Texas solicitations closing this month, newest first
1393
+ page = client.list_sled_opportunities(
1394
+ state="TX",
1395
+ response_deadline_before="2026-10-01",
1396
+ ordering="response_deadline",
1397
+ limit=25,
1398
+ )
1399
+
1400
+ for row in page.results:
1401
+ print(f"{row['state']} {row.get('solicitation_number') or '—'}: {row['title']}")
1402
+
1403
+ # Full-text search puts the matching passage on each row that matched on its body
1404
+ hits = client.list_sled_opportunities(
1405
+ search="environmental mitigation",
1406
+ shape="opportunity_id,title,state,snippet,response_deadline",
1407
+ )
1408
+ ```
1409
+
1410
+ `snippet` is present only under `search=`, and only on rows that matched on their description — a title-or-agency match honestly carries none. Attachment matching contributes ids only: a caller learns *that* a document matched, never what it said.
1411
+
1412
+ ### get_sled_opportunity()
1413
+
1414
+ Get a single solicitation by `opportunity_id`, whatever its status.
1415
+
1416
+ ```python
1417
+ row = client.get_sled_opportunity(
1418
+ "OPPORTUNITY_UUID",
1419
+ shape="opportunity_id,title,status,attachments(*),revisions(*)",
1420
+ )
1421
+ ```
1422
+
1423
+ **Notes:**
1424
+ - `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".
1425
+ - `size_bytes` and `char_count` only mean something as a pair — 3 MB that yielded no characters is a scan awaiting OCR.
1426
+ - `raw(*)` needs a Small plan or above, and is explicitly unstable: its shape varies by portal platform.
1427
+ - `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.
1428
+ - `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.
1429
+
1430
+ **Reading a document body — `attachments(extracted_text)`:**
1431
+
1432
+ ```python
1433
+ row = client.get_sled_opportunity(
1434
+ opportunity_id,
1435
+ shape="opportunity_id,attachments(name,size_bytes,extracted_text)",
1436
+ )
1437
+ ```
1438
+
1439
+ Needs a **Small plan or above**, and Tango API 4.25.1 or later (`client.get_version()` reports what you are calling). Three rules:
1440
+
1441
+ - **You have to name it.** `attachments(*)` does not carry the body, and neither default shape names it — the API only resolves it for a caller who asked, so a default would make every detail fetch pay for a document nobody wanted to read.
1442
+ - **The key is absent, not null**, whenever the text is not being served to you: below Small (where it is withheld and named in `meta.upgrade_hints`), on a contested document, or where the text cannot be resolved. Use `attachment.get("extracted_text")`.
1443
+ - **A contested document never returns text**, at any plan — its stored bytes disagree with what the record advertised, so its text is not reliably that record's.
1444
+
1445
+ Searching document text and reading it are separate things. `search=` matches inside attachment text on **every plan** and returns no fragment of it; `extracted_text` is a per-record read on Small and above. Buying the body does not change search.
1446
+
1447
+ ### list_sled_opportunity_revisions()
1448
+
1449
+ List one solicitation's observed revision history.
1450
+
1451
+ ```python
1452
+ revisions = client.list_sled_opportunity_revisions(
1453
+ "OPPORTUNITY_UUID",
1454
+ kind=None,
1455
+ source_declared=None,
1456
+ observed_after=None,
1457
+ observed_before=None,
1458
+ )
1459
+ ```
1460
+
1461
+ **Notes:**
1462
+ - `observed_at` is the scrape that saw the change, not the date the agency made it. No state portal emits amendment notices, so `kind` is Tango's inference from the diff on about 95% of revisions, resolution is that state's crawl cadence, and history starts when Tango began reading the jurisdiction.
1463
+ - Unlike the `revisions(*)` expand, this route serves `enrichment` rows — Tango's own detail fetch filling in coverage rather than an agency amendment. Pass `kind="enrichment"` for only those.
1464
+ - `changes` (the per-field before and after) needs a Small plan; it is left out of the default shape for that reason. `changed_fields` names what moved at every plan.
1465
+
1466
+ ### get_sled_coverage()
1467
+
1468
+ Get the per-state coverage rollup. Takes no parameters; not shaped or paginated.
1469
+
1470
+ ```python
1471
+ coverage = client.get_sled_coverage()
1472
+ print(coverage["totals"])
1473
+ for row in coverage["states"]:
1474
+ print(row["state"], row["total_count"], row["by_status"], row["last_change_observed_at"])
1475
+ ```
1476
+
1477
+ **Call this before treating a per-state count as market size.** A thin result for a state is at least as likely to be a portal Tango does not read as a quiet market, and that is the ambiguity this endpoint exists to resolve. Every state row carries all five status buckets whether or not they have rows, so a total and two buckets never invite subtraction.
1478
+
1479
+ ### list_sled_forecasts()
1480
+
1481
+ List planned state procurements.
1482
+
1483
+ ```python
1484
+ forecasts = client.list_sled_forecasts(
1485
+ state=None,
1486
+ agency=None,
1487
+ procurement_category=None,
1488
+ procurement_method=None,
1489
+ contract_number=None,
1490
+ incumbent_name=None,
1491
+ advertisement_after=None,
1492
+ advertisement_before=None,
1493
+ first_seen_after=None,
1494
+ first_seen_before=None,
1495
+ modified_after=None,
1496
+ modified_before=None,
1497
+ search=None,
1498
+ ordering=None,
1499
+ )
1500
+ ```
1501
+
1502
+ **Notes:**
1503
+ - **Forecasts carry no liveness at all** — no deadline to have passed, so there is no `status` field, no `active` filter, and no open-only default. Currency is the caller's call from `estimated_advertisement_date`.
1504
+ - `estimated_advertisement_date` is the **start of the published quarter**, not a posting date. `estimated_advertisement_raw` keeps the portal's own words (`"Q3 (Jan.-March 2027)"`), and a large share of rows publish no quarter at all.
1505
+ - `estimated_value(min,max,raw)` is parsed from a free-text award band at serve time. **A band naming one number is a floor, so `max` is `None`** — never read a missing `max` as an unbounded ceiling. `raw` is always there to check the parse against.
1506
+ - `incumbent_name` is published text, not a resolved Tango entity.
1507
+ - `ordering`: `rank`, `estimated_advertisement_date`, `first_seen_at`, `last_seen_at`, `modified`.
1508
+
1509
+ ### get_sled_forecast()
1510
+
1511
+ ```python
1512
+ forecast = client.get_sled_forecast("FORECAST_UUID")
1513
+ ```
1514
+
1515
+ ---
1516
+
1216
1517
  ## Budget
1217
1518
 
1218
1519
  Federal account × fiscal year budget rollups, covering the full budget lifecycle (requested → enacted → apportioned → obligated → outlayed), pre-computed ratios and trends, the contract / assistance / unlinked breakdown, and request-vs-actual spend.
@@ -2008,10 +2309,17 @@ entity = client.get_entity("UEI_KEY", shape=ShapeConfig.ENTITIES_COMPREHENSIVE)
2008
2309
  | `SUBAWARDS_MINIMAL` | `list_subawards` | award_key, prime_recipient(uei,display_name), subaward_recipient(uei,display_name) |
2009
2310
  | `GSA_ELIBRARY_CONTRACTS_MINIMAL` | `list_gsa_elibrary_contracts` | uuid, contract_number, schedule, recipient(display_name,uei), idv(key,award_date) |
2010
2311
  | `PROTESTS_MINIMAL` | `list_protests` | case_id, case_number, title, source_system, outcome, filed_date |
2312
+ | `CONTRACT_APPEALS_MINIMAL` | `list_contract_appeals` | uuid, board, docket_numbers, decision_date, appellant, judge, decision_type, url |
2313
+ | `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) |
2011
2314
  | `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 |
2012
2315
  | `VEHICLE_ORDERS_MINIMAL` | `list_vehicle_orders` | key, piid, award_date, recipient(display_name,uei), total_contract_value, obligated |
2013
2316
  | `ITDASHBOARD_INVESTMENTS_MINIMAL` | `list_itdashboard_investments` | Minimal IT Dashboard investment fields |
2014
2317
  | `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 |
2318
+ | `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) |
2319
+ | `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 |
2320
+ | `SLED_REVISIONS_MINIMAL` | `list_sled_opportunity_revisions` | observed_at, sequence, kind, changed_fields, source_declared (omits `changes`, which needs a Small plan) |
2321
+ | `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(*) |
2322
+ | `SLED_FORECASTS_COMPREHENSIVE` | `get_sled_forecast` | Full forecast with description, contract_term, mbe_dbe_goal, delivery_location, and organization / contact / estimated_value |
2015
2323
 
2016
2324
  All predefined shapes are validated at SDK release time (see [Developer Guide](DEVELOPERS.md#sdk-conformance-maintainers)). For custom shapes, see the [Shaping Guide](SHAPES.md).
2017
2325
 
@@ -87,6 +87,8 @@ tango webhooks list-event-types
87
87
  # alerts.exclusion.match SAM.gov exclusion matched a saved alert
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
+ # 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
90
92
  ```
91
93
 
92
94
  This list is served by the API, so it is always current — the SDK does not hardcode
@@ -97,11 +99,20 @@ Two things that surprise people:
97
99
 
98
100
  - **DIBBS awards are not alertable.** There is no `alerts.dibbs_award.match`;
99
101
  only RFQs and RFPs emit alerts.
100
- - **Nothing fires when a record merely lapses.** An exclusion reaching its
101
- termination date, or an RFQ/RFP passing its close date, emits no event.
102
- Alerts fire on a record matching your saved search, not on the passage of
103
- time. Poll with `list_exclusions(active=...)` / `list_dibbs_rfqs(open=...)`
104
- if you need to observe expiry.
102
+ - **Nothing fires when a record merely lapses — except SLED.** An exclusion
103
+ reaching its termination date, or an RFQ/RFP passing its close date, emits no
104
+ event: open/closed and in-force are derived at query time, so no stored row
105
+ changes. Poll with `list_exclusions(active=...)` /
106
+ `list_dibbs_rfqs(open=...)` if you need to observe expiry.
107
+
108
+ `sled_opportunity` is the exception, and the only one. A SLED solicitation's
109
+ liveness is a stored `status` column that Tango recomputes every fifteen
110
+ minutes rather than deriving per request, so a deadline passing **is** a write
111
+ and `alerts.sled_opportunity.match` can follow it. There is no
112
+ `sled_forecast` query type — a forecast has no deadline, so nothing
113
+ transitions — and SLED revisions and attachments are not separately
114
+ alertable: subscribe to `sled_opportunity` and filter on
115
+ `change_seen_after` or `revision_kind`.
105
116
 
106
117
  ### 2. See what a payload looks like
107
118
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tango-python"
7
- version = "1.5.1"
7
+ version = "1.7.0"
8
8
  description = "Python SDK for the Tango API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -74,7 +74,10 @@ RESOURCE_TO_METHOD: dict[str, str | None] = {
74
74
  "exclusions": "list_exclusions",
75
75
  "sbir/topics": "list_sbir_topics",
76
76
  "sbir/solicitations": "list_sbir_solicitations",
77
+ "sled/opportunities": "list_sled_opportunities",
78
+ "sled/forecasts": "list_sled_forecasts",
77
79
  "protests": "list_protests",
80
+ "contract_appeals": "list_contract_appeals",
78
81
  "offices": "list_offices",
79
82
  "psc": "list_psc",
80
83
  "mas_sins": "list_mas_sins",
@@ -68,6 +68,7 @@ RESOURCE_TO_MODEL: dict[str, str] = {
68
68
  "budget/accounts": "BudgetAccount",
69
69
  "budget_accounts": "BudgetAccount",
70
70
  "protests": "Protest",
71
+ "contract_appeals": "ContractAppeal",
71
72
  "offices": "Office",
72
73
  "assistance_listings": "AssistanceListing",
73
74
  "business_types": "BusinessType",
@@ -79,6 +80,8 @@ RESOURCE_TO_MODEL: dict[str, str] = {
79
80
  "exclusions": "Exclusion",
80
81
  "sbir/topics": "SbirTopic",
81
82
  "sbir/solicitations": "SbirSolicitation",
83
+ "sled/opportunities": "SledOpportunity",
84
+ "sled/forecasts": "SledForecast",
82
85
  "mas_sins": "MasSin",
83
86
  "events": "Event",
84
87
  "news": "News",
@@ -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,6 +10,7 @@ from .exceptions import (
10
10
  )
11
11
  from .models import (
12
12
  BudgetAccount,
13
+ ContractAppeal,
13
14
  DibbsAward,
14
15
  DibbsRfp,
15
16
  DibbsRfq,
@@ -24,6 +25,9 @@ from .models import (
24
25
  SbirTopic,
25
26
  SearchFilters,
26
27
  ShapeConfig,
28
+ SledForecast,
29
+ SledOpportunity,
30
+ SledOpportunityRevision,
27
31
  ValidateResult,
28
32
  Vehicle,
29
33
  VehicleMetrics,
@@ -50,7 +54,7 @@ from .webhooks import (
50
54
  )
51
55
  from .webhooks.receiver import Delivery, WebhookReceiver
52
56
 
53
- __version__ = "1.5.1"
57
+ __version__ = "1.7.0"
54
58
  __all__ = [
55
59
  "TangoClient",
56
60
  "TangoAPIError",
@@ -62,6 +66,7 @@ __all__ = [
62
66
  "ResolveCandidate",
63
67
  "ResolveResult",
64
68
  "BudgetAccount",
69
+ "ContractAppeal",
65
70
  "DibbsAward",
66
71
  "DibbsRfp",
67
72
  "DibbsRfq",
@@ -73,6 +78,9 @@ __all__ = [
73
78
  "PaginatedResponse",
74
79
  "SearchFilters",
75
80
  "ShapeConfig",
81
+ "SledForecast",
82
+ "SledOpportunity",
83
+ "SledOpportunityRevision",
76
84
  "ValidateResult",
77
85
  "Vehicle",
78
86
  "VehicleMetrics",