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.
- {tango_python-1.5.1 → tango_python-1.7.0}/CHANGELOG.md +45 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/PKG-INFO +2 -2
- {tango_python-1.5.1 → tango_python-1.7.0}/docs/API_REFERENCE.md +311 -3
- {tango_python-1.5.1 → tango_python-1.7.0}/docs/WEBHOOKS.md +16 -5
- {tango_python-1.5.1 → tango_python-1.7.0}/pyproject.toml +1 -1
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/check_filter_shape_conformance.py +3 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/check_shape_coverage.py +3 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/generate_shape_overlay.py +18 -3
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/__init__.py +9 -1
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/client.py +595 -5
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/models.py +244 -2
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/explicit_schemas.py +38 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/generated_overlay.py +547 -48
- tango_python-1.7.0/tests/test_contract_appeals.py +213 -0
- tango_python-1.7.0/tests/test_opportunity_attachments.py +72 -0
- tango_python-1.7.0/tests/test_sled.py +342 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/.gitignore +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/LICENSE +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/README.md +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/docs/CLIENT.md +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/docs/DYNAMIC_MODELS.md +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/docs/ERRORS.md +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/docs/PAGINATION.md +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/docs/SHAPES.md +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/README.md +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/fetch_api_schema.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/generate_schemas_from_api.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/pr_review.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/probe_shape_types.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/refresh_contract.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/smoke_api_parity.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/scripts/test_production.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/exceptions.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/__init__.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/factory.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/generator.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/models.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/parser.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/schema.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/shapes/types.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/__init__.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/cli.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/receiver.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/signing.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tango/webhooks/simulate.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/__init__.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
- {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
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_budget_flows +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
- {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
- {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
- {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
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
- {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
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
- {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
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_code +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_name_text +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating_max +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_performance_risk +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_type_of_investment +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_updated_time_range +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_funding_and_cio_evaluation_expansions +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_get_itdashboard_investment_by_uii +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_itdashboard_pagination +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_search +0 -0
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[default-None] +0 -0
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
- {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
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
- {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
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
- {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
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_with_metrics_expansion +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_orders_uses_default_shape +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_with_ordering +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/conftest.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/README.md +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/__init__.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/conftest.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_agencies_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_contracts_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_edge_cases_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_entities_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_forecasts_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_grants_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_itdashboard_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_naics_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_notices_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_offices_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_opportunities_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_organizations_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_otas_otidvs_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_protests_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_reference_data_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_subawards_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/test_vehicles_idvs_integration.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/integration/validation.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/production/__init__.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/production/conftest.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/production/test_production_smoke.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_api_parity.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_client.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_dibbs_exclusions_sbir.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_models.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_shape_coverage_gate.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_shapes.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_webhooks_cli.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_webhooks_receiver.py +0 -0
- {tango_python-1.5.1 → tango_python-1.7.0}/tests/test_webhooks_signing.py +0 -0
- {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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: tango-python
|
|
3
|
-
Version: 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
|
|
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
|
|
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
|
|
101
|
-
termination date, or an RFQ/RFP passing its close date, emits no
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|