tango-python 1.17.0__tar.gz → 1.18.1__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.17.0 → tango_python-1.18.1}/CHANGELOG.md +18 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/PKG-INFO +1 -1
- {tango_python-1.17.0 → tango_python-1.18.1}/docs/API_REFERENCE.md +17 -1
- {tango_python-1.17.0 → tango_python-1.18.1}/pyproject.toml +1 -1
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/__init__.py +1 -1
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/client.py +32 -2
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/models.py +11 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/shapes/explicit_schemas.py +35 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/shapes/generated_overlay.py +18 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_client.py +57 -0
- tango_python-1.18.1/tests/test_obligations_by_fiscal_year.py +88 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/.gitignore +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/LICENSE +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/README.md +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/docs/CLIENT.md +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/docs/DYNAMIC_MODELS.md +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/docs/ERRORS.md +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/docs/PAGINATION.md +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/docs/SHAPES.md +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/docs/WEBHOOKS.md +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/README.md +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/check_filter_shape_conformance.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/check_shape_coverage.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/fetch_api_schema.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/generate_schemas_from_api.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/generate_shape_overlay.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/pr_review.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/probe_shape_types.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/refresh_contract.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/smoke_api_parity.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/scripts/test_production.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/exceptions.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/shapes/__init__.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/shapes/factory.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/shapes/generator.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/shapes/models.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/shapes/parser.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/shapes/schema.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/shapes/types.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/webhooks/__init__.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/webhooks/cli.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/webhooks/receiver.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/webhooks/signing.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tango/webhooks/simulate.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/__init__.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[custom-key,piid,recipient(display_name),total_contract_value,award_date] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_get_entity_budget_flows +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[custom-uei,legal_business_name,cage_code] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[custom-id,title,anticipated_award_date] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[minimal-id,title,anticipated_award_date,fiscal_year,naics_code,status] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[custom-grant_id,title,opportunity_number] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[minimal-grant_id,opportunity_number,title,status(-),agency_code] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_code +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_name_text +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating_max +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_filter_by_performance_risk +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_filter_by_type_of_investment +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_filter_by_updated_time_range +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_funding_and_cio_evaluation_expansions +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_get_itdashboard_investment_by_uii +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_itdashboard_pagination +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_search +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[custom-uii,agency_name,investment_title,updated_time] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[default-None] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[custom-notice_id,title,solicitation_number] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[custom-opportunity_id,title,solicitation_number] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[custom-case_id,title,source_system,outcome] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[minimal-case_id,case_number,title,source_system,outcome,filed_date] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[ultra_minimal-key,piid,recipient(display_name),total_contract_value] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/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.17.0 → tango_python-1.18.1}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_with_metrics_expansion +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_orders_uses_default_shape +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_with_ordering +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/conftest.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/README.md +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/__init__.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/conftest.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_agencies_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_contracts_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_edge_cases_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_entities_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_forecasts_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_grants_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_itdashboard_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_naics_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_notices_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_offices_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_opportunities_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_organizations_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_otas_otidvs_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_protests_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_reference_data_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_subawards_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/test_vehicles_idvs_integration.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/integration/validation.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/production/__init__.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/production/conftest.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/production/test_production_smoke.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_api_parity.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_contract_appeals.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_dibbs_exclusions_sbir.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_ebuy.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_federal_register.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_models.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_opportunity_attachments.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_opportunity_awards.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_organization_scope.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_shape_coverage_gate.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_shapes.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_sled.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_webhooks_cli.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_webhooks_receiver.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_webhooks_signing.py +0 -0
- {tango_python-1.17.0 → tango_python-1.18.1}/tests/test_webhooks_simulate.py +0 -0
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.18.1] - 2026-10-19
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **API errors now carry the server's reason.** A 403 or other non-2xx response the SDK had no dedicated exception for raised `TangoAPIError("API request failed with status 403")` and threw the response body away, so a caller refused for a G2X-only shape field or a plan-gated filter couldn't tell why. The message now ends with the API's own explanation (`API request failed with status 403: G2X shape fields are not allowed for your account.`), and the parsed body is on `response_data`. Non-JSON bodies, such as a proxy's 502 page, keep the old message.
|
|
15
|
+
|
|
16
|
+
## [1.18.0] - 2026-10-19
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`obligations_by_fiscal_year(fiscal_year, obligated)` shape expand on contracts, IDVs, OTAs and OTIDVs** (Tango API 5.11.0). Returns the award's obligations per federal fiscal year, each rounded to cents, with `fiscal_year` parsed as `int` and `obligated` as `Decimal`. It is always `None` on IDVs and OTIDVs, whose transactions report a running lifetime total.
|
|
21
|
+
- **`attachments(extraction_status)` shape field on opportunities and notices** (Tango API 5.11.0). Reports `pending`, `extracting`, `completed`, `failed` or `unsupported` for each attachment, so you can tell why `extracted_text` is missing.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Re-vendored `contracts/filter_shape_contract.json` (Tango API 5.11.0) and regenerated the shape overlay.
|
|
26
|
+
- Documented that `recipient_uei` on `list_contracts()` (and `uei` on the IDV, OTA and OTIDV lists) accepts several UEIs joined by `|` (Tango API 5.11.0); the SDK already passed the value through unchanged.
|
|
27
|
+
|
|
10
28
|
## [1.17.0] - 2026-10-16
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -285,7 +285,7 @@ contracts = client.list_contracts(
|
|
|
285
285
|
- `awarding_agency` - Agency code (e.g., "4700" for GSA)
|
|
286
286
|
- `funding_agency` - Funding agency code
|
|
287
287
|
- `recipient_name` - Vendor/recipient name (mapped to 'recipient' API param)
|
|
288
|
-
- `recipient_uei` - Vendor UEI (mapped to 'uei' API param)
|
|
288
|
+
- `recipient_uei` - Vendor UEI (mapped to 'uei' API param). Several UEIs joined by `|` match awards to any of them, up to 500 (Tango API 5.11.0).
|
|
289
289
|
|
|
290
290
|
**Classification:**
|
|
291
291
|
- `naics_code` - NAICS industry code (mapped to 'naics' API param)
|
|
@@ -381,6 +381,22 @@ contracts = client.list_contracts(
|
|
|
381
381
|
- `psc` - Product/service code (nested)
|
|
382
382
|
- `place_of_performance` - Location (nested)
|
|
383
383
|
|
|
384
|
+
**Obligations by fiscal year — `obligations_by_fiscal_year(fiscal_year, obligated)`** (Tango API 5.11.0):
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
contracts = client.list_contracts(
|
|
388
|
+
shape="key,piid,obligated,obligations_by_fiscal_year(fiscal_year,obligated)",
|
|
389
|
+
piid="SOME_PIID",
|
|
390
|
+
)
|
|
391
|
+
for year in contracts.results[0]["obligations_by_fiscal_year"] or []:
|
|
392
|
+
print(year["fiscal_year"], year["obligated"]) # int, Decimal
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
- Sums the award's transactions per federal fiscal year (October 1 starts the next fiscal year), oldest year first, each rounded to cents. A de-obligation nets against its own year, so a year can be zero or negative.
|
|
396
|
+
- Returned only when named: no default shape includes it.
|
|
397
|
+
- The same expand works on `list_otas()`, `list_idvs()`, `list_otidvs()` and the `get_*` methods. On IDVs and OTIDVs it is always `None`, because their transactions report a running lifetime total rather than each action's obligation.
|
|
398
|
+
- `None` on a contract or OTA with no dated transactions.
|
|
399
|
+
|
|
384
400
|
---
|
|
385
401
|
|
|
386
402
|
## OTAs
|
|
@@ -75,6 +75,31 @@ from tango.shapes import (
|
|
|
75
75
|
_COUNT_TYPE_KEY = "_tango_count_type"
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _error_body(response: httpx.Response) -> dict[str, Any]:
|
|
79
|
+
"""The JSON object an error response carries, or {} for an empty, non-JSON (a proxy's HTML page) or non-object body."""
|
|
80
|
+
try:
|
|
81
|
+
data = response.json() if response.content else {}
|
|
82
|
+
except ValueError:
|
|
83
|
+
return {}
|
|
84
|
+
return data if isinstance(data, dict) else {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _error_reason(error_data: dict[str, Any]) -> str | None:
|
|
88
|
+
"""The server's own explanation from an error body, e.g. why a 403 refused the request.
|
|
89
|
+
|
|
90
|
+
The API sends either ``{"detail": "..."}`` or a structured body such as a tier gate's ``{"error": "...", "code": "tier_required", ...}``, sometimes nested under ``detail``.
|
|
91
|
+
"""
|
|
92
|
+
for body in (error_data, error_data.get("detail")):
|
|
93
|
+
if isinstance(body, str) and body:
|
|
94
|
+
return body
|
|
95
|
+
if isinstance(body, dict):
|
|
96
|
+
for key in ("error", "message", "detail"):
|
|
97
|
+
value = body.get(key)
|
|
98
|
+
if isinstance(value, str) and value:
|
|
99
|
+
return value
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
78
103
|
class TangoClient:
|
|
79
104
|
"""Tango API Client"""
|
|
80
105
|
|
|
@@ -232,8 +257,13 @@ class TangoClient:
|
|
|
232
257
|
detail = error_data.get("detail", "Rate limit exceeded")
|
|
233
258
|
raise TangoRateLimitError(detail, response.status_code, error_data)
|
|
234
259
|
elif not response.is_success:
|
|
260
|
+
error_data = _error_body(response)
|
|
261
|
+
reason = _error_reason(error_data)
|
|
262
|
+
message = f"API request failed with status {response.status_code}"
|
|
235
263
|
raise TangoAPIError(
|
|
236
|
-
f"
|
|
264
|
+
f"{message}: {reason}" if reason else message,
|
|
265
|
+
response.status_code,
|
|
266
|
+
error_data,
|
|
237
267
|
)
|
|
238
268
|
|
|
239
269
|
def _get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
@@ -695,7 +725,7 @@ class TangoClient:
|
|
|
695
725
|
naics_code: NAICS code (mapped to 'naics' API param)
|
|
696
726
|
psc_code: PSC code (mapped to 'psc' API param)
|
|
697
727
|
recipient_name: Vendor/recipient name (mapped to 'recipient' API param)
|
|
698
|
-
recipient_uei: Vendor UEI (mapped to 'uei' API param)
|
|
728
|
+
recipient_uei: Vendor UEI (mapped to 'uei' API param); join several with `|` for OR, up to 500
|
|
699
729
|
set_aside_type: Set-aside type (mapped to 'set_aside' API param)
|
|
700
730
|
sort: Field to sort by (combined with 'order' to produce 'ordering')
|
|
701
731
|
order: Sort order ('asc' or 'desc', default 'asc')
|
|
@@ -281,6 +281,17 @@ class AwardTransaction:
|
|
|
281
281
|
obligated: Decimal | None = None
|
|
282
282
|
|
|
283
283
|
|
|
284
|
+
@dataclass
|
|
285
|
+
class FiscalYearObligation:
|
|
286
|
+
"""Schema definition for FiscalYearObligation (not used for instances).
|
|
287
|
+
|
|
288
|
+
One entry of an award's ``obligations_by_fiscal_year``: the obligations dated in one federal fiscal year, rounded to cents.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
fiscal_year: int
|
|
292
|
+
obligated: Decimal
|
|
293
|
+
|
|
294
|
+
|
|
284
295
|
@dataclass
|
|
285
296
|
class OrganizationOfficePayload:
|
|
286
297
|
"""Schema definition for OrganizationOfficePayload (not used for instances).
|
|
@@ -197,6 +197,12 @@ TRANSACTION_SCHEMA: dict[str, FieldSchema] = {
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
FISCAL_YEAR_OBLIGATION_SCHEMA: dict[str, FieldSchema] = {
|
|
201
|
+
"fiscal_year": FieldSchema(name="fiscal_year", type=int, is_optional=False, is_list=False),
|
|
202
|
+
"obligated": FieldSchema(name="obligated", type=Decimal, is_optional=False, is_list=False),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
200
206
|
DEPARTMENT_SCHEMA: dict[str, FieldSchema] = {
|
|
201
207
|
"abbreviation": FieldSchema(name="abbreviation", type=str, is_optional=True, is_list=False),
|
|
202
208
|
"code": FieldSchema(name="code", type=int, is_optional=True, is_list=False),
|
|
@@ -432,6 +438,13 @@ CONTRACT_SCHEMA: dict[str, FieldSchema] = {
|
|
|
432
438
|
is_list=False,
|
|
433
439
|
nested_model="Vehicle",
|
|
434
440
|
),
|
|
441
|
+
"obligations_by_fiscal_year": FieldSchema(
|
|
442
|
+
name="obligations_by_fiscal_year",
|
|
443
|
+
type=dict,
|
|
444
|
+
is_optional=True,
|
|
445
|
+
is_list=True,
|
|
446
|
+
nested_model="FiscalYearObligation",
|
|
447
|
+
),
|
|
435
448
|
}
|
|
436
449
|
|
|
437
450
|
|
|
@@ -1290,6 +1303,13 @@ IDV_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1290
1303
|
"idv_contracts_value": FieldSchema(
|
|
1291
1304
|
name="idv_contracts_value", type=Decimal, is_optional=True, is_list=False
|
|
1292
1305
|
),
|
|
1306
|
+
"obligations_by_fiscal_year": FieldSchema(
|
|
1307
|
+
name="obligations_by_fiscal_year",
|
|
1308
|
+
type=dict,
|
|
1309
|
+
is_optional=True,
|
|
1310
|
+
is_list=True,
|
|
1311
|
+
nested_model="FiscalYearObligation",
|
|
1312
|
+
),
|
|
1293
1313
|
}
|
|
1294
1314
|
|
|
1295
1315
|
|
|
@@ -1439,6 +1459,13 @@ OTA_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1439
1459
|
is_list=False,
|
|
1440
1460
|
nested_model="RecipientProfile",
|
|
1441
1461
|
),
|
|
1462
|
+
"obligations_by_fiscal_year": FieldSchema(
|
|
1463
|
+
name="obligations_by_fiscal_year",
|
|
1464
|
+
type=dict,
|
|
1465
|
+
is_optional=True,
|
|
1466
|
+
is_list=True,
|
|
1467
|
+
nested_model="FiscalYearObligation",
|
|
1468
|
+
),
|
|
1442
1469
|
}
|
|
1443
1470
|
|
|
1444
1471
|
# OTIDV (Other Transaction IDV) - IDV-like
|
|
@@ -1459,6 +1486,13 @@ OTIDV_SCHEMA: dict[str, FieldSchema] = {
|
|
|
1459
1486
|
is_list=False,
|
|
1460
1487
|
nested_model="RecipientProfile",
|
|
1461
1488
|
),
|
|
1489
|
+
"obligations_by_fiscal_year": FieldSchema(
|
|
1490
|
+
name="obligations_by_fiscal_year",
|
|
1491
|
+
type=dict,
|
|
1492
|
+
is_optional=True,
|
|
1493
|
+
is_list=True,
|
|
1494
|
+
nested_model="FiscalYearObligation",
|
|
1495
|
+
),
|
|
1462
1496
|
}
|
|
1463
1497
|
|
|
1464
1498
|
# Subaward (prime/sub awards)
|
|
@@ -1707,6 +1741,7 @@ EXPLICIT_SCHEMAS: dict[str, dict[str, FieldSchema]] = {
|
|
|
1707
1741
|
"LegislativeMandates": LEGISLATIVE_MANDATES_SCHEMA,
|
|
1708
1742
|
"SubawardsSummary": SUBAWARDS_SUMMARY_SCHEMA,
|
|
1709
1743
|
"Transaction": TRANSACTION_SCHEMA,
|
|
1744
|
+
"FiscalYearObligation": FISCAL_YEAR_OBLIGATION_SCHEMA,
|
|
1710
1745
|
"Department": DEPARTMENT_SCHEMA,
|
|
1711
1746
|
"Contact": CONTACT_SCHEMA,
|
|
1712
1747
|
"RecipientProfile": RECIPIENT_PROFILE_SCHEMA,
|
|
@@ -85,6 +85,9 @@ ATTACHMENTS_SCHEMA: dict[str, FieldSchema] = {
|
|
|
85
85
|
"doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False),
|
|
86
86
|
"doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False),
|
|
87
87
|
"extracted_text": FieldSchema(name="extracted_text", type=str, is_optional=True, is_list=False),
|
|
88
|
+
"extraction_status": FieldSchema(
|
|
89
|
+
name="extraction_status", type=str, is_optional=True, is_list=False
|
|
90
|
+
),
|
|
88
91
|
"file_size": FieldSchema(name="file_size", type=int, is_optional=True, is_list=False),
|
|
89
92
|
"mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False),
|
|
90
93
|
"name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
|
|
@@ -99,6 +102,9 @@ ATTACHMENTS2_SCHEMA: dict[str, FieldSchema] = {
|
|
|
99
102
|
"doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False),
|
|
100
103
|
"doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False),
|
|
101
104
|
"extracted_text": FieldSchema(name="extracted_text", type=str, is_optional=True, is_list=False),
|
|
105
|
+
"extraction_status": FieldSchema(
|
|
106
|
+
name="extraction_status", type=str, is_optional=True, is_list=False
|
|
107
|
+
),
|
|
102
108
|
"file_size": FieldSchema(name="file_size", type=int, is_optional=True, is_list=False),
|
|
103
109
|
"mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False),
|
|
104
110
|
"name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
|
|
@@ -135,6 +141,9 @@ ATTACHMENTS3_SCHEMA: dict[str, FieldSchema] = {
|
|
|
135
141
|
|
|
136
142
|
ATTACHMENTS4_SCHEMA: dict[str, FieldSchema] = {
|
|
137
143
|
"attachment_id": FieldSchema(name="attachment_id", type=str, is_optional=True, is_list=False),
|
|
144
|
+
"extraction_status": FieldSchema(
|
|
145
|
+
name="extraction_status", type=str, is_optional=True, is_list=False
|
|
146
|
+
),
|
|
138
147
|
"file_size": FieldSchema(name="file_size", type=str, is_optional=True, is_list=False),
|
|
139
148
|
"mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False),
|
|
140
149
|
"name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
|
|
@@ -149,6 +158,9 @@ ATTACHMENTS5_SCHEMA: dict[str, FieldSchema] = {
|
|
|
149
158
|
"doc_role": FieldSchema(name="doc_role", type=str, is_optional=True, is_list=False),
|
|
150
159
|
"doc_role_alt": FieldSchema(name="doc_role_alt", type=str, is_optional=True, is_list=False),
|
|
151
160
|
"extracted_text": FieldSchema(name="extracted_text", type=str, is_optional=True, is_list=False),
|
|
161
|
+
"extraction_status": FieldSchema(
|
|
162
|
+
name="extraction_status", type=str, is_optional=True, is_list=False
|
|
163
|
+
),
|
|
152
164
|
"file_size": FieldSchema(name="file_size", type=str, is_optional=True, is_list=False),
|
|
153
165
|
"mime_type": FieldSchema(name="mime_type", type=str, is_optional=True, is_list=False),
|
|
154
166
|
"name": FieldSchema(name="name", type=str, is_optional=True, is_list=False),
|
|
@@ -758,6 +770,11 @@ NOTICE_TYPE_SCHEMA: dict[str, FieldSchema] = {
|
|
|
758
770
|
"type": FieldSchema(name="type", type=str, is_optional=True, is_list=False),
|
|
759
771
|
}
|
|
760
772
|
|
|
773
|
+
OBLIGATIONS_BY_FISCAL_YEAR_SCHEMA: dict[str, FieldSchema] = {
|
|
774
|
+
"fiscal_year": FieldSchema(name="fiscal_year", type=str, is_optional=True, is_list=False),
|
|
775
|
+
"obligated": FieldSchema(name="obligated", type=str, is_optional=True, is_list=False),
|
|
776
|
+
}
|
|
777
|
+
|
|
761
778
|
OFFICERS_SCHEMA: dict[str, FieldSchema] = {
|
|
762
779
|
"highly_compensated_officer_1_amount": FieldSchema(
|
|
763
780
|
name="highly_compensated_officer_1_amount", type=Decimal, is_optional=True, is_list=False
|
|
@@ -1161,6 +1178,7 @@ GENERATED_NESTED: dict[str, dict[str, FieldSchema]] = {
|
|
|
1161
1178
|
"NoticeHistory": NOTICE_HISTORY_SCHEMA,
|
|
1162
1179
|
"NoticeHistory2": NOTICE_HISTORY2_SCHEMA,
|
|
1163
1180
|
"NoticeType": NOTICE_TYPE_SCHEMA,
|
|
1181
|
+
"ObligationsByFiscalYear": OBLIGATIONS_BY_FISCAL_YEAR_SCHEMA,
|
|
1164
1182
|
"Officers": OFFICERS_SCHEMA,
|
|
1165
1183
|
"Opportunity": OPPORTUNITY_SCHEMA,
|
|
1166
1184
|
"Opportunity2": OPPORTUNITY2_SCHEMA,
|
|
@@ -1572,6 +1572,63 @@ class TestErrorHandling:
|
|
|
1572
1572
|
assert exc_info.value.status_code == 500
|
|
1573
1573
|
assert "500" in str(exc_info.value)
|
|
1574
1574
|
|
|
1575
|
+
@staticmethod
|
|
1576
|
+
def _error_response(status, body=None, content=b"{}"):
|
|
1577
|
+
response = Mock()
|
|
1578
|
+
response.is_success = False
|
|
1579
|
+
response.status_code = status
|
|
1580
|
+
response.content = content
|
|
1581
|
+
response.json.return_value = body
|
|
1582
|
+
return response
|
|
1583
|
+
|
|
1584
|
+
@patch("tango.client.httpx.Client.request")
|
|
1585
|
+
def test_403_carries_the_server_reason(self, mock_request):
|
|
1586
|
+
body = {"detail": "G2X shape fields are not allowed for your account."}
|
|
1587
|
+
mock_request.return_value = self._error_response(403, body)
|
|
1588
|
+
|
|
1589
|
+
with pytest.raises(TangoAPIError) as exc_info:
|
|
1590
|
+
TangoClient(api_key="test-key").list_agencies()
|
|
1591
|
+
|
|
1592
|
+
assert (
|
|
1593
|
+
str(exc_info.value)
|
|
1594
|
+
== "API request failed with status 403: G2X shape fields are not allowed for your account."
|
|
1595
|
+
)
|
|
1596
|
+
assert exc_info.value.status_code == 403
|
|
1597
|
+
assert exc_info.value.response_data == body
|
|
1598
|
+
|
|
1599
|
+
@pytest.mark.parametrize(
|
|
1600
|
+
"body",
|
|
1601
|
+
[
|
|
1602
|
+
{"error": "The 'psc' filter requires a Small plan or higher.", "code": "tier_required"},
|
|
1603
|
+
{
|
|
1604
|
+
"detail": {
|
|
1605
|
+
"error": "The 'psc' filter requires a Small plan or higher.",
|
|
1606
|
+
"code": "tier_required",
|
|
1607
|
+
}
|
|
1608
|
+
},
|
|
1609
|
+
],
|
|
1610
|
+
)
|
|
1611
|
+
@patch("tango.client.httpx.Client.request")
|
|
1612
|
+
def test_403_tier_gate_reason_flat_or_nested(self, mock_request, body):
|
|
1613
|
+
mock_request.return_value = self._error_response(403, body)
|
|
1614
|
+
|
|
1615
|
+
with pytest.raises(
|
|
1616
|
+
TangoAPIError, match=r"status 403: The 'psc' filter requires a Small plan"
|
|
1617
|
+
):
|
|
1618
|
+
TangoClient(api_key="test-key").list_agencies()
|
|
1619
|
+
|
|
1620
|
+
@patch("tango.client.httpx.Client.request")
|
|
1621
|
+
def test_non_json_error_body_keeps_the_bare_message(self, mock_request):
|
|
1622
|
+
response = self._error_response(502, content=b"<html>Bad Gateway</html>")
|
|
1623
|
+
response.json.side_effect = ValueError("not JSON")
|
|
1624
|
+
mock_request.return_value = response
|
|
1625
|
+
|
|
1626
|
+
with pytest.raises(TangoAPIError) as exc_info:
|
|
1627
|
+
TangoClient(api_key="test-key").list_agencies()
|
|
1628
|
+
|
|
1629
|
+
assert str(exc_info.value) == "API request failed with status 502"
|
|
1630
|
+
assert exc_info.value.response_data == {}
|
|
1631
|
+
|
|
1575
1632
|
@patch("tango.client.httpx.Client.request")
|
|
1576
1633
|
def test_network_error(self, mock_request):
|
|
1577
1634
|
"""Test network errors raise TangoAPIError"""
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""The `obligations_by_fiscal_year(...)` expand on contracts, IDVs, OTAs and OT IDVs."""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from tango import TangoClient
|
|
9
|
+
from tango.models import IDV, OTA, OTIDV, Contract, Notice, Opportunity
|
|
10
|
+
from tango.shapes.parser import ShapeParser
|
|
11
|
+
|
|
12
|
+
SHAPE = "key,obligations_by_fiscal_year(fiscal_year,obligated)"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _mock(mock_request, payload):
|
|
16
|
+
response = Mock()
|
|
17
|
+
response.is_success = True
|
|
18
|
+
response.json.return_value = payload
|
|
19
|
+
response.content = b"{}"
|
|
20
|
+
mock_request.return_value = response
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _page(rows):
|
|
24
|
+
return {"count": len(rows), "next": None, "previous": None, "results": rows}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestObligationsByFiscalYearShape:
|
|
28
|
+
@pytest.mark.parametrize("model", [Contract, IDV, OTA, OTIDV])
|
|
29
|
+
@pytest.mark.parametrize(
|
|
30
|
+
"shape",
|
|
31
|
+
[
|
|
32
|
+
SHAPE,
|
|
33
|
+
"key,obligations_by_fiscal_year(*)",
|
|
34
|
+
"key,obligations_by_fiscal_year",
|
|
35
|
+
],
|
|
36
|
+
)
|
|
37
|
+
def test_shape_validates(self, model, shape):
|
|
38
|
+
parser = ShapeParser(cache_enabled=False)
|
|
39
|
+
parser.validate(parser.parse(shape), model)
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize("model", [Contract, OTA])
|
|
42
|
+
def test_unknown_sub_field_is_rejected(self, model):
|
|
43
|
+
parser = ShapeParser(cache_enabled=False)
|
|
44
|
+
with pytest.raises(Exception, match="amount"):
|
|
45
|
+
parser.validate(parser.parse("key,obligations_by_fiscal_year(amount)"), model)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestObligationsByFiscalYearResponses:
|
|
49
|
+
@pytest.mark.parametrize("method", ["list_contracts", "list_otas"])
|
|
50
|
+
@patch("tango.client.httpx.Client.request")
|
|
51
|
+
def test_list_parses_each_year(self, mock_request, method):
|
|
52
|
+
_mock(
|
|
53
|
+
mock_request,
|
|
54
|
+
_page(
|
|
55
|
+
[
|
|
56
|
+
{
|
|
57
|
+
"key": "K1",
|
|
58
|
+
"obligations_by_fiscal_year": [
|
|
59
|
+
{"fiscal_year": 2023, "obligated": 1000.0},
|
|
60
|
+
{"fiscal_year": 2024, "obligated": -250.5},
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
(row,) = getattr(TangoClient(api_key="k"), method)(shape=SHAPE).results
|
|
68
|
+
|
|
69
|
+
years = row["obligations_by_fiscal_year"]
|
|
70
|
+
assert [y["fiscal_year"] for y in years] == [2023, 2024]
|
|
71
|
+
assert [y["obligated"] for y in years] == [Decimal("1000.0"), Decimal("-250.5")]
|
|
72
|
+
assert all(isinstance(y["obligated"], Decimal) for y in years)
|
|
73
|
+
|
|
74
|
+
@pytest.mark.parametrize("method", ["list_idvs", "list_otidvs"])
|
|
75
|
+
@patch("tango.client.httpx.Client.request")
|
|
76
|
+
def test_cumulative_award_types_return_none(self, mock_request, method):
|
|
77
|
+
_mock(mock_request, _page([{"key": "K1", "obligations_by_fiscal_year": None}]))
|
|
78
|
+
|
|
79
|
+
(row,) = getattr(TangoClient(api_key="k"), method)(shape=SHAPE).results
|
|
80
|
+
|
|
81
|
+
assert row["obligations_by_fiscal_year"] is None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestAttachmentExtractionStatusShape:
|
|
85
|
+
@pytest.mark.parametrize("model", [Opportunity, Notice])
|
|
86
|
+
def test_shape_validates(self, model):
|
|
87
|
+
parser = ShapeParser(cache_enabled=False)
|
|
88
|
+
parser.validate(parser.parse("title,attachments(name,extraction_status)"), model)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tango_python-1.17.0 → tango_python-1.18.1}/tests/cassettes/TestAgenciesIntegration.test_get_agency
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|