tango-python 0.4.2__tar.gz → 0.4.4__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-0.4.2 → tango_python-0.4.4}/CHANGELOG.md +17 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/PKG-INFO +1 -1
- {tango_python-0.4.2 → tango_python-0.4.4}/pyproject.toml +1 -1
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/__init__.py +3 -1
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/client.py +49 -1
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/exceptions.py +28 -1
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/models.py +15 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/test_client.py +86 -1
- {tango_python-0.4.2 → tango_python-0.4.4}/uv.lock +1 -1
- {tango_python-0.4.2 → tango_python-0.4.4}/.env.example +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/.github/workflows/lint.yml +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/.github/workflows/publish.yml +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/.github/workflows/test.yml +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/.gitignore +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/LICENSE +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/README.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/ROADMAP.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/docs/API_REFERENCE.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/docs/DEVELOPERS.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/docs/SHAPES.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/docs/quick_start.ipynb +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/scripts/README.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/scripts/check_filter_shape_conformance.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/scripts/fetch_api_schema.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/scripts/generate_schemas_from_api.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/scripts/pr_review.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/scripts/test_production.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/__init__.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/explicit_schemas.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/factory.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/generator.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/models.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/parser.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/schema.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/types.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/__init__.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[custom-key,piid,recipient(display_name),total_contract_value,award_date] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[custom-uei,legal_business_name,cage_code] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[custom-id,title,anticipated_award_date] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[minimal-id,title,anticipated_award_date,fiscal_year,naics_code,status] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[custom-grant_id,title,opportunity_number] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[minimal-grant_id,opportunity_number,title,status(-),agency_code] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[custom-notice_id,title,solicitation_number] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[custom-opportunity_id,title,solicitation_number] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[custom-case_id,title,source_system,outcome] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[minimal-case_id,case_number,title,source_system,outcome,filed_date] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[ultra_minimal-key,piid,recipient(display_name),total_contract_value] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/conftest.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/README.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/__init__.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/conftest.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_agencies_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_contracts_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_edge_cases_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_entities_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_forecasts_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_grants_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_naics_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_notices_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_offices_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_opportunities_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_organizations_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_otas_otidvs_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_protests_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_reference_data_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_subawards_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_vehicles_idvs_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/validation.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/production/__init__.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/production/conftest.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/production/test_production_smoke.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/test_models.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.4}/tests/test_shapes.py +0 -0
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.4] - 2026-03-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `parent_piid` filter parameter on `list_contracts` for filtering orders under a specific parent IDV PIID.
|
|
14
|
+
- `user_agent` and `extra_headers` parameters on `TangoClient` for custom request headers.
|
|
15
|
+
- `TangoClient.last_response_headers` property for accessing full HTTP headers from the most recent API response.
|
|
16
|
+
|
|
17
|
+
## [0.4.3] - 2026-03-21
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- `TangoRateLimitError` now exposes `wait_in_seconds`, `detail`, and `limit_type` properties parsed from the API's 429 response body.
|
|
21
|
+
- `RateLimitInfo` dataclass for structured access to rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and per-window daily/burst variants).
|
|
22
|
+
- `TangoClient.rate_limit_info` property returns rate limit info from the most recent API response.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `_request` now passes the full 429 response body to `TangoRateLimitError` (previously discarded), enabling callers to access `wait_in_seconds` and the specific limit type that was exceeded.
|
|
26
|
+
|
|
10
27
|
## [0.4.2] - 2026-03-04
|
|
11
28
|
|
|
12
29
|
### Added
|
|
@@ -11,6 +11,7 @@ from .exceptions import (
|
|
|
11
11
|
from .models import (
|
|
12
12
|
GsaElibraryContract,
|
|
13
13
|
PaginatedResponse,
|
|
14
|
+
RateLimitInfo,
|
|
14
15
|
SearchFilters,
|
|
15
16
|
ShapeConfig,
|
|
16
17
|
WebhookEndpoint,
|
|
@@ -27,7 +28,7 @@ from .shapes import (
|
|
|
27
28
|
TypeGenerator,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
|
-
__version__ = "0.4.
|
|
31
|
+
__version__ = "0.4.3"
|
|
31
32
|
__all__ = [
|
|
32
33
|
"TangoClient",
|
|
33
34
|
"TangoAPIError",
|
|
@@ -35,6 +36,7 @@ __all__ = [
|
|
|
35
36
|
"TangoNotFoundError",
|
|
36
37
|
"TangoValidationError",
|
|
37
38
|
"TangoRateLimitError",
|
|
39
|
+
"RateLimitInfo",
|
|
38
40
|
"GsaElibraryContract",
|
|
39
41
|
"PaginatedResponse",
|
|
40
42
|
"SearchFilters",
|
|
@@ -32,6 +32,7 @@ from tango.models import (
|
|
|
32
32
|
Organization,
|
|
33
33
|
PaginatedResponse,
|
|
34
34
|
Protest,
|
|
35
|
+
RateLimitInfo,
|
|
35
36
|
SearchFilters,
|
|
36
37
|
ShapeConfig,
|
|
37
38
|
Subaward,
|
|
@@ -58,6 +59,8 @@ class TangoClient:
|
|
|
58
59
|
self,
|
|
59
60
|
api_key: str | None = None,
|
|
60
61
|
base_url: str = "https://tango.makegov.com",
|
|
62
|
+
user_agent: str | None = None,
|
|
63
|
+
extra_headers: dict[str, str] | None = None,
|
|
61
64
|
):
|
|
62
65
|
"""
|
|
63
66
|
Initialize the Tango API client
|
|
@@ -66,6 +69,8 @@ class TangoClient:
|
|
|
66
69
|
api_key: API key for authentication. If not provided, will attempt to load from
|
|
67
70
|
TANGO_API_KEY environment variable.
|
|
68
71
|
base_url: Base URL for the API
|
|
72
|
+
user_agent: Custom User-Agent header value.
|
|
73
|
+
extra_headers: Additional headers to include in every request.
|
|
69
74
|
"""
|
|
70
75
|
# Load API key from environment if not provided
|
|
71
76
|
self.api_key = api_key or os.getenv("TANGO_API_KEY")
|
|
@@ -75,8 +80,14 @@ class TangoClient:
|
|
|
75
80
|
headers = {}
|
|
76
81
|
if self.api_key:
|
|
77
82
|
headers["X-API-KEY"] = self.api_key
|
|
83
|
+
if user_agent:
|
|
84
|
+
headers["User-Agent"] = user_agent
|
|
85
|
+
if extra_headers:
|
|
86
|
+
headers.update(extra_headers)
|
|
78
87
|
|
|
79
88
|
self.client = httpx.Client(headers=headers, timeout=30.0)
|
|
89
|
+
self._last_rate_limit_info: RateLimitInfo | None = None
|
|
90
|
+
self._last_response_headers: httpx.Headers | None = None
|
|
80
91
|
|
|
81
92
|
# Use hardcoded sensible defaults
|
|
82
93
|
cache_size = 100
|
|
@@ -98,6 +109,39 @@ class TangoClient:
|
|
|
98
109
|
# Core HTTP Request Utilities
|
|
99
110
|
# ============================================================================
|
|
100
111
|
|
|
112
|
+
@property
|
|
113
|
+
def rate_limit_info(self) -> RateLimitInfo | None:
|
|
114
|
+
"""Rate limit info from the most recent API response."""
|
|
115
|
+
return self._last_rate_limit_info
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def last_response_headers(self) -> httpx.Headers | None:
|
|
119
|
+
"""Full HTTP headers from the most recent API response."""
|
|
120
|
+
return self._last_response_headers
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def _parse_rate_limit_headers(headers: httpx.Headers) -> RateLimitInfo:
|
|
124
|
+
"""Extract rate limit info from response headers."""
|
|
125
|
+
def _int_or_none(val: str | None) -> int | None:
|
|
126
|
+
if val is None:
|
|
127
|
+
return None
|
|
128
|
+
try:
|
|
129
|
+
return int(val)
|
|
130
|
+
except (ValueError, TypeError):
|
|
131
|
+
return None
|
|
132
|
+
|
|
133
|
+
return RateLimitInfo(
|
|
134
|
+
limit=_int_or_none(headers.get("X-RateLimit-Limit")),
|
|
135
|
+
remaining=_int_or_none(headers.get("X-RateLimit-Remaining")),
|
|
136
|
+
reset=_int_or_none(headers.get("X-RateLimit-Reset")),
|
|
137
|
+
daily_limit=_int_or_none(headers.get("X-RateLimit-Daily-Limit")),
|
|
138
|
+
daily_remaining=_int_or_none(headers.get("X-RateLimit-Daily-Remaining")),
|
|
139
|
+
daily_reset=_int_or_none(headers.get("X-RateLimit-Daily-Reset")),
|
|
140
|
+
burst_limit=_int_or_none(headers.get("X-RateLimit-Burst-Limit")),
|
|
141
|
+
burst_remaining=_int_or_none(headers.get("X-RateLimit-Burst-Remaining")),
|
|
142
|
+
burst_reset=_int_or_none(headers.get("X-RateLimit-Burst-Reset")),
|
|
143
|
+
)
|
|
144
|
+
|
|
101
145
|
def _request(
|
|
102
146
|
self,
|
|
103
147
|
method: str,
|
|
@@ -110,6 +154,8 @@ class TangoClient:
|
|
|
110
154
|
|
|
111
155
|
try:
|
|
112
156
|
response = self.client.request(method=method, url=url, params=params, json=json_data)
|
|
157
|
+
self._last_response_headers = response.headers
|
|
158
|
+
self._last_rate_limit_info = self._parse_rate_limit_headers(response.headers)
|
|
113
159
|
|
|
114
160
|
if response.status_code == 401:
|
|
115
161
|
raise TangoAuthError(
|
|
@@ -136,7 +182,9 @@ class TangoClient:
|
|
|
136
182
|
error_data,
|
|
137
183
|
)
|
|
138
184
|
elif response.status_code == 429:
|
|
139
|
-
|
|
185
|
+
error_data = response.json() if response.content else {}
|
|
186
|
+
detail = error_data.get("detail", "Rate limit exceeded")
|
|
187
|
+
raise TangoRateLimitError(detail, response.status_code, error_data)
|
|
140
188
|
elif not response.is_success:
|
|
141
189
|
raise TangoAPIError(
|
|
142
190
|
f"API request failed with status {response.status_code}", response.status_code
|
|
@@ -39,7 +39,34 @@ class TangoValidationError(TangoAPIError):
|
|
|
39
39
|
class TangoRateLimitError(TangoAPIError):
|
|
40
40
|
"""Rate limit exceeded error"""
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
@property
|
|
43
|
+
def wait_in_seconds(self) -> int | None:
|
|
44
|
+
"""Seconds to wait before retrying, from API response."""
|
|
45
|
+
val = self.response_data.get("wait_in_seconds")
|
|
46
|
+
if val is not None:
|
|
47
|
+
try:
|
|
48
|
+
return int(val)
|
|
49
|
+
except (ValueError, TypeError):
|
|
50
|
+
return None
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def detail(self) -> str | None:
|
|
55
|
+
"""Human-readable detail from API response."""
|
|
56
|
+
return self.response_data.get("detail")
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def limit_type(self) -> str | None:
|
|
60
|
+
"""Which limit was hit: 'burst' or 'daily', parsed from detail."""
|
|
61
|
+
d = self.detail
|
|
62
|
+
if not d:
|
|
63
|
+
return None
|
|
64
|
+
lower = d.lower()
|
|
65
|
+
if "burst" in lower or "minute" in lower:
|
|
66
|
+
return "burst"
|
|
67
|
+
if "daily" in lower or "day" in lower:
|
|
68
|
+
return "daily"
|
|
69
|
+
return None
|
|
43
70
|
|
|
44
71
|
|
|
45
72
|
class ShapeError(TangoAPIError):
|
|
@@ -23,6 +23,21 @@ T = TypeVar("T")
|
|
|
23
23
|
# ============================================================================
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
@dataclass
|
|
27
|
+
class RateLimitInfo:
|
|
28
|
+
"""Rate limit information from API response headers."""
|
|
29
|
+
|
|
30
|
+
limit: int | None = None
|
|
31
|
+
remaining: int | None = None
|
|
32
|
+
reset: int | None = None
|
|
33
|
+
daily_limit: int | None = None
|
|
34
|
+
daily_remaining: int | None = None
|
|
35
|
+
daily_reset: int | None = None
|
|
36
|
+
burst_limit: int | None = None
|
|
37
|
+
burst_remaining: int | None = None
|
|
38
|
+
burst_reset: int | None = None
|
|
39
|
+
|
|
40
|
+
|
|
26
41
|
@dataclass
|
|
27
42
|
class SearchFilters:
|
|
28
43
|
"""Search filter parameters for contract search
|
|
@@ -1065,10 +1065,16 @@ class TestErrorHandling:
|
|
|
1065
1065
|
|
|
1066
1066
|
@patch("tango.client.httpx.Client.request")
|
|
1067
1067
|
def test_429_rate_limit_error(self, mock_request):
|
|
1068
|
-
"""Test 429 Rate Limit raises TangoRateLimitError"""
|
|
1068
|
+
"""Test 429 Rate Limit raises TangoRateLimitError with parsed body"""
|
|
1069
1069
|
mock_response = Mock()
|
|
1070
1070
|
mock_response.is_success = False
|
|
1071
1071
|
mock_response.status_code = 429
|
|
1072
|
+
mock_response.content = b'{"detail": "Rate limit exceeded for burst. Please try again in 45 seconds.", "wait_in_seconds": 45}'
|
|
1073
|
+
mock_response.json.return_value = {
|
|
1074
|
+
"detail": "Rate limit exceeded for burst. Please try again in 45 seconds.",
|
|
1075
|
+
"wait_in_seconds": 45,
|
|
1076
|
+
}
|
|
1077
|
+
mock_response.headers = {}
|
|
1072
1078
|
mock_request.return_value = mock_response
|
|
1073
1079
|
|
|
1074
1080
|
client = TangoClient(api_key="test-key")
|
|
@@ -1077,6 +1083,85 @@ class TestErrorHandling:
|
|
|
1077
1083
|
client.list_agencies()
|
|
1078
1084
|
|
|
1079
1085
|
assert exc_info.value.status_code == 429
|
|
1086
|
+
assert exc_info.value.wait_in_seconds == 45
|
|
1087
|
+
assert "burst" in exc_info.value.detail
|
|
1088
|
+
assert exc_info.value.limit_type == "burst"
|
|
1089
|
+
|
|
1090
|
+
@patch("tango.client.httpx.Client.request")
|
|
1091
|
+
def test_429_daily_limit_error(self, mock_request):
|
|
1092
|
+
"""Test 429 for daily limit includes correct limit_type"""
|
|
1093
|
+
mock_response = Mock()
|
|
1094
|
+
mock_response.is_success = False
|
|
1095
|
+
mock_response.status_code = 429
|
|
1096
|
+
mock_response.content = b'{"detail": "Rate limit exceeded for daily. Please try again in 3600 seconds.", "wait_in_seconds": 3600}'
|
|
1097
|
+
mock_response.json.return_value = {
|
|
1098
|
+
"detail": "Rate limit exceeded for daily. Please try again in 3600 seconds.",
|
|
1099
|
+
"wait_in_seconds": 3600,
|
|
1100
|
+
}
|
|
1101
|
+
mock_response.headers = {}
|
|
1102
|
+
mock_request.return_value = mock_response
|
|
1103
|
+
|
|
1104
|
+
client = TangoClient(api_key="test-key")
|
|
1105
|
+
|
|
1106
|
+
with pytest.raises(TangoRateLimitError) as exc_info:
|
|
1107
|
+
client.list_agencies()
|
|
1108
|
+
|
|
1109
|
+
assert exc_info.value.limit_type == "daily"
|
|
1110
|
+
assert exc_info.value.wait_in_seconds == 3600
|
|
1111
|
+
|
|
1112
|
+
@patch("tango.client.httpx.Client.request")
|
|
1113
|
+
def test_429_empty_body(self, mock_request):
|
|
1114
|
+
"""Test 429 with no content body still works"""
|
|
1115
|
+
mock_response = Mock()
|
|
1116
|
+
mock_response.is_success = False
|
|
1117
|
+
mock_response.status_code = 429
|
|
1118
|
+
mock_response.content = None
|
|
1119
|
+
mock_response.headers = {}
|
|
1120
|
+
mock_request.return_value = mock_response
|
|
1121
|
+
|
|
1122
|
+
client = TangoClient(api_key="test-key")
|
|
1123
|
+
|
|
1124
|
+
with pytest.raises(TangoRateLimitError) as exc_info:
|
|
1125
|
+
client.list_agencies()
|
|
1126
|
+
|
|
1127
|
+
assert exc_info.value.status_code == 429
|
|
1128
|
+
assert exc_info.value.wait_in_seconds is None
|
|
1129
|
+
assert exc_info.value.limit_type is None
|
|
1130
|
+
|
|
1131
|
+
@patch("tango.client.httpx.Client.request")
|
|
1132
|
+
def test_rate_limit_headers_parsed(self, mock_request):
|
|
1133
|
+
"""Test rate limit headers are parsed from successful responses"""
|
|
1134
|
+
mock_response = Mock()
|
|
1135
|
+
mock_response.is_success = True
|
|
1136
|
+
mock_response.status_code = 200
|
|
1137
|
+
mock_response.content = b'{"results": []}'
|
|
1138
|
+
mock_response.json.return_value = {"results": []}
|
|
1139
|
+
mock_response.headers = {
|
|
1140
|
+
"X-RateLimit-Limit": "100",
|
|
1141
|
+
"X-RateLimit-Remaining": "95",
|
|
1142
|
+
"X-RateLimit-Reset": "45",
|
|
1143
|
+
"X-RateLimit-Daily-Limit": "2400",
|
|
1144
|
+
"X-RateLimit-Daily-Remaining": "2350",
|
|
1145
|
+
"X-RateLimit-Daily-Reset": "86400",
|
|
1146
|
+
"X-RateLimit-Burst-Limit": "100",
|
|
1147
|
+
"X-RateLimit-Burst-Remaining": "95",
|
|
1148
|
+
"X-RateLimit-Burst-Reset": "45",
|
|
1149
|
+
}
|
|
1150
|
+
mock_request.return_value = mock_response
|
|
1151
|
+
|
|
1152
|
+
client = TangoClient(api_key="test-key")
|
|
1153
|
+
assert client.rate_limit_info is None
|
|
1154
|
+
|
|
1155
|
+
client._request("GET", "/api/agencies/")
|
|
1156
|
+
|
|
1157
|
+
info = client.rate_limit_info
|
|
1158
|
+
assert info is not None
|
|
1159
|
+
assert info.limit == 100
|
|
1160
|
+
assert info.remaining == 95
|
|
1161
|
+
assert info.reset == 45
|
|
1162
|
+
assert info.daily_limit == 2400
|
|
1163
|
+
assert info.daily_remaining == 2350
|
|
1164
|
+
assert info.burst_remaining == 95
|
|
1080
1165
|
|
|
1081
1166
|
@patch("tango.client.httpx.Client.request")
|
|
1082
1167
|
def test_500_server_error(self, mock_request):
|
|
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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestAgenciesIntegration.test_get_agency
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestAgenciesIntegration.test_list_agencies
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
|
|
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-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNaicsIntegration.test_list_naics
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
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTAsIntegration.test_list_otas
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOfficesIntegration.test_get_office
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOfficesIntegration.test_list_offices
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_opportunities_integration.py
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_organizations_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_reference_data_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_vehicles_idvs_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|