tango-python 1.3.0__tar.gz → 1.5.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.3.0 → tango_python-1.5.0}/CHANGELOG.md +35 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/PKG-INFO +30 -2
- {tango_python-1.3.0 → tango_python-1.5.0}/README.md +29 -1
- {tango_python-1.3.0 → tango_python-1.5.0}/pyproject.toml +1 -1
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/check_filter_shape_conformance.py +38 -13
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/check_shape_coverage.py +35 -1
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/__init__.py +1 -1
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/client.py +59 -1
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/models.py +93 -1
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/shapes/generated_overlay.py +603 -45
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/test_client.py +225 -0
- tango_python-1.5.0/tests/test_shape_coverage_gate.py +108 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/.github/workflows/docs-dispatch.yml +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/.github/workflows/lint.yml +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/.github/workflows/publish.yml +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/.github/workflows/test.yml +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/.gitignore +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/LICENSE +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/docs/API_REFERENCE.md +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/docs/CLIENT.md +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/docs/DYNAMIC_MODELS.md +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/docs/ERRORS.md +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/docs/PAGINATION.md +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/docs/SHAPES.md +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/docs/WEBHOOKS.md +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/README.md +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/fetch_api_schema.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/generate_schemas_from_api.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/generate_shape_overlay.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/pr_review.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/probe_shape_types.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/refresh_contract.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/smoke_api_parity.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/scripts/test_production.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/exceptions.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/shapes/__init__.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/shapes/explicit_schemas.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/shapes/factory.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/shapes/generator.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/shapes/models.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/shapes/parser.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/shapes/schema.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/shapes/types.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/webhooks/__init__.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/webhooks/cli.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/webhooks/receiver.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/webhooks/signing.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tango/webhooks/simulate.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/__init__.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_budget_flows +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[custom-uei,legal_business_name,cage_code] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[custom-id,title,anticipated_award_date] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[custom-grant_id,title,opportunity_number] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[minimal-grant_id,opportunity_number,title,status(-),agency_code] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_code +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_name_text +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating_max +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_performance_risk +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_type_of_investment +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_updated_time_range +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_funding_and_cio_evaluation_expansions +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_get_itdashboard_investment_by_uii +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_itdashboard_pagination +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_search +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[custom-uii,agency_name,investment_title,updated_time] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[default-None] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[custom-notice_id,title,solicitation_number] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[custom-opportunity_id,title,solicitation_number] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[custom-case_id,title,source_system,outcome] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[ultra_minimal-key,piid,recipient(display_name),total_contract_value] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.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.3.0 → tango_python-1.5.0}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_with_metrics_expansion +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_orders_uses_default_shape +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_with_ordering +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/conftest.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/README.md +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/__init__.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/conftest.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_agencies_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_contracts_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_edge_cases_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_entities_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_forecasts_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_grants_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_itdashboard_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_naics_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_notices_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_offices_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_opportunities_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_organizations_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_otas_otidvs_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_protests_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_reference_data_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_subawards_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/test_vehicles_idvs_integration.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/integration/validation.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/production/__init__.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/production/conftest.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/production/test_production_smoke.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/test_api_parity.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/test_dibbs_exclusions_sbir.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/test_models.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/test_shapes.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/test_webhooks_cli.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/test_webhooks_receiver.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/test_webhooks_signing.py +0 -0
- {tango_python-1.3.0 → tango_python-1.5.0}/tests/test_webhooks_simulate.py +0 -0
|
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.5.0] - 2026-08-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`relationships(type, source)` on entities.** Tango API 4.20.0 added two keys to each entry in the entity `relationships` expand: `type`, the stable relationship-type code (`prime_sub`, `parent_subsidiary`, `ultimate_parent`, `predecessor`), and `source`, where the tie came from (`sam`, `subawards`). Both are shape-selectable, so `shape="uei,relationships(type,source,uei)"` now resolves against the SDK's schema. Re-vendored the contract and regenerated the shape overlay; the coverage gate reports 0 gaps.
|
|
14
|
+
- `PaginatedResponse.meta` now carries the response-level `meta` block the API returns, with three accessors over it: `unresolved_agency_tokens` (agency tokens that matched nothing, per filter), `resolved_agencies` (the organization each token *did* match), and `agency_warnings` (the API's human-readable notes). Agency values resolve fuzzily, so a token can match an organization the caller did not intend and quietly scope the query to that subtree — and a token that resolves to nothing was previously dropped with no signal at all. Both cases were indistinguishable from "no such records exist". The API now reports both; until now the SDK read the envelope key-by-key and dropped `meta` on the floor, so SDK users were the one group that could not see it. `resolved_agencies` is the accessor that matters for the wrong-organization case: nothing is dropped there, so an unresolved-token check cannot detect it. All accessors return empty rather than raising when `meta` is absent (most responses) or malformed.
|
|
15
|
+
- A fully-unresolvable agency filter now raises `TangoValidationError` naming the offending value instead of returning an empty page. This needed no SDK change — the existing 400 handler already reads the API's `error` key — but it is new behavior for callers of `list_subawards()`, `list_opportunities()`, `list_notices()`, and `list_vehicles()`, which previously returned an empty result set. Contracts, IDVs, OTAs, and OTIDVs already behaved this way.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **The `relation` value vocabulary changed upstream — match on `type` instead.** In Tango API 4.20.0 the `relation` label stopped collapsing to `affiliate` for subcontracting and corporate-succession ties and now names the partner's role: `subcontractor` / `prime`, `predecessor` / `successor`, and `descendant` (rather than `child`) on the far side of an ultimate-parent tie. `affiliate` survives only as a fallback for a type the API doesn't recognize. This affects the large majority of relationship entries.
|
|
19
|
+
|
|
20
|
+
No SDK code change is required — `relation` was and remains a `str`. But if you have application code branching on the string `affiliate`, switch it to `type`, which is stable and won't churn again. See the [entities data dictionary](https://docs.makegov.com/data-dictionary/entities/#relationships) for the full vocabulary table.
|
|
21
|
+
|
|
22
|
+
### Notes
|
|
23
|
+
- `PaginatedResponse.page_metadata` is documented as always `None`: the API has never emitted a `page_metadata` key, so the field has only ever read a value that does not exist. It is retained so existing attribute access keeps working. Use `meta`.
|
|
24
|
+
|
|
25
|
+
## [1.4.0] - 2026-07-20
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `list_protests()` now accepts `naics_code`, filtering protests by the solicitation's NAICS code. The API has always supported it (verified live: 15,027 protests unfiltered, 12 for `541519`, 2 for `336411`), but `protests` was absent from the conformance checker's resource-to-method map, so the gap was never reported.
|
|
29
|
+
- `list_psc()` now accepts `has_awards`, restricting results to codes with contract award history (2,526 PSC codes unfiltered, 2,243 with awards). `False` is sent explicitly rather than dropped, since the API distinguishes the two. This param has always worked but was invisible to the contract until [makegov/tango#2948](https://github.com/makegov/tango/pull/2948) added the `view_handled_params` declaration for params a viewset reads straight from `query_params`. That PR also fixed a truthiness bug where `has_awards=false` filtered identically to `true`, and is deployed — verified live at 2,526 for `False` and 2,243 for `True`.
|
|
30
|
+
|
|
31
|
+
One caveat that is not an SDK issue: `/api/psc/` responses are cached for 30 days per query-param key, so any `?has_awards=` key requested *before* that deploy still serves the old awards-only result until its entry expires or is invalidated.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
- **The reverse shape-coverage gate was blind to seven resources, and the SDK's schemas had silently drifted behind four of them.** The gate skipped any resource whose contract shape tree was falsy (`if not shape: continue`). Tango published `"shape": null` for `entities`, `opportunities`, `notices`, `protests`, `itdashboard`, `events`, and `news` — five of them because its contract generator crashed on tier-aware viewsets ([makegov/tango#2944](https://github.com/makegov/tango/pull/2944)) — so the gate reported full coverage while checking nothing for them. Re-vendoring the fixed contract surfaced **69 real gaps** (36 fields, 20 expands, 13 flattened expands) across entities, notices, protests, and itdashboard, all now closed by a regenerated overlay.
|
|
35
|
+
|
|
36
|
+
The user-visible consequence was worse than missing schema entries. Because `ShapeParser` validates client-side, the SDK *rejected* 13 fields the API accepts, before ever issuing a request — `list_entities(shape="display_name")` raised `ShapeValidationError` against an endpoint that returns 200 for it. Affected: `Entity`'s `display_name`, `entity_type`, `entity_structure`, `organization_structure`, `profit_structure`, `purpose_of_registration`, `country_of_incorporation`, `past_performance`, `registered`, `sba_business_types`, and `Protest`'s `decisions`, `resolved_agency`, `resolved_protester`. All 13 now validate. Note that `registered` and `sba_business_types` regressed *in 1.3.0* — they validated under 1.1.1 — so the overlay generation had narrowed `Entity` while nothing was watching.
|
|
37
|
+
|
|
38
|
+
The gate no longer skips silently: a resource declaring `shape_supported: true` with no shape tree is now a hard finding (`contract_missing_shape`) reported as an upstream contract defect rather than an SDK one. Contracts predating `schema_version: 2` omit the key, where a null tree stays genuinely ambiguous and skipping remains correct.
|
|
39
|
+
- **Seven resources with working SDK methods were never conformance-checked at all.** `RESOURCE_TO_METHOD` omitted `protests`, `psc`, `mas_sins`, `departments`, `business_types`, and `assistance_listings` entirely, and mapped `offices` to `None` as "not yet implemented" even though `list_offices` exists. Their filter coverage had never been validated. Wiring them up found one real gap (`protests.naics_code`, above); the other six were already complete. `events` and `news` stay mapped to `None` — genuinely absent from the SDK, now with a note explaining that `list_webhook_event_types` is unrelated.
|
|
40
|
+
- **`refresh_contract.py` + `probe_shape_types.py` re-run against the corrected contract.** `contracts/observed_shape_types.json` gained live-sampled types for the five newly-visible resources (entities alone contributes 136 observed paths), so the regenerated overlay resolves their types from real API responses rather than name heuristics.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- `CONTRACT_OMITTED_PARAMS` now warns when an entry is no longer needed, mirroring how baseline entries are burned down, and the list is now **empty** — which is the goal state. It briefly held `mas_sins: search`, added because the API applies it (330 results unfiltered, 0 for a nonsense term, 32 for `office`) while the contract recorded `filter_params: []`, so the checker would otherwise have flagged a working parameter as a silent no-op and invited its removal. Both that entry and `budget/accounts: search` were retired by the new warning once [makegov/tango#2944](https://github.com/makegov/tango/pull/2944) and [#2948](https://github.com/makegov/tango/pull/2948) taught the contract generator to publish them. The carve-out mechanism stays for the next such case.
|
|
44
|
+
|
|
10
45
|
## [1.3.0] - 2026-07-19
|
|
11
46
|
|
|
12
47
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tango-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.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
|
|
@@ -191,9 +191,37 @@ contracts = client.list_contracts(
|
|
|
191
191
|
- `expiring_gte`, `expiring_lte` - Contract expiration date range
|
|
192
192
|
|
|
193
193
|
**Party Filters:**
|
|
194
|
-
- `awarding_agency`, `funding_agency` - Agency codes
|
|
194
|
+
- `awarding_agency`, `funding_agency` - Agency codes, names, abbreviations, or organization UUIDs. Multi-value OR via `|`.
|
|
195
195
|
- `recipient_name`, `recipient_uei` - Vendor/recipient filters
|
|
196
196
|
|
|
197
|
+
### Checking how agency filters resolved
|
|
198
|
+
|
|
199
|
+
Agency values are resolved fuzzily, so a token can match an organization you did not
|
|
200
|
+
intend — which silently scopes the query to that organization's subtree. A short result
|
|
201
|
+
set is then indistinguishable from "no such records exist". Responses expose what
|
|
202
|
+
actually happened:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
response = client.list_contracts(awarding_agency="HUD|HUDD")
|
|
206
|
+
|
|
207
|
+
# Tokens that matched nothing and were ignored.
|
|
208
|
+
if response.unresolved_agency_tokens:
|
|
209
|
+
raise SystemExit(f"dropped: {response.unresolved_agency_tokens}")
|
|
210
|
+
# {'awarding_agency': ['HUDD']}
|
|
211
|
+
|
|
212
|
+
# What the tokens that DID match resolved to — the only way to catch a
|
|
213
|
+
# plausible-but-wrong match, where nothing was dropped at all.
|
|
214
|
+
for org in response.resolved_agencies.get("awarding_agency", []):
|
|
215
|
+
print(org["name"], org["cgac"])
|
|
216
|
+
# Department of Housing and Urban Development 086
|
|
217
|
+
|
|
218
|
+
for warning in response.agency_warnings:
|
|
219
|
+
print(warning)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
If *every* token for a filter fails to resolve, the API returns `400` and the SDK raises
|
|
223
|
+
`TangoValidationError` naming the offending value, rather than an empty page.
|
|
224
|
+
|
|
197
225
|
**Classification:**
|
|
198
226
|
- `naics_code`, `psc_code` - Industry/product codes
|
|
199
227
|
- `set_aside_type` - Set-aside type
|
|
@@ -137,9 +137,37 @@ contracts = client.list_contracts(
|
|
|
137
137
|
- `expiring_gte`, `expiring_lte` - Contract expiration date range
|
|
138
138
|
|
|
139
139
|
**Party Filters:**
|
|
140
|
-
- `awarding_agency`, `funding_agency` - Agency codes
|
|
140
|
+
- `awarding_agency`, `funding_agency` - Agency codes, names, abbreviations, or organization UUIDs. Multi-value OR via `|`.
|
|
141
141
|
- `recipient_name`, `recipient_uei` - Vendor/recipient filters
|
|
142
142
|
|
|
143
|
+
### Checking how agency filters resolved
|
|
144
|
+
|
|
145
|
+
Agency values are resolved fuzzily, so a token can match an organization you did not
|
|
146
|
+
intend — which silently scopes the query to that organization's subtree. A short result
|
|
147
|
+
set is then indistinguishable from "no such records exist". Responses expose what
|
|
148
|
+
actually happened:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
response = client.list_contracts(awarding_agency="HUD|HUDD")
|
|
152
|
+
|
|
153
|
+
# Tokens that matched nothing and were ignored.
|
|
154
|
+
if response.unresolved_agency_tokens:
|
|
155
|
+
raise SystemExit(f"dropped: {response.unresolved_agency_tokens}")
|
|
156
|
+
# {'awarding_agency': ['HUDD']}
|
|
157
|
+
|
|
158
|
+
# What the tokens that DID match resolved to — the only way to catch a
|
|
159
|
+
# plausible-but-wrong match, where nothing was dropped at all.
|
|
160
|
+
for org in response.resolved_agencies.get("awarding_agency", []):
|
|
161
|
+
print(org["name"], org["cgac"])
|
|
162
|
+
# Department of Housing and Urban Development 086
|
|
163
|
+
|
|
164
|
+
for warning in response.agency_warnings:
|
|
165
|
+
print(warning)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
If *every* token for a filter fails to resolve, the API returns `400` and the SDK raises
|
|
169
|
+
`TangoValidationError` naming the offending value, rather than an empty page.
|
|
170
|
+
|
|
143
171
|
**Classification:**
|
|
144
172
|
- `naics_code`, `psc_code` - Industry/product codes
|
|
145
173
|
- `set_aside_type` - Set-aside type
|
|
@@ -74,21 +74,35 @@ 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
|
-
|
|
78
|
-
"offices":
|
|
77
|
+
"protests": "list_protests",
|
|
78
|
+
"offices": "list_offices",
|
|
79
|
+
"psc": "list_psc",
|
|
80
|
+
"mas_sins": "list_mas_sins",
|
|
81
|
+
"departments": "list_departments",
|
|
82
|
+
"business_types": "list_business_types",
|
|
83
|
+
"assistance_listings": "list_assistance_listings",
|
|
84
|
+
# Genuinely absent from the SDK. Both are content endpoints with no shaping
|
|
85
|
+
# and no list method; `list_webhook_event_types` is unrelated to `events`.
|
|
86
|
+
"events": None,
|
|
87
|
+
"news": None,
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
# Params the API genuinely accepts but the contract omits, so they would read as
|
|
82
|
-
# stale (a silent no-op) when they in fact work. Tango surfaces
|
|
83
|
-
# search params for filtersets
|
|
84
|
-
#
|
|
91
|
+
# stale (a silent no-op) when they in fact work. Tango's generator surfaces
|
|
92
|
+
# view-handled search params only for filtersets declaring `min_length_params`
|
|
93
|
+
# and DRF `SearchFilter` backends declaring `search_fields`; a resource that
|
|
94
|
+
# implements search another way is absent from the contract even though the
|
|
95
|
+
# endpoint honors it.
|
|
96
|
+
#
|
|
85
97
|
# Each entry must be verified against the live API before being added, and
|
|
86
|
-
# removed once Tango's
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
# removed once Tango's generator surfaces it — `check_conformance` warns when an
|
|
99
|
+
# entry is no longer needed, so this list cannot quietly rot.
|
|
100
|
+
#
|
|
101
|
+
# Currently empty, and that is the goal state. Two entries have come and gone:
|
|
102
|
+
# budget/accounts `search` (published by makegov/tango#2944's SearchFilter
|
|
103
|
+
# extraction) and mas_sins `search` (published by makegov/tango#2948's
|
|
104
|
+
# `view_handled_params` declaration). Both were retired by the warning above.
|
|
105
|
+
CONTRACT_OMITTED_PARAMS: dict[str, frozenset[str]] = {}
|
|
92
106
|
|
|
93
107
|
# SDK-level conveniences that never correspond to API filter params.
|
|
94
108
|
# `sort`/`order` compose into the API's `ordering`; `filters` is the legacy
|
|
@@ -395,11 +409,22 @@ def run_check(manifest_path: Path) -> tuple[list[str], list[str]]:
|
|
|
395
409
|
)
|
|
396
410
|
|
|
397
411
|
# Direction 2 — staleness: SDK sends it, API no longer accepts it.
|
|
412
|
+
omitted = CONTRACT_OMITTED_PARAMS.get(resource_name, frozenset())
|
|
413
|
+
|
|
414
|
+
# A carve-out is only justified while the contract still omits the param.
|
|
415
|
+
# Once Tango publishes it, the entry hides real staleness — warn so it
|
|
416
|
+
# gets removed, mirroring how baseline entries are burned down above.
|
|
417
|
+
redundant = sorted(omitted & runtime_filters)
|
|
418
|
+
if redundant:
|
|
419
|
+
warnings.append(
|
|
420
|
+
f"{resource_name}: CONTRACT_OMITTED_PARAMS entries no longer needed "
|
|
421
|
+
f"(the contract now publishes them): {', '.join(redundant)}"
|
|
422
|
+
)
|
|
423
|
+
|
|
398
424
|
stale = sorted(
|
|
399
425
|
f"{arg} (sends `{api_param}`)" if arg != api_param else arg
|
|
400
426
|
for arg, api_param in exposed.items()
|
|
401
|
-
if api_param not in runtime_filters
|
|
402
|
-
and api_param not in CONTRACT_OMITTED_PARAMS.get(resource_name, frozenset())
|
|
427
|
+
if api_param not in runtime_filters and api_param not in omitted
|
|
403
428
|
)
|
|
404
429
|
if stale:
|
|
405
430
|
errors.append(
|
|
@@ -171,8 +171,29 @@ def collect_gaps(contract: dict[str, Any], registry: Any, models: Any) -> list[d
|
|
|
171
171
|
walk(resource, child_path, enode, child_schema)
|
|
172
172
|
|
|
173
173
|
for rkey, r in contract.get("resources", {}).items():
|
|
174
|
-
|
|
174
|
+
runtime = r.get("runtime") or {}
|
|
175
|
+
shape = runtime.get("shape")
|
|
175
176
|
if not shape:
|
|
177
|
+
# A null tree used to be an unconditional skip, which made this gate
|
|
178
|
+
# blind to exactly the resources it most needed to check. Tango
|
|
179
|
+
# published null for entities/opportunities/notices/protests/
|
|
180
|
+
# itdashboard because its generator crashed on tier-aware viewsets
|
|
181
|
+
# (makegov/tango#2944), and this gate reported full coverage while
|
|
182
|
+
# the SDK's Entity and Protest schemas drifted 13 fields behind.
|
|
183
|
+
#
|
|
184
|
+
# Contracts at schema_version >= 2 declare shape_supported, so a
|
|
185
|
+
# null tree on a shaping resource is now a hard finding. Older
|
|
186
|
+
# contracts omit the key; there `None` is genuinely ambiguous and
|
|
187
|
+
# skipping stays the only safe read.
|
|
188
|
+
if runtime.get("shape_supported"):
|
|
189
|
+
findings.append(
|
|
190
|
+
{
|
|
191
|
+
"kind": "contract_missing_shape",
|
|
192
|
+
"resource": rkey,
|
|
193
|
+
"path": "(root)",
|
|
194
|
+
"name": runtime.get("shape_error") or "no shape tree published",
|
|
195
|
+
}
|
|
196
|
+
)
|
|
176
197
|
continue
|
|
177
198
|
model_name = RESOURCE_TO_MODEL.get(rkey)
|
|
178
199
|
model = getattr(models, model_name, None) if model_name else None
|
|
@@ -305,6 +326,19 @@ def main() -> int:
|
|
|
305
326
|
print("\nNo new shape-coverage drift. ✓")
|
|
306
327
|
return 0
|
|
307
328
|
|
|
329
|
+
contract_gaps = [f for f in new if f["kind"] == "contract_missing_shape"]
|
|
330
|
+
if contract_gaps:
|
|
331
|
+
print(
|
|
332
|
+
"\n*** CONTRACT DEFECT — these resources support shaping but publish no shape tree ***"
|
|
333
|
+
)
|
|
334
|
+
_print_grouped("RESOURCES WITH NO SHAPE TREE", contract_gaps)
|
|
335
|
+
print(
|
|
336
|
+
"\n This is an upstream problem, not an SDK one: the vendored contract understates\n"
|
|
337
|
+
" the API, so coverage cannot be checked for these resources at all. Refresh the\n"
|
|
338
|
+
" contract (scripts/refresh_contract.py); if it persists, the generator is failing\n"
|
|
339
|
+
" to extract them — see makegov/tango contracts/README.md."
|
|
340
|
+
)
|
|
341
|
+
|
|
308
342
|
print("\n*** NEW shape-coverage drift (Tango exposes these; the SDK schema does not) ***")
|
|
309
343
|
_print_grouped("MISSING FIELDS", [f for f in new if f["kind"] == "missing_field"])
|
|
310
344
|
_print_grouped("MISSING EXPANDS", [f for f in new if f["kind"] == "missing_expand"])
|