tango-python 1.8.0__tar.gz → 1.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tango_python-1.8.0 → tango_python-1.10.0}/CHANGELOG.md +14 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/PKG-INFO +10 -1
- {tango_python-1.8.0 → tango_python-1.10.0}/README.md +9 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/docs/API_REFERENCE.md +115 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/docs/WEBHOOKS.md +1 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/pyproject.toml +1 -1
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/check_filter_shape_conformance.py +1 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/check_shape_coverage.py +1 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/__init__.py +9 -1
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/client.py +262 -45
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/exceptions.py +10 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/models.py +112 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/shapes/explicit_schemas.py +93 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/shapes/generated_overlay.py +8 -19
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_dibbs_exclusions_sbir.py +6 -0
- tango_python-1.10.0/tests/test_ebuy.py +322 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/.gitignore +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/LICENSE +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/docs/CLIENT.md +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/docs/DYNAMIC_MODELS.md +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/docs/ERRORS.md +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/docs/PAGINATION.md +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/docs/SHAPES.md +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/README.md +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/fetch_api_schema.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/generate_schemas_from_api.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/generate_shape_overlay.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/pr_review.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/probe_shape_types.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/refresh_contract.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/smoke_api_parity.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/scripts/test_production.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/shapes/__init__.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/shapes/factory.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/shapes/generator.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/shapes/models.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/shapes/parser.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/shapes/schema.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/shapes/types.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/webhooks/__init__.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/webhooks/cli.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/webhooks/receiver.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/webhooks/signing.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tango/webhooks/simulate.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/__init__.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[custom-key,piid,recipient(display_name),total_contract_value,award_date] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[detailed-key,piid,award_date,description,total_contract_value,obligated,fiscal_year,set_aside,recipient(display_name,uei),awarding_office(-),place_of_performa...ce114a3c47e2037aaa3c15d00b7031bd +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_budget_flows +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[comprehensive-uei,legal_business_name,dba_name,cage_code,business_types,primary_naics,naics_codes,psc_codes,email_address,entity_url,description,capabilities,ke...1603a7d52e211cf2b3bc7d32080238aa +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[custom-uei,legal_business_name,cage_code] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[custom-id,title,anticipated_award_date] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[detailed-id,source_system,external_id,title,description,anticipated_award_date,fiscal_year,naics_code,status,is_active] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[minimal-id,title,anticipated_award_date,fiscal_year,naics_code,status] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[custom-grant_id,title,opportunity_number] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[detailed-grant_id,opportunity_number,title,status(-),agency_code,description,last_updated,cfda_numbers(number,title),applicant_types(-),funding_categories(-)] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[minimal-grant_id,opportunity_number,title,status(-),agency_code] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_code +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_name_text +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating_max +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_performance_risk +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_type_of_investment +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_updated_time_range +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_funding_and_cio_evaluation_expansions +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_get_itdashboard_investment_by_uii +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_itdashboard_pagination +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_search +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[custom-uii,agency_name,investment_title,updated_time] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[default-None] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[minimal-uii,agency_name,bureau_name,investment_title,type_of_investment,part_of_it_portfolio,updated_time,url] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[custom-notice_id,title,solicitation_number] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[detailed-notice_id,title,description,solicitation_number,posted_date,naics_code,set_aside,office(-),place_of_performance(-)] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[custom-opportunity_id,title,solicitation_number] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[detailed-opportunity_id,title,description,solicitation_number,response_deadline,first_notice_date,last_notice_date,active,naics_code,psc_code,set_asid...23b6b4502ddd665b7184afcff6c6d8d9 +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[custom-case_id,title,source_system,outcome] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[minimal-case_id,case_number,title,source_system,outcome,filed_date] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[with_dockets-case_id,case_number,title,outcome,filed_date,dockets(docket_number,filed_date,outcome)] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[ultra_minimal-key,piid,recipient(display_name),total_contract_value] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[detailed-notice_id,title,description,solicitation_number,posted_date,naics_code,set_aside,office(-),place_of_performance(-)] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[detailed-opportunity_id,title,description,solicitation_number,response_deadline,first_notice_date,last_notice_date,active,naics_code,psc_code,set_aside,sam_url,office(-),place_of_performance(-)] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_with_metrics_expansion +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_orders_uses_default_shape +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_with_ordering +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/conftest.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/README.md +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/__init__.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/conftest.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_agencies_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_contracts_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_edge_cases_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_entities_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_forecasts_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_grants_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_itdashboard_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_naics_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_notices_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_offices_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_opportunities_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_organizations_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_otas_otidvs_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_protests_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_reference_data_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_subawards_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/test_vehicles_idvs_integration.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/integration/validation.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/production/__init__.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/production/conftest.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/production/test_production_smoke.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_api_parity.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_client.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_contract_appeals.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_federal_register.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_models.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_opportunity_attachments.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_organization_scope.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_shape_coverage_gate.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_shapes.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_sled.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_webhooks_cli.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_webhooks_receiver.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_webhooks_signing.py +0 -0
- {tango_python-1.8.0 → tango_python-1.10.0}/tests/test_webhooks_simulate.py +0 -0
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.10.0] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`list_sbir_topics(cycle_name=...)`** filters topics by their DSIP solicitation cycle (e.g. `DOD_SBIR_2026_P1_CBZ`), and `SbirTopic` gains a `cycle_name` field (Tango API 5.3.1). The vendored API contract is refreshed to 5.3.1.
|
|
15
|
+
|
|
16
|
+
## [1.9.0] - 2026-09-28
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **GSA eBuy requests** (Tango API 5.1.0). Four methods over the `/api/ebuy/` resource: `list_ebuy_requests()`, `get_ebuy_request()`, `get_ebuy_attachment_url()` and `get_ebuy_access()`, plus `EbuyRequest` / `EbuyAttachment` schemas, an `EbuyAccess` result type, and the `EBUY_REQUESTS_MINIMAL` / `EBUY_REQUESTS_COMPREHENSIVE` defaults. These are the RFQs, RFPs and RFIs posted to GSA eBuy. All fourteen of the API's filters are explicit named parameters, including `search` (which also matches attachment text), `reference_number`, `sin`, `agency` and `contract_number`.
|
|
20
|
+
|
|
21
|
+
Access is scoped to your account: you see only requests posted under the GSA schedule contracts linked to it, and the endpoints require the Pro tier or above. With no linked contract, `list_ebuy_requests()` returns an empty page rather than an error, so `get_ebuy_access()` reports whether access is enabled, why not (`tier_required` or `no_contract_grant`), and which contracts are linked. `status` is frozen at the last state a request was seen in, because a request that closes stops appearing rather than getting a final row; read `last_seen` for staleness. `get_ebuy_attachment_url()` returns the short-lived signed URL the API redirects to without downloading the document, and raises the new `TangoAttachmentLinkError` (a `TangoValidationError`) with the link on `.url` when the entry is an external link rather than a stored document. The `agency` filter requires Tango API 5.3.0.
|
|
22
|
+
- **`alerts.ebuy_request.match` is an alertable event type.** `create_webhook_alert(query_type="ebuy_request", ...)` works.
|
|
23
|
+
|
|
10
24
|
## [1.8.0] - 2026-09-28
|
|
11
25
|
|
|
12
26
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tango-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.0
|
|
4
4
|
Summary: Python SDK for the Tango API
|
|
5
5
|
Project-URL: Homepage, https://github.com/makegov/tango-python
|
|
6
6
|
Project-URL: Documentation, https://docs.makegov.com/tango-python
|
|
@@ -308,6 +308,15 @@ protests = client.list_protests(source_system="gao", outcome="Sustained", limit=
|
|
|
308
308
|
protest = client.get_protest("CASE_UUID")
|
|
309
309
|
```
|
|
310
310
|
|
|
311
|
+
### GSA eBuy
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
access = client.get_ebuy_access() # scoped to the schedule contracts linked to your account
|
|
315
|
+
requests = client.list_ebuy_requests(status="Open", sin="54151S", limit=25)
|
|
316
|
+
request = client.get_ebuy_request("RFQ1835158")
|
|
317
|
+
url = client.get_ebuy_attachment_url("RFQ1835158", doc_seq_num=1) # short-lived signed URL
|
|
318
|
+
```
|
|
319
|
+
|
|
311
320
|
### GSA eLibrary Contracts
|
|
312
321
|
|
|
313
322
|
```python
|
|
@@ -254,6 +254,15 @@ protests = client.list_protests(source_system="gao", outcome="Sustained", limit=
|
|
|
254
254
|
protest = client.get_protest("CASE_UUID")
|
|
255
255
|
```
|
|
256
256
|
|
|
257
|
+
### GSA eBuy
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
access = client.get_ebuy_access() # scoped to the schedule contracts linked to your account
|
|
261
|
+
requests = client.list_ebuy_requests(status="Open", sin="54151S", limit=25)
|
|
262
|
+
request = client.get_ebuy_request("RFQ1835158")
|
|
263
|
+
url = client.get_ebuy_attachment_url("RFQ1835158", doc_seq_num=1) # short-lived signed URL
|
|
264
|
+
```
|
|
265
|
+
|
|
257
266
|
### GSA eLibrary Contracts
|
|
258
267
|
|
|
259
268
|
```python
|
|
@@ -24,6 +24,7 @@ Complete reference for all Tango Python SDK methods and functionality.
|
|
|
24
24
|
- [Protests](#protests)
|
|
25
25
|
- [Contract Appeals](#contract-appeals)
|
|
26
26
|
- [Federal Register](#federal-register)
|
|
27
|
+
- [GSA eBuy](#gsa-ebuy)
|
|
27
28
|
- [Budget](#budget)
|
|
28
29
|
- [Business Types](#business-types)
|
|
29
30
|
- [NAICS](#naics)
|
|
@@ -1602,6 +1603,118 @@ forecast = client.get_sled_forecast("FORECAST_UUID")
|
|
|
1602
1603
|
|
|
1603
1604
|
---
|
|
1604
1605
|
|
|
1606
|
+
## GSA eBuy
|
|
1607
|
+
|
|
1608
|
+
GSA eBuy requests for quotes, proposals and information (RFQs, RFPs, RFIs), keyed by `rfq_id`.
|
|
1609
|
+
|
|
1610
|
+
**Access is scoped to your account.** You see only requests posted under the GSA schedule contracts linked to your account, and the endpoints require the Pro tier or above (below it they return 403). With no linked contract, `list_ebuy_requests()` returns an empty page rather than an error, and `get_ebuy_request()` raises `TangoNotFoundError` for a request outside your scope, the same as for an id that does not exist. Use `get_ebuy_access()` to tell "no access" from "no matches".
|
|
1611
|
+
|
|
1612
|
+
### list_ebuy_requests()
|
|
1613
|
+
|
|
1614
|
+
List requests with filtering and shaping.
|
|
1615
|
+
|
|
1616
|
+
```python
|
|
1617
|
+
requests = client.list_ebuy_requests(
|
|
1618
|
+
page=1,
|
|
1619
|
+
limit=25,
|
|
1620
|
+
shape=ShapeConfig.EBUY_REQUESTS_MINIMAL,
|
|
1621
|
+
# Filter parameters (all optional)
|
|
1622
|
+
search=None,
|
|
1623
|
+
rfq_id=None,
|
|
1624
|
+
reference_number=None,
|
|
1625
|
+
request_type=None,
|
|
1626
|
+
status=None,
|
|
1627
|
+
sin=None,
|
|
1628
|
+
schedule=None,
|
|
1629
|
+
buyer_agency=None,
|
|
1630
|
+
agency=None,
|
|
1631
|
+
contract_number=None,
|
|
1632
|
+
issue_date_after=None,
|
|
1633
|
+
issue_date_before=None,
|
|
1634
|
+
close_date_after=None,
|
|
1635
|
+
close_date_before=None,
|
|
1636
|
+
ordering=None,
|
|
1637
|
+
)
|
|
1638
|
+
```
|
|
1639
|
+
|
|
1640
|
+
**Filter Parameters:**
|
|
1641
|
+
- `search` - Full-text search over the title, description, reference number, request id and attachment text. Results rank by relevance unless `ordering` is given
|
|
1642
|
+
- `rfq_id` - Exact request id, e.g. `"RFQ1835158"`
|
|
1643
|
+
- `reference_number` - The buyer's own solicitation number; dashes are ignored
|
|
1644
|
+
- `request_type` - `"RFQ"`, `"RFP"` or `"RFI"`
|
|
1645
|
+
- `status` - `"Open"` or `"Cancelled"`, as last seen (see the note below)
|
|
1646
|
+
- `sin` - Special Item Number, e.g. `"54151S"`
|
|
1647
|
+
- `schedule` - GSA schedule
|
|
1648
|
+
- `buyer_agency` - The buyer agency as eBuy names it (free text)
|
|
1649
|
+
- `agency` - A Tango agency name, abbreviation, code or organization key, e.g. `"GSA"`. Matches the whole organization subtree, so a department includes its sub-agencies. Requires Tango API 5.3.0
|
|
1650
|
+
- `contract_number` - Narrow to requests posted under one of your own linked contracts. A contract not linked to your account returns an empty page, not an error
|
|
1651
|
+
- `issue_date_after` / `issue_date_before` - Issue date range (`YYYY-MM-DD`, inclusive)
|
|
1652
|
+
- `close_date_after` / `close_date_before` - Close date range (`YYYY-MM-DD`, inclusive)
|
|
1653
|
+
- `ordering` - `issue_date` (the default, as `-issue_date`), `close_date`, `last_seen` or `modified`; prefix `-` for descending
|
|
1654
|
+
|
|
1655
|
+
String filters accept several values joined with `|` (OR).
|
|
1656
|
+
|
|
1657
|
+
**Returns:** [PaginatedResponse](#paginatedresponse) with request dictionaries
|
|
1658
|
+
|
|
1659
|
+
**Example:**
|
|
1660
|
+
```python
|
|
1661
|
+
requests = client.list_ebuy_requests(status="Open", sin="54151S", ordering="close_date")
|
|
1662
|
+
|
|
1663
|
+
for req in requests.results:
|
|
1664
|
+
print(f"{req['rfq_id']} closes {req['close_date']}: {req['title']}")
|
|
1665
|
+
print(f" last seen {req['last_seen']}")
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1668
|
+
**Notes:**
|
|
1669
|
+
- **`status` is frozen at the last state the request was seen in.** Only currently-active requests are carried, so a request that closes stops appearing rather than getting a final row. `Open` means "open the last time it was seen", not "open now"; read `last_seen` for staleness.
|
|
1670
|
+
- The contract number a request was posted under is never returned in any payload.
|
|
1671
|
+
- `buyer_agency_code` and some other buyer and contact fields are sparse on older requests.
|
|
1672
|
+
|
|
1673
|
+
### get_ebuy_request()
|
|
1674
|
+
|
|
1675
|
+
Get a single request by `rfq_id`.
|
|
1676
|
+
|
|
1677
|
+
```python
|
|
1678
|
+
request = client.get_ebuy_request(
|
|
1679
|
+
"RFQ1835158",
|
|
1680
|
+
shape=ShapeConfig.EBUY_REQUESTS_COMPREHENSIVE,
|
|
1681
|
+
)
|
|
1682
|
+
|
|
1683
|
+
for attachment in request["attachments"]:
|
|
1684
|
+
print(attachment["doc_seq_num"], attachment["doc_name"], attachment["is_link"])
|
|
1685
|
+
```
|
|
1686
|
+
|
|
1687
|
+
The default shape returns every field plus two expands: `organization` (the buyer office, with the same seven keys as other resources' `organization` expand) and `attachments`. Each attachment carries `doc_seq_num`, `doc_name`, `doc_type`, `doc_path`, `is_link` and `doc_session_date`. `is_link=True` means `doc_path` is an outbound URL with no stored document behind it. `amendments`, `line_items` and `addresses` are lists of objects, served as eBuy publishes them.
|
|
1688
|
+
|
|
1689
|
+
### get_ebuy_attachment_url()
|
|
1690
|
+
|
|
1691
|
+
Get a short-lived download URL for one stored attachment.
|
|
1692
|
+
|
|
1693
|
+
```python
|
|
1694
|
+
url = client.get_ebuy_attachment_url("RFQ1835158", doc_seq_num=1)
|
|
1695
|
+
```
|
|
1696
|
+
|
|
1697
|
+
**Returns:** The signed URL the API redirects to, as a string. The SDK reads the redirect without following it, so no document is downloaded. The URL expires after about five minutes: fetch it promptly, and call this again rather than storing it.
|
|
1698
|
+
|
|
1699
|
+
**Raises:**
|
|
1700
|
+
- `TangoAttachmentLinkError` (a `TangoValidationError`) - The entry is an external link (`is_link`), not a stored document. The link is on `error.url`
|
|
1701
|
+
- `TangoNotFoundError` - The request is unknown or outside your scope, the attachment does not exist, or its document has not been captured yet
|
|
1702
|
+
|
|
1703
|
+
### get_ebuy_access()
|
|
1704
|
+
|
|
1705
|
+
Check whether your account can read eBuy requests.
|
|
1706
|
+
|
|
1707
|
+
```python
|
|
1708
|
+
access = client.get_ebuy_access()
|
|
1709
|
+
if not access.enabled:
|
|
1710
|
+
print(access.reason) # "tier_required" or "no_contract_grant"
|
|
1711
|
+
print(access.contracts) # your own linked contracts, sorted
|
|
1712
|
+
```
|
|
1713
|
+
|
|
1714
|
+
**Returns:** `EbuyAccess` with `enabled` (bool), `reason` (`"tier_required"`, `"no_contract_grant"` or `None`; `tier_required` wins when both apply) and `contracts` (list of str).
|
|
1715
|
+
|
|
1716
|
+
---
|
|
1717
|
+
|
|
1605
1718
|
## Budget
|
|
1606
1719
|
|
|
1607
1720
|
Federal account × fiscal year budget rollups, covering the full budget lifecycle (requested → enacted → apportioned → obligated → outlayed), pre-computed ratios and trends, the contract / assistance / unlinked breakdown, and request-vs-actual spend.
|
|
@@ -2401,6 +2514,8 @@ entity = client.get_entity("UEI_KEY", shape=ShapeConfig.ENTITIES_COMPREHENSIVE)
|
|
|
2401
2514
|
| `CONTRACT_APPEALS_COMPREHENSIVE` | `get_contract_appeal` | The list fields plus docket_raw, docket_source, decision_date_repaired, decision_type_raw, listing_year, first_listed_at, listed, text_status, text_char_count (omits `decision_text`, which needs an Enterprise plan) |
|
|
2402
2515
|
| `FEDERAL_REGISTER_MINIMAL` | `list_federal_register_documents` | uuid, document_number, publication_date, type, subtype, title, abstract, action, agencies, cfr_references, citation, significant, comments_close_on, effective_on, html_url, pdf_url |
|
|
2403
2516
|
| `FEDERAL_REGISTER_COMPREHENSIVE` | `get_federal_register_document` | The list fields plus dates, signing_date, start_page, end_page, volume, docket_ids, dockets, regulation_id_numbers, topics, correction_of, corrections, executive_order_number, presidential_document_number, proclamation_number, comment_url, regulations_dot_gov_url, raw_text_url, body_html_url (omits `full_text`) |
|
|
2517
|
+
| `EBUY_REQUESTS_MINIMAL` | `list_ebuy_requests` | rfq_id, request_type, title, schedule, sin, status, buyer_name, buyer_agency, buyer_agency_code, reference_number, issue_date, close_date, attachment_count, link_count, last_seen |
|
|
2518
|
+
| `EBUY_REQUESTS_COMPREHENSIVE` | `get_ebuy_request` | Every field, plus the `organization` and `attachments` expands |
|
|
2404
2519
|
| `BUDGET_ACCOUNTS_MINIMAL` | `list_budget_accounts`, `get_budget_account` | id, federal_account_symbol, fiscal_year, agency_code/name, bureau_name, account_title, bea_category, on_off_budget, subfunction_code, lifecycle (requested/enacted/apportioned/obligated/outlayed/unobligated), contract & assistance rollups, key ratios, next-year growth |
|
|
2405
2520
|
| `VEHICLE_ORDERS_MINIMAL` | `list_vehicle_orders` | key, piid, award_date, recipient(display_name,uei), total_contract_value, obligated |
|
|
2406
2521
|
| `ITDASHBOARD_INVESTMENTS_MINIMAL` | `list_itdashboard_investments` | Minimal IT Dashboard investment fields |
|
|
@@ -90,6 +90,7 @@ tango webhooks list-event-types
|
|
|
90
90
|
# alerts.sled_opportunity.match State/local/education solicitation matched a saved alert
|
|
91
91
|
# alerts.contract_appeal.match CBCA/ASBCA appeal decision matched a saved alert
|
|
92
92
|
# alerts.federal_register.match Federal Register document matched a saved alert
|
|
93
|
+
# alerts.ebuy_request.match GSA eBuy request matched a saved alert
|
|
93
94
|
```
|
|
94
95
|
|
|
95
96
|
This list is served by the API, so it is always current — the SDK does not hardcode
|
|
@@ -79,6 +79,7 @@ RESOURCE_TO_METHOD: dict[str, str | None] = {
|
|
|
79
79
|
"protests": "list_protests",
|
|
80
80
|
"contract_appeals": "list_contract_appeals",
|
|
81
81
|
"federal_register": "list_federal_register_documents",
|
|
82
|
+
"ebuy/requests": "list_ebuy_requests",
|
|
82
83
|
"offices": "list_offices",
|
|
83
84
|
"psc": "list_psc",
|
|
84
85
|
"mas_sins": "list_mas_sins",
|
|
@@ -70,6 +70,7 @@ RESOURCE_TO_MODEL: dict[str, str] = {
|
|
|
70
70
|
"protests": "Protest",
|
|
71
71
|
"contract_appeals": "ContractAppeal",
|
|
72
72
|
"federal_register": "FederalRegisterDocument",
|
|
73
|
+
"ebuy/requests": "EbuyRequest",
|
|
73
74
|
"offices": "Office",
|
|
74
75
|
"assistance_listings": "AssistanceListing",
|
|
75
76
|
"business_types": "BusinessType",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .client import TangoClient
|
|
4
4
|
from .exceptions import (
|
|
5
5
|
TangoAPIError,
|
|
6
|
+
TangoAttachmentLinkError,
|
|
6
7
|
TangoAuthError,
|
|
7
8
|
TangoNotFoundError,
|
|
8
9
|
TangoRateLimitError,
|
|
@@ -14,6 +15,9 @@ from .models import (
|
|
|
14
15
|
DibbsAward,
|
|
15
16
|
DibbsRfp,
|
|
16
17
|
DibbsRfq,
|
|
18
|
+
EbuyAccess,
|
|
19
|
+
EbuyAttachment,
|
|
20
|
+
EbuyRequest,
|
|
17
21
|
Exclusion,
|
|
18
22
|
FederalRegisterDocument,
|
|
19
23
|
GsaElibraryContract,
|
|
@@ -55,13 +59,14 @@ from .webhooks import (
|
|
|
55
59
|
)
|
|
56
60
|
from .webhooks.receiver import Delivery, WebhookReceiver
|
|
57
61
|
|
|
58
|
-
__version__ = "1.
|
|
62
|
+
__version__ = "1.10.0"
|
|
59
63
|
__all__ = [
|
|
60
64
|
"TangoClient",
|
|
61
65
|
"TangoAPIError",
|
|
62
66
|
"TangoAuthError",
|
|
63
67
|
"TangoNotFoundError",
|
|
64
68
|
"TangoValidationError",
|
|
69
|
+
"TangoAttachmentLinkError",
|
|
65
70
|
"TangoRateLimitError",
|
|
66
71
|
"RateLimitInfo",
|
|
67
72
|
"ResolveCandidate",
|
|
@@ -69,6 +74,9 @@ __all__ = [
|
|
|
69
74
|
"BudgetAccount",
|
|
70
75
|
"ContractAppeal",
|
|
71
76
|
"FederalRegisterDocument",
|
|
77
|
+
"EbuyAccess",
|
|
78
|
+
"EbuyAttachment",
|
|
79
|
+
"EbuyRequest",
|
|
72
80
|
"DibbsAward",
|
|
73
81
|
"DibbsRfp",
|
|
74
82
|
"DibbsRfq",
|
|
@@ -5,12 +5,13 @@ import warnings
|
|
|
5
5
|
from datetime import date, datetime
|
|
6
6
|
from decimal import Decimal
|
|
7
7
|
from typing import Any, Literal, cast
|
|
8
|
-
from urllib.parse import urljoin
|
|
8
|
+
from urllib.parse import quote, urljoin
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
11
|
|
|
12
12
|
from tango.exceptions import (
|
|
13
13
|
TangoAPIError,
|
|
14
|
+
TangoAttachmentLinkError,
|
|
14
15
|
TangoAuthError,
|
|
15
16
|
TangoNotFoundError,
|
|
16
17
|
TangoRateLimitError,
|
|
@@ -28,6 +29,8 @@ from tango.models import (
|
|
|
28
29
|
DibbsAward,
|
|
29
30
|
DibbsRfp,
|
|
30
31
|
DibbsRfq,
|
|
32
|
+
EbuyAccess,
|
|
33
|
+
EbuyRequest,
|
|
31
34
|
Entity,
|
|
32
35
|
Exclusion,
|
|
33
36
|
FederalRegisterDocument,
|
|
@@ -175,55 +178,57 @@ class TangoClient:
|
|
|
175
178
|
self._last_response_headers = response.headers
|
|
176
179
|
self._last_rate_limit_info = self._parse_rate_limit_headers(response.headers)
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
raise TangoAuthError(
|
|
180
|
-
"Invalid API key or authentication required", response.status_code
|
|
181
|
-
)
|
|
182
|
-
elif response.status_code == 404:
|
|
183
|
-
raise TangoNotFoundError("Resource not found", response.status_code)
|
|
184
|
-
elif response.status_code == 400:
|
|
185
|
-
error_data = response.json() if response.content else {}
|
|
186
|
-
error_msg = "Invalid request parameters"
|
|
187
|
-
if error_data:
|
|
188
|
-
# Try to extract a more specific error message
|
|
189
|
-
if isinstance(error_data, dict):
|
|
190
|
-
detail = (
|
|
191
|
-
error_data.get("detail")
|
|
192
|
-
or error_data.get("message")
|
|
193
|
-
or error_data.get("error")
|
|
194
|
-
)
|
|
195
|
-
if detail:
|
|
196
|
-
error_msg = f"Invalid request parameters: {detail}"
|
|
197
|
-
issues = error_data.get("issues")
|
|
198
|
-
if isinstance(issues, list):
|
|
199
|
-
rejected = [
|
|
200
|
-
f"{issue['path']} ({issue['reason']})"
|
|
201
|
-
if issue.get("reason")
|
|
202
|
-
else str(issue["path"])
|
|
203
|
-
for issue in issues
|
|
204
|
-
if isinstance(issue, dict) and issue.get("path")
|
|
205
|
-
]
|
|
206
|
-
if rejected:
|
|
207
|
-
error_msg = f"{error_msg}: {', '.join(rejected)}"
|
|
208
|
-
raise TangoValidationError(
|
|
209
|
-
error_msg,
|
|
210
|
-
response.status_code,
|
|
211
|
-
error_data,
|
|
212
|
-
)
|
|
213
|
-
elif response.status_code == 429:
|
|
214
|
-
error_data = response.json() if response.content else {}
|
|
215
|
-
detail = error_data.get("detail", "Rate limit exceeded")
|
|
216
|
-
raise TangoRateLimitError(detail, response.status_code, error_data)
|
|
217
|
-
elif not response.is_success:
|
|
218
|
-
raise TangoAPIError(
|
|
219
|
-
f"API request failed with status {response.status_code}", response.status_code
|
|
220
|
-
)
|
|
181
|
+
self._raise_for_status(response)
|
|
221
182
|
|
|
222
183
|
return response.json() if response.content else {}
|
|
223
184
|
|
|
224
185
|
except httpx.HTTPError as e:
|
|
225
186
|
raise TangoAPIError(f"Request failed: {str(e)}") from e
|
|
226
187
|
|
|
188
|
+
def _raise_for_status(self, response: httpx.Response) -> None:
|
|
189
|
+
"""Raise the SDK exception matching a non-success response; return quietly on success."""
|
|
190
|
+
if response.status_code == 401:
|
|
191
|
+
raise TangoAuthError("Invalid API key or authentication required", response.status_code)
|
|
192
|
+
elif response.status_code == 404:
|
|
193
|
+
raise TangoNotFoundError("Resource not found", response.status_code)
|
|
194
|
+
elif response.status_code == 400:
|
|
195
|
+
error_data = response.json() if response.content else {}
|
|
196
|
+
error_msg = "Invalid request parameters"
|
|
197
|
+
if error_data:
|
|
198
|
+
# Try to extract a more specific error message
|
|
199
|
+
if isinstance(error_data, dict):
|
|
200
|
+
detail = (
|
|
201
|
+
error_data.get("detail")
|
|
202
|
+
or error_data.get("message")
|
|
203
|
+
or error_data.get("error")
|
|
204
|
+
)
|
|
205
|
+
if detail:
|
|
206
|
+
error_msg = f"Invalid request parameters: {detail}"
|
|
207
|
+
issues = error_data.get("issues")
|
|
208
|
+
if isinstance(issues, list):
|
|
209
|
+
rejected = [
|
|
210
|
+
f"{issue['path']} ({issue['reason']})"
|
|
211
|
+
if issue.get("reason")
|
|
212
|
+
else str(issue["path"])
|
|
213
|
+
for issue in issues
|
|
214
|
+
if isinstance(issue, dict) and issue.get("path")
|
|
215
|
+
]
|
|
216
|
+
if rejected:
|
|
217
|
+
error_msg = f"{error_msg}: {', '.join(rejected)}"
|
|
218
|
+
raise TangoValidationError(
|
|
219
|
+
error_msg,
|
|
220
|
+
response.status_code,
|
|
221
|
+
error_data,
|
|
222
|
+
)
|
|
223
|
+
elif response.status_code == 429:
|
|
224
|
+
error_data = response.json() if response.content else {}
|
|
225
|
+
detail = error_data.get("detail", "Rate limit exceeded")
|
|
226
|
+
raise TangoRateLimitError(detail, response.status_code, error_data)
|
|
227
|
+
elif not response.is_success:
|
|
228
|
+
raise TangoAPIError(
|
|
229
|
+
f"API request failed with status {response.status_code}", response.status_code
|
|
230
|
+
)
|
|
231
|
+
|
|
227
232
|
def _get(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
228
233
|
"""Make a GET request"""
|
|
229
234
|
return self._request("GET", endpoint, params=params)
|
|
@@ -2963,6 +2968,215 @@ class TangoClient:
|
|
|
2963
2968
|
data, shape, FederalRegisterDocument, flat, flat_lists
|
|
2964
2969
|
)
|
|
2965
2970
|
|
|
2971
|
+
# ============================================================================
|
|
2972
|
+
# GSA eBuy requests
|
|
2973
|
+
# ============================================================================
|
|
2974
|
+
|
|
2975
|
+
def list_ebuy_requests(
|
|
2976
|
+
self,
|
|
2977
|
+
page: int = 1,
|
|
2978
|
+
limit: int = 25,
|
|
2979
|
+
shape: str | None = None,
|
|
2980
|
+
flat: bool = False,
|
|
2981
|
+
flat_lists: bool = False,
|
|
2982
|
+
search: str | None = None,
|
|
2983
|
+
rfq_id: str | None = None,
|
|
2984
|
+
reference_number: str | None = None,
|
|
2985
|
+
request_type: str | None = None,
|
|
2986
|
+
status: str | None = None,
|
|
2987
|
+
sin: str | None = None,
|
|
2988
|
+
schedule: str | None = None,
|
|
2989
|
+
buyer_agency: str | None = None,
|
|
2990
|
+
agency: str | None = None,
|
|
2991
|
+
contract_number: str | None = None,
|
|
2992
|
+
issue_date_after: str | None = None,
|
|
2993
|
+
issue_date_before: str | None = None,
|
|
2994
|
+
close_date_after: str | None = None,
|
|
2995
|
+
close_date_before: str | None = None,
|
|
2996
|
+
ordering: str | None = None,
|
|
2997
|
+
) -> PaginatedResponse:
|
|
2998
|
+
"""
|
|
2999
|
+
List GSA eBuy requests (RFQs, RFPs and RFIs).
|
|
3000
|
+
|
|
3001
|
+
Requires the Pro tier or above; below it the API returns 403.
|
|
3002
|
+
Results are scoped to your account: you see only requests posted under the GSA schedule contracts linked to it.
|
|
3003
|
+
With no linked contract the page is empty rather than an error, so use ``get_ebuy_access()`` to tell "no access" from "no matches".
|
|
3004
|
+
|
|
3005
|
+
``status`` is frozen at the last state the request was seen in.
|
|
3006
|
+
Only currently-active requests are carried, so a request that closes stops appearing rather than getting a final row, and ``Open`` means "open the last time it was seen".
|
|
3007
|
+
Read ``last_seen`` for staleness.
|
|
3008
|
+
|
|
3009
|
+
Args:
|
|
3010
|
+
page: Page number
|
|
3011
|
+
limit: Results per page (max 100)
|
|
3012
|
+
shape: Response shape string (defaults to minimal shape)
|
|
3013
|
+
flat: If True, flatten nested objects in shaped response
|
|
3014
|
+
flat_lists: If True, flatten arrays using indexed keys
|
|
3015
|
+
search: Full-text search over the title, description, reference number, request id and attachment text. Results rank by relevance unless ``ordering`` is given
|
|
3016
|
+
rfq_id: Exact request id, e.g. ``RFQ1835158``. OR several with ``|``
|
|
3017
|
+
reference_number: The buyer's own solicitation number; dashes are ignored
|
|
3018
|
+
request_type: ``RFQ``, ``RFP`` or ``RFI``. OR several with ``|``
|
|
3019
|
+
status: ``Open`` or ``Cancelled``, as last seen. OR several with ``|``
|
|
3020
|
+
sin: Special Item Number, e.g. ``54151S``. OR several with ``|``
|
|
3021
|
+
schedule: GSA schedule. OR several with ``|``
|
|
3022
|
+
buyer_agency: The buyer agency as eBuy names it (free text). OR several with ``|``
|
|
3023
|
+
agency: A Tango agency name, abbreviation, code or organization key, e.g. ``GSA``. Matches the whole organization subtree, so a department includes its sub-agencies. OR several with ``|``. Requires Tango API 5.3.0
|
|
3024
|
+
contract_number: Narrow to requests posted under one of your own linked contracts. A contract not linked to your account returns an empty page, not an error
|
|
3025
|
+
issue_date_after: Issued on or after (YYYY-MM-DD)
|
|
3026
|
+
issue_date_before: Issued on or before (YYYY-MM-DD)
|
|
3027
|
+
close_date_after: Closes on or after (YYYY-MM-DD)
|
|
3028
|
+
close_date_before: Closes on or before (YYYY-MM-DD)
|
|
3029
|
+
ordering: Sort field — ``issue_date`` (the default, as ``-issue_date``), ``close_date``, ``last_seen`` or ``modified``. Prefix ``-`` for descending
|
|
3030
|
+
"""
|
|
3031
|
+
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
|
|
3032
|
+
|
|
3033
|
+
if shape is None:
|
|
3034
|
+
shape = ShapeConfig.EBUY_REQUESTS_MINIMAL
|
|
3035
|
+
if shape:
|
|
3036
|
+
params["shape"] = shape
|
|
3037
|
+
if flat:
|
|
3038
|
+
params["flat"] = "true"
|
|
3039
|
+
if flat_lists:
|
|
3040
|
+
params["flat_lists"] = "true"
|
|
3041
|
+
|
|
3042
|
+
for key, val in (
|
|
3043
|
+
("search", search),
|
|
3044
|
+
("rfq_id", rfq_id),
|
|
3045
|
+
("reference_number", reference_number),
|
|
3046
|
+
("request_type", request_type),
|
|
3047
|
+
("status", status),
|
|
3048
|
+
("sin", sin),
|
|
3049
|
+
("schedule", schedule),
|
|
3050
|
+
("buyer_agency", buyer_agency),
|
|
3051
|
+
("agency", agency),
|
|
3052
|
+
("contract_number", contract_number),
|
|
3053
|
+
("issue_date_after", issue_date_after),
|
|
3054
|
+
("issue_date_before", issue_date_before),
|
|
3055
|
+
("close_date_after", close_date_after),
|
|
3056
|
+
("close_date_before", close_date_before),
|
|
3057
|
+
("ordering", ordering),
|
|
3058
|
+
):
|
|
3059
|
+
if val is not None:
|
|
3060
|
+
params[key] = val
|
|
3061
|
+
|
|
3062
|
+
data = self._get("/api/ebuy/requests/", params)
|
|
3063
|
+
|
|
3064
|
+
results = [
|
|
3065
|
+
self._parse_response_with_shape(item, shape, EbuyRequest, flat, flat_lists)
|
|
3066
|
+
for item in data["results"]
|
|
3067
|
+
]
|
|
3068
|
+
|
|
3069
|
+
return PaginatedResponse(
|
|
3070
|
+
count=data["count"],
|
|
3071
|
+
next=data.get("next"),
|
|
3072
|
+
previous=data.get("previous"),
|
|
3073
|
+
results=results,
|
|
3074
|
+
meta=data.get("meta"),
|
|
3075
|
+
)
|
|
3076
|
+
|
|
3077
|
+
def get_ebuy_request(
|
|
3078
|
+
self,
|
|
3079
|
+
rfq_id: str,
|
|
3080
|
+
shape: str | None = None,
|
|
3081
|
+
flat: bool = False,
|
|
3082
|
+
flat_lists: bool = False,
|
|
3083
|
+
) -> Any:
|
|
3084
|
+
"""
|
|
3085
|
+
Get a single GSA eBuy request by its request id.
|
|
3086
|
+
|
|
3087
|
+
A request outside your account's scope raises ``TangoNotFoundError``, the same as an id that does not exist.
|
|
3088
|
+
|
|
3089
|
+
Args:
|
|
3090
|
+
rfq_id: Request id, e.g. ``RFQ1835158``
|
|
3091
|
+
shape: Response shape string (defaults to the comprehensive shape: every field plus the ``organization`` and ``attachments`` expands)
|
|
3092
|
+
flat: If True, flatten nested objects in shaped response
|
|
3093
|
+
flat_lists: If True, flatten arrays using indexed keys
|
|
3094
|
+
"""
|
|
3095
|
+
params: dict[str, Any] = {}
|
|
3096
|
+
if shape is None:
|
|
3097
|
+
shape = ShapeConfig.EBUY_REQUESTS_COMPREHENSIVE
|
|
3098
|
+
if shape:
|
|
3099
|
+
params["shape"] = shape
|
|
3100
|
+
if flat:
|
|
3101
|
+
params["flat"] = "true"
|
|
3102
|
+
if flat_lists:
|
|
3103
|
+
params["flat_lists"] = "true"
|
|
3104
|
+
|
|
3105
|
+
data = self._get(f"/api/ebuy/requests/{quote(rfq_id, safe='')}/", params)
|
|
3106
|
+
return self._parse_response_with_shape(data, shape, EbuyRequest, flat, flat_lists)
|
|
3107
|
+
|
|
3108
|
+
def get_ebuy_attachment_url(self, rfq_id: str, doc_seq_num: int) -> str:
|
|
3109
|
+
"""
|
|
3110
|
+
Get a short-lived download URL for one attachment on a GSA eBuy request.
|
|
3111
|
+
|
|
3112
|
+
The API answers with a redirect to a signed URL that expires after about five minutes; this returns that URL without downloading the document.
|
|
3113
|
+
Fetch it promptly, and call this again rather than storing it.
|
|
3114
|
+
|
|
3115
|
+
Args:
|
|
3116
|
+
rfq_id: Request id, e.g. ``RFQ1835158``
|
|
3117
|
+
doc_seq_num: The attachment's ``doc_seq_num`` from ``get_ebuy_request()``
|
|
3118
|
+
|
|
3119
|
+
Raises:
|
|
3120
|
+
TangoAttachmentLinkError: The entry is an external link (``is_link``), not a stored document. The link is on ``.url`` and in the message. Subclasses ``TangoValidationError``.
|
|
3121
|
+
TangoNotFoundError: The request is unknown or outside your scope, the attachment does not exist, or its document has not been captured yet.
|
|
3122
|
+
"""
|
|
3123
|
+
endpoint = (
|
|
3124
|
+
f"/api/ebuy/requests/{quote(rfq_id, safe='')}/attachments/{int(doc_seq_num)}/download/"
|
|
3125
|
+
)
|
|
3126
|
+
url = urljoin(f"{self.base_url}/", endpoint.lstrip("/"))
|
|
3127
|
+
|
|
3128
|
+
try:
|
|
3129
|
+
response = self.client.request(method="GET", url=url, follow_redirects=False)
|
|
3130
|
+
except httpx.HTTPError as e:
|
|
3131
|
+
raise TangoAPIError(f"Request failed: {str(e)}") from e
|
|
3132
|
+
self._last_response_headers = response.headers
|
|
3133
|
+
self._last_rate_limit_info = self._parse_rate_limit_headers(response.headers)
|
|
3134
|
+
|
|
3135
|
+
location = response.headers.get("Location")
|
|
3136
|
+
if response.is_redirect and location:
|
|
3137
|
+
return str(location)
|
|
3138
|
+
|
|
3139
|
+
if response.status_code in (400, 404):
|
|
3140
|
+
try:
|
|
3141
|
+
error_data = response.json() if response.content else {}
|
|
3142
|
+
except ValueError:
|
|
3143
|
+
error_data = {}
|
|
3144
|
+
if not isinstance(error_data, dict):
|
|
3145
|
+
error_data = {}
|
|
3146
|
+
if response.status_code == 400 and error_data.get("url"):
|
|
3147
|
+
raise TangoAttachmentLinkError(
|
|
3148
|
+
f"Attachment {doc_seq_num} on {rfq_id} is an external link, not a stored document: {error_data['url']}",
|
|
3149
|
+
response.status_code,
|
|
3150
|
+
error_data,
|
|
3151
|
+
)
|
|
3152
|
+
if response.status_code == 404:
|
|
3153
|
+
raise TangoNotFoundError(
|
|
3154
|
+
error_data.get("detail") or "Resource not found",
|
|
3155
|
+
response.status_code,
|
|
3156
|
+
error_data,
|
|
3157
|
+
)
|
|
3158
|
+
|
|
3159
|
+
self._raise_for_status(response)
|
|
3160
|
+
raise TangoAPIError(
|
|
3161
|
+
f"Expected a redirect to the attachment, got status {response.status_code}",
|
|
3162
|
+
response.status_code,
|
|
3163
|
+
)
|
|
3164
|
+
|
|
3165
|
+
def get_ebuy_access(self) -> EbuyAccess:
|
|
3166
|
+
"""
|
|
3167
|
+
Check whether your account can read GSA eBuy requests.
|
|
3168
|
+
|
|
3169
|
+
``list_ebuy_requests()`` returns an empty page, not an error, when no contract is linked; this tells the two apart.
|
|
3170
|
+
``reason`` is ``"tier_required"`` below the Pro tier (it wins when both apply), ``"no_contract_grant"`` when no contract is linked, and ``None`` when ``enabled`` is true.
|
|
3171
|
+
``contracts`` lists your own linked contracts, sorted.
|
|
3172
|
+
"""
|
|
3173
|
+
data = self._get("/api/ebuy/access/")
|
|
3174
|
+
return EbuyAccess(
|
|
3175
|
+
enabled=bool(data.get("enabled")),
|
|
3176
|
+
reason=data.get("reason"),
|
|
3177
|
+
contracts=list(data.get("contracts") or []),
|
|
3178
|
+
)
|
|
3179
|
+
|
|
2966
3180
|
# ============================================================================
|
|
2967
3181
|
# DLA DIBBS (RFQs, RFPs, awards)
|
|
2968
3182
|
# ============================================================================
|
|
@@ -3519,6 +3733,7 @@ class TangoClient:
|
|
|
3519
3733
|
flat_lists: bool = False,
|
|
3520
3734
|
topic_number: str | None = None,
|
|
3521
3735
|
solicitation_number: str | None = None,
|
|
3736
|
+
cycle_name: str | None = None,
|
|
3522
3737
|
agency: str | None = None,
|
|
3523
3738
|
activity: str | None = None,
|
|
3524
3739
|
year: int | None = None,
|
|
@@ -3545,6 +3760,7 @@ class TangoClient:
|
|
|
3545
3760
|
flat_lists: If True, flatten arrays using indexed keys
|
|
3546
3761
|
topic_number: Filter by topic number
|
|
3547
3762
|
solicitation_number: Filter by solicitation number
|
|
3763
|
+
cycle_name: Filter by DSIP solicitation cycle, exact (e.g. "DOD_SBIR_2026_P1_CBZ"; requires Tango API 5.3.1)
|
|
3548
3764
|
agency: Filter by agency
|
|
3549
3765
|
activity: Filter by activity (component/branch)
|
|
3550
3766
|
year: Filter by solicitation year
|
|
@@ -3572,6 +3788,7 @@ class TangoClient:
|
|
|
3572
3788
|
for key, val in (
|
|
3573
3789
|
("topic_number", topic_number),
|
|
3574
3790
|
("solicitation_number", solicitation_number),
|
|
3791
|
+
("cycle_name", cycle_name),
|
|
3575
3792
|
("agency", agency),
|
|
3576
3793
|
("activity", activity),
|
|
3577
3794
|
("year", year),
|
|
@@ -5022,7 +5239,7 @@ class TangoClient:
|
|
|
5022
5239
|
query_type: One of ``opportunity``, ``contract``, ``idv``, ``ota``,
|
|
5023
5240
|
``otidv``, ``entity``, ``grant``, ``forecast``, ``exclusion``,
|
|
5024
5241
|
``dibbs_rfq``, ``dibbs_rfp``, ``sled_opportunity``,
|
|
5025
|
-
``contract_appeal`` or ``
|
|
5242
|
+
``contract_appeal``, ``federal_register`` or ``ebuy_request``. :meth:`list_webhook_event_types` is the
|
|
5026
5243
|
current authority — this list can only go stale.
|
|
5027
5244
|
filters: Dict of query parameters that the alert matches against
|
|
5028
5245
|
(e.g. ``{"naics": "541330", "set_aside": "SBA"}``).
|
|
@@ -53,6 +53,16 @@ class TangoValidationError(TangoAPIError):
|
|
|
53
53
|
return val if isinstance(val, dict) else None
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
class TangoAttachmentLinkError(TangoValidationError):
|
|
57
|
+
"""The requested attachment is an external link, not a stored document, so there is nothing to download."""
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def url(self) -> str | None:
|
|
61
|
+
"""The external link the entry points at."""
|
|
62
|
+
val = self.response_data.get("url")
|
|
63
|
+
return val if isinstance(val, str) else None
|
|
64
|
+
|
|
65
|
+
|
|
56
66
|
class TangoRateLimitError(TangoAPIError):
|
|
57
67
|
"""Rate limit exceeded error"""
|
|
58
68
|
|