tango-python 0.4.2__tar.gz → 0.4.3__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.3}/CHANGELOG.md +10 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/PKG-INFO +1 -1
- {tango_python-0.4.2 → tango_python-0.4.3}/pyproject.toml +1 -1
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/__init__.py +3 -1
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/client.py +34 -1
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/exceptions.py +28 -1
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/models.py +15 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/test_client.py +86 -1
- {tango_python-0.4.2 → tango_python-0.4.3}/uv.lock +1 -1
- {tango_python-0.4.2 → tango_python-0.4.3}/.env.example +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/.github/workflows/lint.yml +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/.github/workflows/publish.yml +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/.github/workflows/test.yml +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/.gitignore +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/LICENSE +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/README.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/ROADMAP.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/docs/API_REFERENCE.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/docs/DEVELOPERS.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/docs/SHAPES.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/docs/quick_start.ipynb +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/scripts/README.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/scripts/check_filter_shape_conformance.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/scripts/fetch_api_schema.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/scripts/generate_schemas_from_api.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/scripts/pr_review.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/scripts/test_production.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/shapes/__init__.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/shapes/explicit_schemas.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/shapes/factory.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/shapes/generator.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/shapes/models.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/shapes/parser.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/shapes/schema.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tango/shapes/types.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/__init__.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/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.3}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/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.3}/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.3}/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.3}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/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.3}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/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.3}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/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.3}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/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.3}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/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.3}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/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.3}/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.3}/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.3}/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.3}/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.3}/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.3}/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.3}/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.3}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/conftest.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/README.md +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/__init__.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/conftest.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_agencies_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_contracts_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_edge_cases_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_entities_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_forecasts_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_grants_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_naics_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_notices_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_offices_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_opportunities_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_organizations_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_otas_otidvs_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_protests_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_reference_data_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_subawards_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/test_vehicles_idvs_integration.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/integration/validation.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/production/__init__.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/production/conftest.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/production/test_production_smoke.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/test_models.py +0 -0
- {tango_python-0.4.2 → tango_python-0.4.3}/tests/test_shapes.py +0 -0
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.3] - 2026-03-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `TangoRateLimitError` now exposes `wait_in_seconds`, `detail`, and `limit_type` properties parsed from the API's 429 response body.
|
|
14
|
+
- `RateLimitInfo` dataclass for structured access to rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and per-window daily/burst variants).
|
|
15
|
+
- `TangoClient.rate_limit_info` property returns rate limit info from the most recent API response.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- `_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.
|
|
19
|
+
|
|
10
20
|
## [0.4.2] - 2026-03-04
|
|
11
21
|
|
|
12
22
|
### 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,
|
|
@@ -77,6 +78,7 @@ class TangoClient:
|
|
|
77
78
|
headers["X-API-KEY"] = self.api_key
|
|
78
79
|
|
|
79
80
|
self.client = httpx.Client(headers=headers, timeout=30.0)
|
|
81
|
+
self._last_rate_limit_info: RateLimitInfo | None = None
|
|
80
82
|
|
|
81
83
|
# Use hardcoded sensible defaults
|
|
82
84
|
cache_size = 100
|
|
@@ -98,6 +100,34 @@ class TangoClient:
|
|
|
98
100
|
# Core HTTP Request Utilities
|
|
99
101
|
# ============================================================================
|
|
100
102
|
|
|
103
|
+
@property
|
|
104
|
+
def rate_limit_info(self) -> RateLimitInfo | None:
|
|
105
|
+
"""Rate limit info from the most recent API response."""
|
|
106
|
+
return self._last_rate_limit_info
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def _parse_rate_limit_headers(headers: httpx.Headers) -> RateLimitInfo:
|
|
110
|
+
"""Extract rate limit info from response headers."""
|
|
111
|
+
def _int_or_none(val: str | None) -> int | None:
|
|
112
|
+
if val is None:
|
|
113
|
+
return None
|
|
114
|
+
try:
|
|
115
|
+
return int(val)
|
|
116
|
+
except (ValueError, TypeError):
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
return RateLimitInfo(
|
|
120
|
+
limit=_int_or_none(headers.get("X-RateLimit-Limit")),
|
|
121
|
+
remaining=_int_or_none(headers.get("X-RateLimit-Remaining")),
|
|
122
|
+
reset=_int_or_none(headers.get("X-RateLimit-Reset")),
|
|
123
|
+
daily_limit=_int_or_none(headers.get("X-RateLimit-Daily-Limit")),
|
|
124
|
+
daily_remaining=_int_or_none(headers.get("X-RateLimit-Daily-Remaining")),
|
|
125
|
+
daily_reset=_int_or_none(headers.get("X-RateLimit-Daily-Reset")),
|
|
126
|
+
burst_limit=_int_or_none(headers.get("X-RateLimit-Burst-Limit")),
|
|
127
|
+
burst_remaining=_int_or_none(headers.get("X-RateLimit-Burst-Remaining")),
|
|
128
|
+
burst_reset=_int_or_none(headers.get("X-RateLimit-Burst-Reset")),
|
|
129
|
+
)
|
|
130
|
+
|
|
101
131
|
def _request(
|
|
102
132
|
self,
|
|
103
133
|
method: str,
|
|
@@ -110,6 +140,7 @@ class TangoClient:
|
|
|
110
140
|
|
|
111
141
|
try:
|
|
112
142
|
response = self.client.request(method=method, url=url, params=params, json=json_data)
|
|
143
|
+
self._last_rate_limit_info = self._parse_rate_limit_headers(response.headers)
|
|
113
144
|
|
|
114
145
|
if response.status_code == 401:
|
|
115
146
|
raise TangoAuthError(
|
|
@@ -136,7 +167,9 @@ class TangoClient:
|
|
|
136
167
|
error_data,
|
|
137
168
|
)
|
|
138
169
|
elif response.status_code == 429:
|
|
139
|
-
|
|
170
|
+
error_data = response.json() if response.content else {}
|
|
171
|
+
detail = error_data.get("detail", "Rate limit exceeded")
|
|
172
|
+
raise TangoRateLimitError(detail, response.status_code, error_data)
|
|
140
173
|
elif not response.is_success:
|
|
141
174
|
raise TangoAPIError(
|
|
142
175
|
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.3}/tests/cassettes/TestAgenciesIntegration.test_get_agency
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.3}/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.3}/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.3}/tests/cassettes/TestOTAsIntegration.test_list_otas
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.3}/tests/cassettes/TestOfficesIntegration.test_get_office
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.3}/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.3}/tests/integration/test_opportunities_integration.py
RENAMED
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.3}/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.3}/tests/integration/test_reference_data_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
{tango_python-0.4.2 → tango_python-0.4.3}/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
|