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.
Files changed (167) hide show
  1. {tango_python-0.4.2 → tango_python-0.4.4}/CHANGELOG.md +17 -0
  2. {tango_python-0.4.2 → tango_python-0.4.4}/PKG-INFO +1 -1
  3. {tango_python-0.4.2 → tango_python-0.4.4}/pyproject.toml +1 -1
  4. {tango_python-0.4.2 → tango_python-0.4.4}/tango/__init__.py +3 -1
  5. {tango_python-0.4.2 → tango_python-0.4.4}/tango/client.py +49 -1
  6. {tango_python-0.4.2 → tango_python-0.4.4}/tango/exceptions.py +28 -1
  7. {tango_python-0.4.2 → tango_python-0.4.4}/tango/models.py +15 -0
  8. {tango_python-0.4.2 → tango_python-0.4.4}/tests/test_client.py +86 -1
  9. {tango_python-0.4.2 → tango_python-0.4.4}/uv.lock +1 -1
  10. {tango_python-0.4.2 → tango_python-0.4.4}/.env.example +0 -0
  11. {tango_python-0.4.2 → tango_python-0.4.4}/.github/workflows/lint.yml +0 -0
  12. {tango_python-0.4.2 → tango_python-0.4.4}/.github/workflows/publish.yml +0 -0
  13. {tango_python-0.4.2 → tango_python-0.4.4}/.github/workflows/test.yml +0 -0
  14. {tango_python-0.4.2 → tango_python-0.4.4}/.gitignore +0 -0
  15. {tango_python-0.4.2 → tango_python-0.4.4}/LICENSE +0 -0
  16. {tango_python-0.4.2 → tango_python-0.4.4}/README.md +0 -0
  17. {tango_python-0.4.2 → tango_python-0.4.4}/ROADMAP.md +0 -0
  18. {tango_python-0.4.2 → tango_python-0.4.4}/docs/API_REFERENCE.md +0 -0
  19. {tango_python-0.4.2 → tango_python-0.4.4}/docs/DEVELOPERS.md +0 -0
  20. {tango_python-0.4.2 → tango_python-0.4.4}/docs/SHAPES.md +0 -0
  21. {tango_python-0.4.2 → tango_python-0.4.4}/docs/quick_start.ipynb +0 -0
  22. {tango_python-0.4.2 → tango_python-0.4.4}/scripts/README.md +0 -0
  23. {tango_python-0.4.2 → tango_python-0.4.4}/scripts/check_filter_shape_conformance.py +0 -0
  24. {tango_python-0.4.2 → tango_python-0.4.4}/scripts/fetch_api_schema.py +0 -0
  25. {tango_python-0.4.2 → tango_python-0.4.4}/scripts/generate_schemas_from_api.py +0 -0
  26. {tango_python-0.4.2 → tango_python-0.4.4}/scripts/pr_review.py +0 -0
  27. {tango_python-0.4.2 → tango_python-0.4.4}/scripts/test_production.py +0 -0
  28. {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/__init__.py +0 -0
  29. {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/explicit_schemas.py +0 -0
  30. {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/factory.py +0 -0
  31. {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/generator.py +0 -0
  32. {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/models.py +0 -0
  33. {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/parser.py +0 -0
  34. {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/schema.py +0 -0
  35. {tango_python-0.4.2 → tango_python-0.4.4}/tango/shapes/types.py +0 -0
  36. {tango_python-0.4.2 → tango_python-0.4.4}/tests/__init__.py +0 -0
  37. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
  38. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
  39. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
  40. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
  41. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
  42. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
  43. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
  44. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
  45. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
  46. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
  47. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
  48. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
  49. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
  50. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
  51. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
  52. {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
  53. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
  54. {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
  55. {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
  56. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
  57. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
  58. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
  59. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
  60. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
  61. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
  62. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
  63. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
  64. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
  65. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
  66. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
  67. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
  68. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
  69. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
  70. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
  71. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
  72. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
  73. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
  74. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
  75. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
  76. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
  77. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
  78. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
  79. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
  80. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
  81. {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
  82. {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
  83. {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
  84. {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
  85. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
  86. {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
  87. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
  88. {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
  89. {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
  90. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
  91. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
  92. {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
  93. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
  94. {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
  95. {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
  96. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
  97. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
  98. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
  99. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
  100. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
  101. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
  102. {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
  103. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
  104. {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
  105. {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
  106. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
  107. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
  108. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
  109. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
  110. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
  111. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
  112. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
  113. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
  114. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
  115. {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
  116. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
  117. {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
  118. {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
  119. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
  120. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
  121. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
  122. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
  123. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
  124. {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
  125. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
  126. {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
  127. {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
  128. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
  129. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
  130. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
  131. {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
  132. {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
  133. {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
  134. {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
  135. {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
  136. {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
  137. {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
  138. {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
  139. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
  140. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
  141. {tango_python-0.4.2 → tango_python-0.4.4}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
  142. {tango_python-0.4.2 → tango_python-0.4.4}/tests/conftest.py +0 -0
  143. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/README.md +0 -0
  144. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/__init__.py +0 -0
  145. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/conftest.py +0 -0
  146. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_agencies_integration.py +0 -0
  147. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_contracts_integration.py +0 -0
  148. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_edge_cases_integration.py +0 -0
  149. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_entities_integration.py +0 -0
  150. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_forecasts_integration.py +0 -0
  151. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_grants_integration.py +0 -0
  152. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_naics_integration.py +0 -0
  153. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_notices_integration.py +0 -0
  154. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_offices_integration.py +0 -0
  155. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_opportunities_integration.py +0 -0
  156. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_organizations_integration.py +0 -0
  157. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_otas_otidvs_integration.py +0 -0
  158. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_protests_integration.py +0 -0
  159. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_reference_data_integration.py +0 -0
  160. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_subawards_integration.py +0 -0
  161. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/test_vehicles_idvs_integration.py +0 -0
  162. {tango_python-0.4.2 → tango_python-0.4.4}/tests/integration/validation.py +0 -0
  163. {tango_python-0.4.2 → tango_python-0.4.4}/tests/production/__init__.py +0 -0
  164. {tango_python-0.4.2 → tango_python-0.4.4}/tests/production/conftest.py +0 -0
  165. {tango_python-0.4.2 → tango_python-0.4.4}/tests/production/test_production_smoke.py +0 -0
  166. {tango_python-0.4.2 → tango_python-0.4.4}/tests/test_models.py +0 -0
  167. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tango-python
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Python SDK for the Tango API
5
5
  Project-URL: Homepage, https://github.com/makegov/tango-python
6
6
  Project-URL: Documentation, https://docs.makegov.com/tango-python
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tango-python"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Python SDK for the Tango API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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.1"
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
- raise TangoRateLimitError("Rate limit exceeded", response.status_code)
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
- pass
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):
@@ -1831,7 +1831,7 @@ wheels = [
1831
1831
 
1832
1832
  [[package]]
1833
1833
  name = "tango-python"
1834
- version = "0.4.2"
1834
+ version = "0.4.4"
1835
1835
  source = { editable = "." }
1836
1836
  dependencies = [
1837
1837
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes