tango-python 1.14.0__tar.gz → 1.15.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.14.0 → tango_python-1.15.0}/CHANGELOG.md +11 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/PKG-INFO +10 -1
- {tango_python-1.14.0 → tango_python-1.15.0}/README.md +9 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/docs/API_REFERENCE.md +38 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/pyproject.toml +1 -1
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/__init__.py +1 -1
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/models.py +17 -5
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/shapes/generated_overlay.py +4 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_client.py +16 -0
- tango_python-1.15.0/tests/test_opportunity_attachments.py +166 -0
- tango_python-1.14.0/tests/test_opportunity_attachments.py +0 -72
- {tango_python-1.14.0 → tango_python-1.15.0}/.gitignore +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/LICENSE +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/docs/CLIENT.md +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/docs/DYNAMIC_MODELS.md +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/docs/ERRORS.md +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/docs/PAGINATION.md +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/docs/SHAPES.md +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/docs/WEBHOOKS.md +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/README.md +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/check_filter_shape_conformance.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/check_shape_coverage.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/fetch_api_schema.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/generate_schemas_from_api.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/generate_shape_overlay.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/pr_review.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/probe_shape_types.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/refresh_contract.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/smoke_api_parity.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/scripts/test_production.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/client.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/exceptions.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/shapes/__init__.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/shapes/explicit_schemas.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/shapes/factory.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/shapes/generator.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/shapes/models.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/shapes/parser.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/shapes/schema.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/shapes/types.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/webhooks/__init__.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/webhooks/cli.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/webhooks/receiver.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/webhooks/signing.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tango/webhooks/simulate.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/__init__.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_budget_flows +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[custom-uei,legal_business_name,cage_code] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[custom-id,title,anticipated_award_date] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[custom-grant_id,title,opportunity_number] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[minimal-grant_id,opportunity_number,title,status(-),agency_code] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_code +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_name_text +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating_max +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_performance_risk +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_type_of_investment +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_filter_by_updated_time_range +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_funding_and_cio_evaluation_expansions +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_get_itdashboard_investment_by_uii +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_itdashboard_pagination +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_search +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[custom-uii,agency_name,investment_title,updated_time] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[default-None] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[custom-notice_id,title,solicitation_number] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[custom-opportunity_id,title,solicitation_number] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[custom-case_id,title,source_system,outcome] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[ultra_minimal-key,piid,recipient(display_name),total_contract_value] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.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.14.0 → tango_python-1.15.0}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_with_metrics_expansion +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_orders_uses_default_shape +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_with_ordering +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/conftest.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/README.md +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/__init__.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/conftest.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_agencies_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_contracts_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_edge_cases_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_entities_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_forecasts_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_grants_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_itdashboard_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_naics_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_notices_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_offices_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_opportunities_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_organizations_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_otas_otidvs_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_protests_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_reference_data_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_subawards_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/test_vehicles_idvs_integration.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/integration/validation.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/production/__init__.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/production/conftest.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/production/test_production_smoke.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_api_parity.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_contract_appeals.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_dibbs_exclusions_sbir.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_ebuy.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_federal_register.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_models.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_opportunity_awards.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_organization_scope.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_shape_coverage_gate.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_shapes.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_sled.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_webhooks_cli.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_webhooks_receiver.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_webhooks_signing.py +0 -0
- {tango_python-1.14.0 → tango_python-1.15.0}/tests/test_webhooks_simulate.py +0 -0
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.15.0] - 2026-10-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`matched_by` on agency-filter diagnostics** (Tango API 5.9.0). Each entry of `PaginatedResponse.meta["resolved_filters"][<filter name>]` that resolved now says how its token matched: `key` (an organization UUID), `code` (a 3-digit CGAC or 4-digit FPDS code), `name` (the organization's name, including a department's everyday name, a spelling variant or a rename), `alias` (an abbreviation or the organization's own alias) or `fuzzy` (a looser text match, worth checking against the resolved name). An entry that did not resolve has no `matched_by`. `meta` is passed through as the API sends it, so no code change was needed to receive the field; this release documents it and pins it with a test. `agency_warnings`, `unresolved_agency_tokens` and `resolved_agencies` are unchanged.
|
|
15
|
+
- **File and link counts on opportunities and notices** (Tango API 5.9.0). An attachment's `type` is `file` (a document attached to the notice; only files have extracted text) or `link` (a URL the notice lists), and the new fields count each: `meta.files_count` and `meta.links_count` on opportunities, `file_count` and `link_count` on notices. The existing `meta.attachments_count` and `attachment_count` still count every attachment, links included, and an attachment of any other type counts only there, so files plus links need not equal the total. The new fields are `None` until a record has been counted, so treat `None` as unknown, not zero. All four are accepted in custom shapes, as is `opportunity(meta(files_count,links_count))` on vehicles; no default SDK shape names them.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Re-vendored `contracts/filter_shape_contract.json` (Tango API 5.9.0) and regenerated `tango/shapes/generated_overlay.py`.
|
|
20
|
+
|
|
10
21
|
## [1.14.0] - 2026-10-02
|
|
11
22
|
|
|
12
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tango-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.15.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
|
|
@@ -219,6 +219,15 @@ for warning in response.agency_warnings:
|
|
|
219
219
|
print(warning)
|
|
220
220
|
```
|
|
221
221
|
|
|
222
|
+
Each entry of `response.meta["resolved_filters"][<filter name>]` that resolved also says how its token matched, in `matched_by` (Tango API 5.9.0+): `key` (an organization UUID), `code` (a 3-digit CGAC or 4-digit FPDS code), `name` (the organization's name, including a department's everyday name, a spelling variant or a rename), `alias` (an abbreviation or the organization's own alias) or `fuzzy` (a looser text match).
|
|
223
|
+
A `fuzzy` match is the one worth checking against the resolved name:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
for entry in response.meta["resolved_filters"].get("awarding_agency", []):
|
|
227
|
+
if entry.get("matched_by") == "fuzzy":
|
|
228
|
+
print(f"{entry['token']!r} loosely matched {entry['resolved']['name']}")
|
|
229
|
+
```
|
|
230
|
+
|
|
222
231
|
If *every* token for a filter fails to resolve, the API returns `400` and the SDK raises
|
|
223
232
|
`TangoValidationError` naming the offending value, rather than an empty page.
|
|
224
233
|
|
|
@@ -165,6 +165,15 @@ for warning in response.agency_warnings:
|
|
|
165
165
|
print(warning)
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
+
Each entry of `response.meta["resolved_filters"][<filter name>]` that resolved also says how its token matched, in `matched_by` (Tango API 5.9.0+): `key` (an organization UUID), `code` (a 3-digit CGAC or 4-digit FPDS code), `name` (the organization's name, including a department's everyday name, a spelling variant or a rename), `alias` (an abbreviation or the organization's own alias) or `fuzzy` (a looser text match).
|
|
169
|
+
A `fuzzy` match is the one worth checking against the resolved name:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
for entry in response.meta["resolved_filters"].get("awarding_agency", []):
|
|
173
|
+
if entry.get("matched_by") == "fuzzy":
|
|
174
|
+
print(f"{entry['token']!r} loosely matched {entry['resolved']['name']}")
|
|
175
|
+
```
|
|
176
|
+
|
|
168
177
|
If *every* token for a filter fails to resolve, the API returns `400` and the SDK raises
|
|
169
178
|
`TangoValidationError` naming the offending value, rather than an empty page.
|
|
170
179
|
|
|
@@ -981,6 +981,27 @@ awarded = client.list_opportunities(
|
|
|
981
981
|
- The `awarded` and `awardee_uei` filters search every opportunity, not just active ones, so pass `active=True` to narrow to open opportunities.
|
|
982
982
|
- Notices accept `award_date`, `award_amount`, `awardee` and `awardee_uei` in `shape` too.
|
|
983
983
|
|
|
984
|
+
**Attachment counts — `meta(attachments_count, files_count, links_count)`:**
|
|
985
|
+
|
|
986
|
+
Tango API 5.9.0 splits an opportunity's attachment count by attachment `type`.
|
|
987
|
+
A `file` is a document attached to the notice, and only files have extracted text; a `link` is a URL the notice lists.
|
|
988
|
+
|
|
989
|
+
```python
|
|
990
|
+
opp = client.get_opportunity(
|
|
991
|
+
opportunity_id,
|
|
992
|
+
shape="opportunity_id,title,meta(attachments_count,files_count,links_count)",
|
|
993
|
+
)
|
|
994
|
+
files = opp["meta"]["files_count"]
|
|
995
|
+
if files is None:
|
|
996
|
+
print("not counted yet")
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
- `meta.files_count` counts the files and `meta.links_count` counts the links.
|
|
1000
|
+
- `meta.attachments_count` still counts every attachment, links included. An attachment of any other type counts only there, so `files_count + links_count` need not equal it.
|
|
1001
|
+
- **`None` means unknown, not zero.** Both new fields are null until the record has been counted.
|
|
1002
|
+
- None of the three is in the SDK's default shape, so name the ones you want.
|
|
1003
|
+
- The opportunity embedded in a vehicle accepts them too: `opportunity(meta(files_count,links_count))`.
|
|
1004
|
+
|
|
984
1005
|
---
|
|
985
1006
|
|
|
986
1007
|
## Notices
|
|
@@ -1053,6 +1074,23 @@ for notice in notices.results:
|
|
|
1053
1074
|
- `posted_date` - Date posted
|
|
1054
1075
|
- `naics_code` - Industry code
|
|
1055
1076
|
|
|
1077
|
+
**Attachment counts — `attachment_count`, `file_count`, `link_count`:**
|
|
1078
|
+
|
|
1079
|
+
Tango API 5.9.0 splits a notice's attachment count by attachment `type`.
|
|
1080
|
+
A `file` is a document attached to the notice, and only files have extracted text; a `link` is a URL the notice lists.
|
|
1081
|
+
|
|
1082
|
+
```python
|
|
1083
|
+
notices = client.list_notices(
|
|
1084
|
+
agency="GSA",
|
|
1085
|
+
shape="notice_id,title,attachment_count,file_count,link_count",
|
|
1086
|
+
)
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
- `file_count` counts the files and `link_count` counts the links.
|
|
1090
|
+
- `attachment_count` still counts every attachment, links included. An attachment of any other type counts only there, so `file_count + link_count` need not equal it.
|
|
1091
|
+
- **`None` means unknown, not zero.** Both new fields are null until the notice has been counted.
|
|
1092
|
+
- The API returns all three when no `shape` is sent, but the SDK's default `NOTICES_MINIMAL` shape does not name them, so ask for them.
|
|
1093
|
+
|
|
1056
1094
|
---
|
|
1057
1095
|
|
|
1058
1096
|
## Grants
|
|
@@ -571,6 +571,12 @@ class Opportunity:
|
|
|
571
571
|
The award fields (``award_date``, ``award_amount``, ``awardee``, ``awardee_uei``) are filled only where SAM.gov posted an award notice, and ``award_amount`` is the text SAM.gov published, not a number.
|
|
572
572
|
SAM.gov often posts an award as its own opportunity; when that award notice references its solicitation, ``solicitation_opportunity_id`` points back to it, and the solicitation's ``awards(...)`` expand lists up to ten of the most recent linked awards, with ``award_count`` giving the full count.
|
|
573
573
|
An award notice that does not reference its solicitation is not linked.
|
|
574
|
+
|
|
575
|
+
The ``meta(...)`` expand counts an opportunity's attachments three ways (Tango API 5.9.0).
|
|
576
|
+
An attachment's ``type`` is ``file`` (a document attached to the notice; only files have extracted text) or ``link`` (a URL the notice lists).
|
|
577
|
+
``meta.files_count`` and ``meta.links_count`` count each, and ``meta.attachments_count`` counts every attachment, links included.
|
|
578
|
+
An attachment of any other type counts only in the total, so files plus links need not equal it.
|
|
579
|
+
``files_count`` and ``links_count`` are null until the record has been counted, so treat null as unknown, not zero.
|
|
574
580
|
"""
|
|
575
581
|
|
|
576
582
|
opportunity_id: str
|
|
@@ -600,6 +606,10 @@ class Notice:
|
|
|
600
606
|
Both must be named, e.g. ``attachments(name,url,doc_role,doc_role_alt)``, because ``attachments(*)`` does not include them.
|
|
601
607
|
An attachment Tango has not classified omits both keys rather than returning null, so read them with ``.get()``.
|
|
602
608
|
A Free-plan request that names them gets the response without them, plus an entry in ``meta.upgrade_hints``.
|
|
609
|
+
|
|
610
|
+
``file_count`` and ``link_count`` (Tango API 5.9.0) split ``attachment_count`` by attachment ``type``: ``file`` is a document attached to the notice (only files have extracted text) and ``link`` is a URL the notice lists.
|
|
611
|
+
``attachment_count`` still counts every attachment, links included, and an attachment of any other type counts only there, so files plus links need not equal it.
|
|
612
|
+
``file_count`` and ``link_count`` are null until the notice has been counted, so treat null as unknown, not zero.
|
|
603
613
|
"""
|
|
604
614
|
|
|
605
615
|
notice_id: str
|
|
@@ -613,6 +623,9 @@ class Notice:
|
|
|
613
623
|
award_amount: str | None = None
|
|
614
624
|
awardee: str | None = None
|
|
615
625
|
awardee_uei: str | None = None
|
|
626
|
+
attachment_count: int | None = None
|
|
627
|
+
file_count: int | None = None
|
|
628
|
+
link_count: int | None = None
|
|
616
629
|
|
|
617
630
|
|
|
618
631
|
@dataclass
|
|
@@ -1454,11 +1467,10 @@ class PaginatedResponse[T]:
|
|
|
1454
1467
|
results: List of result items (type depends on shape parameter)
|
|
1455
1468
|
cursor: Cursor token for cursor-based pagination (None if not available)
|
|
1456
1469
|
meta: Response-level metadata the API attached to this page, when present.
|
|
1457
|
-
Currently carries agency-filter diagnostics: ``resolved_filters`` maps
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
:meth:`agency_warnings` and :meth:`unresolved_agency_tokens`.
|
|
1470
|
+
Currently carries agency-filter diagnostics: ``resolved_filters`` maps each agency filter to the organizations its ``|``-separated tokens resolved to (or ``None``), and ``warnings`` lists human-readable notes about tokens that were dropped or matched loosely.
|
|
1471
|
+
Each ``resolved_filters`` entry that resolved also carries ``matched_by`` (Tango API 5.9.0+), saying how the token matched: ``"key"`` (an organization UUID), ``"code"`` (a 3-digit CGAC or 4-digit FPDS code), ``"name"`` (the organization's name, including a department's everyday name, a spelling variant or a rename), ``"alias"`` (an abbreviation or the organization's own alias) or ``"fuzzy"`` (a looser text match, worth checking against the resolved ``name``).
|
|
1472
|
+
An entry that did not resolve has no ``matched_by``.
|
|
1473
|
+
See :meth:`agency_warnings`, :meth:`unresolved_agency_tokens` and :meth:`resolved_agencies`.
|
|
1462
1474
|
page_metadata: Always ``None`` — the API has never emitted a
|
|
1463
1475
|
``page_metadata`` key. Retained so existing attribute access keeps
|
|
1464
1476
|
working; use ``meta`` instead.
|
|
@@ -641,6 +641,8 @@ META2_SCHEMA: dict[str, FieldSchema] = {
|
|
|
641
641
|
"attachments_count": FieldSchema(
|
|
642
642
|
name="attachments_count", type=int, is_optional=True, is_list=False
|
|
643
643
|
),
|
|
644
|
+
"files_count": FieldSchema(name="files_count", type=int, is_optional=True, is_list=False),
|
|
645
|
+
"links_count": FieldSchema(name="links_count", type=int, is_optional=True, is_list=False),
|
|
644
646
|
"notice_type": FieldSchema(
|
|
645
647
|
name="notice_type", type=dict, is_optional=True, is_list=False, nested_model="NoticeType"
|
|
646
648
|
),
|
|
@@ -2252,6 +2254,8 @@ GENERATED_OVERLAY: dict[str, dict[str, FieldSchema]] = {
|
|
|
2252
2254
|
"award_date": FieldSchema(name="award_date", type=date, is_optional=True, is_list=False),
|
|
2253
2255
|
"awardee": FieldSchema(name="awardee", type=str, is_optional=True, is_list=False),
|
|
2254
2256
|
"awardee_uei": FieldSchema(name="awardee_uei", type=str, is_optional=True, is_list=False),
|
|
2257
|
+
"file_count": FieldSchema(name="file_count", type=int, is_optional=True, is_list=False),
|
|
2258
|
+
"link_count": FieldSchema(name="link_count", type=int, is_optional=True, is_list=False),
|
|
2255
2259
|
"meta": FieldSchema(
|
|
2256
2260
|
name="meta", type=dict, is_optional=True, is_list=False, nested_model="Meta"
|
|
2257
2261
|
),
|
|
@@ -2292,6 +2292,22 @@ class TestAgencyFilterDiagnostics:
|
|
|
2292
2292
|
|
|
2293
2293
|
assert response.meta == meta
|
|
2294
2294
|
|
|
2295
|
+
@patch("tango.client.httpx.Client.request")
|
|
2296
|
+
def test_matched_by_reaches_the_caller_and_leaves_the_accessors_unchanged(self, mock_request):
|
|
2297
|
+
entries = [
|
|
2298
|
+
{"token": "HUD", "matched_by": "alias", "resolved": self.HUD},
|
|
2299
|
+
{"token": "Housing and Urban Developmnt", "matched_by": "fuzzy", "resolved": self.HUD},
|
|
2300
|
+
{"token": "HUDD", "resolved": None},
|
|
2301
|
+
]
|
|
2302
|
+
client = self._mock(mock_request, {"resolved_filters": {"awarding_agency": entries}})
|
|
2303
|
+
|
|
2304
|
+
response = client.list_contracts(awarding_agency="HUD|Housing and Urban Developmnt|HUDD")
|
|
2305
|
+
|
|
2306
|
+
returned = response.meta["resolved_filters"]["awarding_agency"]
|
|
2307
|
+
assert [entry.get("matched_by") for entry in returned] == ["alias", "fuzzy", None]
|
|
2308
|
+
assert response.unresolved_agency_tokens == {"awarding_agency": ["HUDD"]}
|
|
2309
|
+
assert response.resolved_agencies == {"awarding_agency": [self.HUD, self.HUD]}
|
|
2310
|
+
|
|
2295
2311
|
@patch("tango.client.httpx.Client.request")
|
|
2296
2312
|
def test_dropped_tokens_are_reported_per_filter(self, mock_request):
|
|
2297
2313
|
client = self._mock(
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""The `attachments(...)` expand on opportunities and notices, including the Pro-plan document-role fields, and the attachment, file and link counts."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from tango import TangoClient
|
|
8
|
+
from tango.models import Notice, Opportunity, Vehicle
|
|
9
|
+
from tango.shapes import SchemaRegistry
|
|
10
|
+
from tango.shapes.parser import ShapeParser
|
|
11
|
+
|
|
12
|
+
ROLE_SHAPE = "title,attachments(name,doc_role,doc_role_alt)"
|
|
13
|
+
OPPORTUNITY_COUNT_SHAPE = "opportunity_id,meta(attachments_count,files_count,links_count)"
|
|
14
|
+
NOTICE_COUNT_SHAPE = "notice_id,attachment_count,file_count,link_count"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _mock(mock_request, payload):
|
|
18
|
+
response = Mock()
|
|
19
|
+
response.is_success = True
|
|
20
|
+
response.json.return_value = payload
|
|
21
|
+
response.content = b"{}"
|
|
22
|
+
mock_request.return_value = response
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _attachments():
|
|
26
|
+
return [
|
|
27
|
+
{"name": "sow.pdf", "doc_role": "requirement", "doc_role_alt": None},
|
|
28
|
+
{"name": "pricing.xlsx", "doc_role": "pricing", "doc_role_alt": "terms"},
|
|
29
|
+
{"name": "unclassified.docx"},
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestAttachmentShapes:
|
|
34
|
+
@pytest.mark.parametrize("model", [Opportunity, Notice])
|
|
35
|
+
@pytest.mark.parametrize(
|
|
36
|
+
"shape",
|
|
37
|
+
[
|
|
38
|
+
ROLE_SHAPE,
|
|
39
|
+
"title,attachments(name,url,extracted_text)",
|
|
40
|
+
"title,attachments(*)",
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
def test_attachment_shape_validates(self, model, shape):
|
|
44
|
+
parser = ShapeParser(cache_enabled=False)
|
|
45
|
+
parser.validate(parser.parse(shape), model)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestAttachmentRoles:
|
|
49
|
+
@pytest.mark.parametrize("method", ["get_opportunity", "get_notice"])
|
|
50
|
+
@patch("tango.client.httpx.Client.request")
|
|
51
|
+
def test_detail_returns_document_roles(self, mock_request, method):
|
|
52
|
+
_mock(mock_request, {"title": "Base ops support", "attachments": _attachments()})
|
|
53
|
+
row = getattr(TangoClient(api_key="k"), method)("abc", shape=ROLE_SHAPE)
|
|
54
|
+
assert [a.get("doc_role") for a in row["attachments"]] == ["requirement", "pricing", None]
|
|
55
|
+
assert row["attachments"][1]["doc_role_alt"] == "terms"
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("method", ["list_opportunities", "list_notices"])
|
|
58
|
+
@patch("tango.client.httpx.Client.request")
|
|
59
|
+
def test_list_returns_document_roles(self, mock_request, method):
|
|
60
|
+
_mock(
|
|
61
|
+
mock_request,
|
|
62
|
+
{
|
|
63
|
+
"count": 1,
|
|
64
|
+
"next": None,
|
|
65
|
+
"previous": None,
|
|
66
|
+
"results": [{"title": "Amendment 1", "attachments": _attachments()}],
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
page = getattr(TangoClient(api_key="k"), method)(shape=ROLE_SHAPE)
|
|
70
|
+
assert mock_request.call_args.kwargs["params"]["shape"] == ROLE_SHAPE
|
|
71
|
+
assert [a.get("doc_role") for a in page.results[0]["attachments"]] == [
|
|
72
|
+
"requirement",
|
|
73
|
+
"pricing",
|
|
74
|
+
None,
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestAttachmentCountShapes:
|
|
79
|
+
@pytest.mark.parametrize(
|
|
80
|
+
("shape", "model"),
|
|
81
|
+
[
|
|
82
|
+
(OPPORTUNITY_COUNT_SHAPE, Opportunity),
|
|
83
|
+
(NOTICE_COUNT_SHAPE, Notice),
|
|
84
|
+
("uuid,opportunity(opportunity_id,meta(files_count,links_count))", Vehicle),
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
def test_count_shape_validates(self, shape, model):
|
|
88
|
+
parser = ShapeParser(cache_enabled=False)
|
|
89
|
+
parser.validate(parser.parse(shape), model)
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize(
|
|
92
|
+
("model", "path"),
|
|
93
|
+
[
|
|
94
|
+
(Opportunity, ("meta", "files_count")),
|
|
95
|
+
(Opportunity, ("meta", "links_count")),
|
|
96
|
+
(Notice, ("file_count",)),
|
|
97
|
+
(Notice, ("link_count",)),
|
|
98
|
+
],
|
|
99
|
+
)
|
|
100
|
+
def test_count_is_typed_as_nullable_int(self, model, path):
|
|
101
|
+
registry = SchemaRegistry()
|
|
102
|
+
schema = registry.get_schema(model)
|
|
103
|
+
for name in path[:-1]:
|
|
104
|
+
schema = registry.get_schema(schema[name].nested_model)
|
|
105
|
+
field = schema[path[-1]]
|
|
106
|
+
assert (field.type, field.is_optional, field.is_list) == (int, True, False)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class TestAttachmentCounts:
|
|
110
|
+
@patch("tango.client.httpx.Client.request")
|
|
111
|
+
def test_get_opportunity_returns_file_and_link_counts(self, mock_request):
|
|
112
|
+
_mock(
|
|
113
|
+
mock_request,
|
|
114
|
+
{
|
|
115
|
+
"opportunity_id": "abc",
|
|
116
|
+
"meta": {"attachments_count": 6, "files_count": 3, "links_count": 2},
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
row = TangoClient(api_key="k").get_opportunity("abc", shape=OPPORTUNITY_COUNT_SHAPE)
|
|
120
|
+
assert mock_request.call_args.kwargs["params"]["shape"] == OPPORTUNITY_COUNT_SHAPE
|
|
121
|
+
assert row["meta"] == {"attachments_count": 6, "files_count": 3, "links_count": 2}
|
|
122
|
+
|
|
123
|
+
@patch("tango.client.httpx.Client.request")
|
|
124
|
+
def test_get_opportunity_keeps_uncounted_as_none(self, mock_request):
|
|
125
|
+
_mock(
|
|
126
|
+
mock_request,
|
|
127
|
+
{
|
|
128
|
+
"opportunity_id": "abc",
|
|
129
|
+
"meta": {"attachments_count": 4, "files_count": None, "links_count": None},
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
row = TangoClient(api_key="k").get_opportunity("abc", shape=OPPORTUNITY_COUNT_SHAPE)
|
|
133
|
+
assert row["meta"]["attachments_count"] == 4
|
|
134
|
+
assert row["meta"]["files_count"] is None
|
|
135
|
+
assert row["meta"]["links_count"] is None
|
|
136
|
+
|
|
137
|
+
@patch("tango.client.httpx.Client.request")
|
|
138
|
+
def test_list_notices_returns_file_and_link_counts(self, mock_request):
|
|
139
|
+
_mock(
|
|
140
|
+
mock_request,
|
|
141
|
+
{
|
|
142
|
+
"count": 2,
|
|
143
|
+
"next": None,
|
|
144
|
+
"previous": None,
|
|
145
|
+
"results": [
|
|
146
|
+
{"notice_id": "n1", "attachment_count": 6, "file_count": 3, "link_count": 2},
|
|
147
|
+
{
|
|
148
|
+
"notice_id": "n2",
|
|
149
|
+
"attachment_count": 1,
|
|
150
|
+
"file_count": None,
|
|
151
|
+
"link_count": None,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
page = TangoClient(api_key="k").list_notices(shape=NOTICE_COUNT_SHAPE)
|
|
157
|
+
assert mock_request.call_args.kwargs["params"]["shape"] == NOTICE_COUNT_SHAPE
|
|
158
|
+
counted, uncounted = page.results
|
|
159
|
+
assert (counted["attachment_count"], counted["file_count"], counted["link_count"]) == (
|
|
160
|
+
6,
|
|
161
|
+
3,
|
|
162
|
+
2,
|
|
163
|
+
)
|
|
164
|
+
assert uncounted["attachment_count"] == 1
|
|
165
|
+
assert uncounted["file_count"] is None
|
|
166
|
+
assert uncounted["link_count"] is None
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
"""The `attachments(...)` expand on opportunities and notices, including the Pro-plan document-role fields."""
|
|
2
|
-
|
|
3
|
-
from unittest.mock import Mock, patch
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from tango import TangoClient
|
|
8
|
-
from tango.models import Notice, Opportunity
|
|
9
|
-
from tango.shapes.parser import ShapeParser
|
|
10
|
-
|
|
11
|
-
ROLE_SHAPE = "title,attachments(name,doc_role,doc_role_alt)"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _mock(mock_request, payload):
|
|
15
|
-
response = Mock()
|
|
16
|
-
response.is_success = True
|
|
17
|
-
response.json.return_value = payload
|
|
18
|
-
response.content = b"{}"
|
|
19
|
-
mock_request.return_value = response
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def _attachments():
|
|
23
|
-
return [
|
|
24
|
-
{"name": "sow.pdf", "doc_role": "requirement", "doc_role_alt": None},
|
|
25
|
-
{"name": "pricing.xlsx", "doc_role": "pricing", "doc_role_alt": "terms"},
|
|
26
|
-
{"name": "unclassified.docx"},
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class TestAttachmentShapes:
|
|
31
|
-
@pytest.mark.parametrize("model", [Opportunity, Notice])
|
|
32
|
-
@pytest.mark.parametrize(
|
|
33
|
-
"shape",
|
|
34
|
-
[
|
|
35
|
-
ROLE_SHAPE,
|
|
36
|
-
"title,attachments(name,url,extracted_text)",
|
|
37
|
-
"title,attachments(*)",
|
|
38
|
-
],
|
|
39
|
-
)
|
|
40
|
-
def test_attachment_shape_validates(self, model, shape):
|
|
41
|
-
parser = ShapeParser(cache_enabled=False)
|
|
42
|
-
parser.validate(parser.parse(shape), model)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class TestAttachmentRoles:
|
|
46
|
-
@pytest.mark.parametrize("method", ["get_opportunity", "get_notice"])
|
|
47
|
-
@patch("tango.client.httpx.Client.request")
|
|
48
|
-
def test_detail_returns_document_roles(self, mock_request, method):
|
|
49
|
-
_mock(mock_request, {"title": "Base ops support", "attachments": _attachments()})
|
|
50
|
-
row = getattr(TangoClient(api_key="k"), method)("abc", shape=ROLE_SHAPE)
|
|
51
|
-
assert [a.get("doc_role") for a in row["attachments"]] == ["requirement", "pricing", None]
|
|
52
|
-
assert row["attachments"][1]["doc_role_alt"] == "terms"
|
|
53
|
-
|
|
54
|
-
@pytest.mark.parametrize("method", ["list_opportunities", "list_notices"])
|
|
55
|
-
@patch("tango.client.httpx.Client.request")
|
|
56
|
-
def test_list_returns_document_roles(self, mock_request, method):
|
|
57
|
-
_mock(
|
|
58
|
-
mock_request,
|
|
59
|
-
{
|
|
60
|
-
"count": 1,
|
|
61
|
-
"next": None,
|
|
62
|
-
"previous": None,
|
|
63
|
-
"results": [{"title": "Amendment 1", "attachments": _attachments()}],
|
|
64
|
-
},
|
|
65
|
-
)
|
|
66
|
-
page = getattr(TangoClient(api_key="k"), method)(shape=ROLE_SHAPE)
|
|
67
|
-
assert mock_request.call_args.kwargs["params"]["shape"] == ROLE_SHAPE
|
|
68
|
-
assert [a.get("doc_role") for a in page.results[0]["attachments"]] == [
|
|
69
|
-
"requirement",
|
|
70
|
-
"pricing",
|
|
71
|
-
None,
|
|
72
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tango_python-1.14.0 → tango_python-1.15.0}/tests/cassettes/TestAgenciesIntegration.test_get_agency
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|