tango-python 1.1.0__tar.gz → 1.1.2__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.1.0 → tango_python-1.1.2}/.github/workflows/docs-dispatch.yml +1 -1
- {tango_python-1.1.0 → tango_python-1.1.2}/.github/workflows/lint.yml +8 -10
- {tango_python-1.1.0 → tango_python-1.1.2}/.github/workflows/publish.yml +2 -2
- {tango_python-1.1.0 → tango_python-1.1.2}/.github/workflows/test.yml +4 -4
- {tango_python-1.1.0 → tango_python-1.1.2}/CHANGELOG.md +57 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/PKG-INFO +1 -4
- {tango_python-1.1.0 → tango_python-1.1.2}/docs/API_REFERENCE.md +134 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/pyproject.toml +1 -5
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/__init__.py +1 -1
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/client.py +28 -3
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/shapes/factory.py +12 -76
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/shapes/generator.py +12 -27
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/shapes/parser.py +17 -16
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/shapes/schema.py +3 -2
- tango_python-1.1.2/tests/cassettes/TestEntitiesIntegration.test_get_entity_budget_flows +229 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_entities_integration.py +58 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/test_client.py +59 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/.gitignore +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/LICENSE +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/README.md +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/docs/CLIENT.md +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/docs/DYNAMIC_MODELS.md +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/docs/ERRORS.md +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/docs/PAGINATION.md +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/docs/SHAPES.md +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/docs/WEBHOOKS.md +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/scripts/README.md +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/scripts/check_filter_shape_conformance.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/scripts/fetch_api_schema.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/scripts/generate_schemas_from_api.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/scripts/pr_review.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/scripts/smoke_api_parity.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/scripts/test_production.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/exceptions.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/models.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/shapes/__init__.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/shapes/explicit_schemas.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/shapes/models.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/shapes/types.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/webhooks/__init__.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/webhooks/cli.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/webhooks/receiver.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/webhooks/signing.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tango/webhooks/simulate.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/__init__.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestAgenciesIntegration.test_get_agency +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestAgenciesIntegration.test_list_agencies +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_field_type_validation +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestBusinessTypesIntegration.test_business_type_parsing_consistency +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestBusinessTypesIntegration.test_list_business_types +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_combined_filters_work_together +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_contract_cursor_pagination +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_contract_data_object_parsing +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_contract_field_types +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[keyword-software] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_filter_parameter_mappings[psc_code-R425] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_awarding_agency_filter +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_date_range_filter +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_flat +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_naics_code_filter +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[custom-key,piid,recipient(display_name),total_contract_value,award_date] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_list_contracts_with_shapes[default-None] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_new_expiring_filters +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_new_fiscal_year_range_filters +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_new_identifier_filters +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_search_contracts_with_filters +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_search_filters_object_with_new_parameters +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[asc-] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestContractsIntegration.test_sort_and_order_mapped_to_ordering[desc--] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_contracts +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_api_schema_stability_detection_entities +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_date_field_parsing_edge_cases +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_decimal_field_parsing_edge_cases +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_empty_list_responses +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_entity_parsing_with_various_address_formats +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_flattened_responses_with_flat_lists +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_list_field_parsing_consistency +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_nested_objects_with_missing_data +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_null_missing_fields_in_contracts +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEdgeCasesIntegration.test_parsing_with_minimal_shape_sparse_data +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEntitiesIntegration.test_entity_field_types +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEntitiesIntegration.test_entity_location_parsing +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEntitiesIntegration.test_entity_parsing_with_business_types +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEntitiesIntegration.test_entity_with_various_identifiers +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEntitiesIntegration.test_get_entity_by_uei +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_flat +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_search +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[custom-uei,legal_business_name,cage_code] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestEntitiesIntegration.test_list_entities_with_shapes[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestForecastsIntegration.test_forecast_field_types +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[custom-id,title,anticipated_award_date] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[default-None] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestForecastsIntegration.test_list_forecasts_with_shapes[minimal-id,title,anticipated_award_date,fiscal_year,naics_code,status] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestGrantsIntegration.test_grant_field_types +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestGrantsIntegration.test_grant_pagination +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[custom-grant_id,title,opportunity_number] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[default-None] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestGrantsIntegration.test_list_grants_with_shapes[minimal-grant_id,opportunity_number,title,status(-),agency_code] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestIDVsIntegration.test_get_idv_uses_default_shape +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestIDVsIntegration.test_list_idv_awards_uses_default_shape +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestIDVsIntegration.test_list_idv_child_idvs_uses_default_shape +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestIDVsIntegration.test_list_idv_transactions +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestIDVsIntegration.test_list_idvs_uses_default_shape_and_keyset_params +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_code +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_filter_by_agency_name_text +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_filter_by_cio_rating_max +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_filter_by_performance_risk +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_filter_by_type_of_investment +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_filter_by_updated_time_range +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_funding_and_cio_evaluation_expansions +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_get_itdashboard_investment_by_uii +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_itdashboard_pagination +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_search +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[custom-uii,agency_name,investment_title,updated_time] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestITDashboardIntegration.test_list_itdashboard_investments_with_shapes[default-None] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestNaicsIntegration.test_list_naics +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[custom-notice_id,title,solicitation_number] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[default-None] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestNoticesIntegration.test_list_notices_with_shapes[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestNoticesIntegration.test_notice_field_types +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestNoticesIntegration.test_notice_pagination +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestNoticesIntegration.test_notice_with_meta_fields +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOTAsIntegration.test_get_ota +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOTAsIntegration.test_list_otas +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOTIDVsIntegration.test_get_otidv +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOTIDVsIntegration.test_list_otidvs +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOfficesIntegration.test_get_office +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOfficesIntegration.test_list_offices +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[custom-opportunity_id,title,solicitation_number] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[default-None] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestOpportunitiesIntegration.test_list_opportunities_with_shapes[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOpportunitiesIntegration.test_opportunity_field_types +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOrganizationsIntegration.test_get_organization +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestOrganizationsIntegration.test_list_organizations +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestProtestsIntegration.test_get_protest_by_case_id +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_filter +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[custom-case_id,title,source_system,outcome] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[default-None] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestProtestsIntegration.test_list_protests_with_shapes[minimal-case_id,case_number,title,source_system,outcome,filed_date] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestProtestsIntegration.test_protest_pagination +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestSubawardsIntegration.test_list_subawards +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[custom-key,piid,description] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[minimal-key,piid,award_date,recipient(display_name),description,total_contract_value] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestTypeHintsIntegration.test_contracts_dict_access[ultra_minimal-key,piid,recipient(display_name),total_contract_value] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[minimal-uei,legal_business_name,cage_code,business_types] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestTypeHintsIntegration.test_entities_dict_access[with_address-uei,legal_business_name,cage_code,business_types,physical_address] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestTypeHintsIntegration.test_notices_dict_access[minimal-notice_id,title,solicitation_number,posted_date] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/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.1.0 → tango_python-1.1.2}/tests/cassettes/TestTypeHintsIntegration.test_opportunities_dict_access[minimal-opportunity_id,title,solicitation_number,response_deadline,active] +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_supports_joiner_and_flat_lists +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestVehiclesIntegration.test_get_vehicle_with_metrics_expansion +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_awardees_uses_default_shape +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestVehiclesIntegration.test_list_vehicle_orders_uses_default_shape +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_uses_default_shape_and_search +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/cassettes/TestVehiclesIntegration.test_list_vehicles_with_ordering +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/conftest.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/README.md +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/__init__.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/conftest.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_agencies_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_contracts_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_edge_cases_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_forecasts_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_grants_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_itdashboard_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_naics_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_notices_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_offices_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_opportunities_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_organizations_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_otas_otidvs_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_protests_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_reference_data_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_subawards_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/test_vehicles_idvs_integration.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/integration/validation.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/production/__init__.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/production/conftest.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/production/test_production_smoke.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/test_api_parity.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/test_models.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/test_shapes.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/test_webhooks_cli.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/test_webhooks_receiver.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/test_webhooks_signing.py +0 -0
- {tango_python-1.1.0 → tango_python-1.1.2}/tests/test_webhooks_simulate.py +0 -0
|
@@ -3,9 +3,8 @@ name: Linting
|
|
|
3
3
|
# Lint gate runs on every PR and push to main.
|
|
4
4
|
#
|
|
5
5
|
# - ruff format + ruff check are HARD gates (block the PR).
|
|
6
|
-
# - mypy is
|
|
7
|
-
#
|
|
8
|
-
# in makegov/tango-python; flip `continue-on-error` off once that's clear.
|
|
6
|
+
# - mypy is a HARD gate: the package type-checks cleanly under strict mypy.
|
|
7
|
+
# (The earlier ~28-error burn-down is complete.)
|
|
9
8
|
# - The SDK filter/shape conformance check needs the canonical manifest from the
|
|
10
9
|
# private makegov/tango repo, which requires a TANGO_API_REPO_ACCESS_TOKEN
|
|
11
10
|
# secret the public CI does not have. The conformance job SKIPS cleanly when
|
|
@@ -23,10 +22,10 @@ jobs:
|
|
|
23
22
|
runs-on: ubuntu-latest
|
|
24
23
|
|
|
25
24
|
steps:
|
|
26
|
-
- uses: actions/checkout@
|
|
25
|
+
- uses: actions/checkout@v6
|
|
27
26
|
|
|
28
27
|
- name: Install uv
|
|
29
|
-
uses: astral-sh/setup-uv@
|
|
28
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
30
29
|
with:
|
|
31
30
|
version: "latest"
|
|
32
31
|
|
|
@@ -42,8 +41,7 @@ jobs:
|
|
|
42
41
|
- name: Lint with ruff
|
|
43
42
|
run: uv run ruff check tango/
|
|
44
43
|
|
|
45
|
-
- name: Type check with mypy
|
|
46
|
-
continue-on-error: true
|
|
44
|
+
- name: Type check with mypy
|
|
47
45
|
run: uv run mypy tango/
|
|
48
46
|
|
|
49
47
|
conformance:
|
|
@@ -66,12 +64,12 @@ jobs:
|
|
|
66
64
|
echo "::notice::Skipping SDK conformance check — TANGO_API_REPO_ACCESS_TOKEN not configured."
|
|
67
65
|
fi
|
|
68
66
|
|
|
69
|
-
- uses: actions/checkout@
|
|
67
|
+
- uses: actions/checkout@v6
|
|
70
68
|
if: steps.gate.outputs.ready == 'true'
|
|
71
69
|
|
|
72
70
|
- name: Checkout tango API repo (manifest source)
|
|
73
71
|
if: steps.gate.outputs.ready == 'true'
|
|
74
|
-
uses: actions/checkout@
|
|
72
|
+
uses: actions/checkout@v6
|
|
75
73
|
with:
|
|
76
74
|
repository: makegov/tango
|
|
77
75
|
path: tango-api
|
|
@@ -79,7 +77,7 @@ jobs:
|
|
|
79
77
|
|
|
80
78
|
- name: Install uv
|
|
81
79
|
if: steps.gate.outputs.ready == 'true'
|
|
82
|
-
uses: astral-sh/setup-uv@
|
|
80
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
83
81
|
with:
|
|
84
82
|
version: "latest"
|
|
85
83
|
|
|
@@ -20,10 +20,10 @@ jobs:
|
|
|
20
20
|
if: matrix.os == 'windows-latest'
|
|
21
21
|
run: git config --global core.longpaths true
|
|
22
22
|
|
|
23
|
-
- uses: actions/checkout@
|
|
23
|
+
- uses: actions/checkout@v6
|
|
24
24
|
|
|
25
25
|
- name: Install uv
|
|
26
|
-
uses: astral-sh/setup-uv@
|
|
26
|
+
uses: astral-sh/setup-uv@v8.1.0
|
|
27
27
|
with:
|
|
28
28
|
version: "latest"
|
|
29
29
|
|
|
@@ -37,8 +37,8 @@ jobs:
|
|
|
37
37
|
run: uv run pytest
|
|
38
38
|
|
|
39
39
|
- name: Upload coverage to Codecov
|
|
40
|
-
uses: codecov/codecov-action@
|
|
40
|
+
uses: codecov/codecov-action@v5
|
|
41
41
|
with:
|
|
42
|
-
|
|
42
|
+
files: ./coverage.xml
|
|
43
43
|
flags: unittests
|
|
44
44
|
name: codecov-${{ matrix.os }}-py${{ matrix.python-version }}
|
|
@@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.2] - 2026-06-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `get_entity_budget_flows()` now exposes the backend's standard page/limit
|
|
14
|
+
pagination and `fiscal_year` filter, and returns
|
|
15
|
+
`PaginatedResponse[dict[str, Any]]` instead of a raw dict. The backend has
|
|
16
|
+
always paginated this endpoint via `StandardResultsSetPagination`; the
|
|
17
|
+
previous signature gave callers no way to reach pages beyond the first or
|
|
18
|
+
to narrow by fiscal year. Callers that were indexing `result["results"]`
|
|
19
|
+
on the old return value should switch to `result.results` (and can now use
|
|
20
|
+
`result.next` / `page=` to walk further pages).
|
|
21
|
+
- Completed the strict-`mypy` burn-down across `tango/shapes/` (parser,
|
|
22
|
+
generator, factory, schema). All changes are type-annotation/typing
|
|
23
|
+
corrections with no runtime behavior change, except:
|
|
24
|
+
- `FieldSchema.nested_model` is now typed `type | str | None` (it always
|
|
25
|
+
accepted string model names from the explicit schemas; the annotation was
|
|
26
|
+
wrong). `ModelFactory.validate_data` and `ShapeParser._validate_field_spec`
|
|
27
|
+
likewise accept `type | str` for the model argument.
|
|
28
|
+
- Removed two dead `elif field_spec.is_wildcard:` branches (in
|
|
29
|
+
`TypeGenerator.generate_type` and `ModelFactory.create_instance`) and the
|
|
30
|
+
now-orphaned `_parse_nested_wildcard` helper. These were unreachable —
|
|
31
|
+
wildcard field specs are fully handled by the top-of-loop branch that
|
|
32
|
+
`continue`s before reaching them — so removal is behavior-preserving.
|
|
33
|
+
|
|
34
|
+
### Docs
|
|
35
|
+
- `docs/API_REFERENCE.md` now documents the Budget surface that shipped in
|
|
36
|
+
v1.1.0: a new `## Budget` section covering `list_budget_accounts`,
|
|
37
|
+
`get_budget_account`, `get_budget_account_quarters`, and
|
|
38
|
+
`get_budget_account_recipients`; a `get_entity_budget_flows()` entry under
|
|
39
|
+
Entity Sub-resources; a `BUDGET_ACCOUNTS_MINIMAL` row in the ShapeConfig
|
|
40
|
+
table; and a Budget entry in the table of contents.
|
|
41
|
+
|
|
42
|
+
### CI
|
|
43
|
+
- Bumped GitHub Actions off the deprecated Node 20 runtime (forced off
|
|
44
|
+
2026-06-02): `actions/checkout` v4→v6, `astral-sh/setup-uv` v4→v8.1.0
|
|
45
|
+
(pinned exact — no floating `v8` major tag is published yet), and
|
|
46
|
+
`codecov/codecov-action` v3→v5 (with the renamed `files:` input).
|
|
47
|
+
- `mypy` is now a **hard gate** in `lint.yml` (no longer advisory). The
|
|
48
|
+
`tango/` package type-checks cleanly under strict mypy.
|
|
49
|
+
|
|
50
|
+
## [1.1.1] - 2026-05-29
|
|
51
|
+
|
|
52
|
+
### Removed
|
|
53
|
+
- The `notebooks` optional extra (`jupyter`, `ipykernel`). It only powered local
|
|
54
|
+
editing of `docs/quick_start.ipynb`, which still renders on GitHub/PyPI without
|
|
55
|
+
it, and its transitive tree (jupyter-server, jupyterlab, nbconvert, tornado,
|
|
56
|
+
etc.) accounted for the bulk of the repo's open Dependabot alerts. Contributors
|
|
57
|
+
who want to re-run the notebook can `pip install jupyter` directly. The shipped
|
|
58
|
+
SDK is unaffected — its only runtime dependency remains `httpx`.
|
|
59
|
+
|
|
60
|
+
### Changed
|
|
61
|
+
- Refreshed the dev/test dependency lock to clear the remaining Dependabot
|
|
62
|
+
alerts (patched `idna`, `pygments`, `pytest`, `python-dotenv`; dropped
|
|
63
|
+
`urllib3`/`requests` transitives). Dev-tool majors moved forward
|
|
64
|
+
(`mypy` 1.18→2.1, `pytest` 8→9, `ruff` 0.14→0.15, `vcrpy` 7→8); all lint,
|
|
65
|
+
type, and test gates stay green. No change to the shipped SDK's runtime deps.
|
|
66
|
+
|
|
10
67
|
## [1.1.0] - 2026-05-29
|
|
11
68
|
|
|
12
69
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tango-python
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.2
|
|
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
|
|
@@ -48,9 +48,6 @@ Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
|
48
48
|
Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
|
|
49
49
|
Requires-Dist: pyyaml>=6.0; extra == 'dev'
|
|
50
50
|
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
51
|
-
Provides-Extra: notebooks
|
|
52
|
-
Requires-Dist: ipykernel>=6.25.0; extra == 'notebooks'
|
|
53
|
-
Requires-Dist: jupyter>=1.0.0; extra == 'notebooks'
|
|
54
51
|
Provides-Extra: webhooks
|
|
55
52
|
Requires-Dist: click>=8.1; extra == 'webhooks'
|
|
56
53
|
Description-Content-Type: text/markdown
|
|
@@ -21,6 +21,7 @@ Complete reference for all Tango Python SDK methods and functionality.
|
|
|
21
21
|
- [Grants](#grants)
|
|
22
22
|
- [GSA eLibrary Contracts](#gsa-elibrary-contracts)
|
|
23
23
|
- [Protests](#protests)
|
|
24
|
+
- [Budget](#budget)
|
|
24
25
|
- [Business Types](#business-types)
|
|
25
26
|
- [NAICS](#naics)
|
|
26
27
|
- [Webhooks](#webhooks)
|
|
@@ -1212,6 +1213,118 @@ protest = client.get_protest(
|
|
|
1212
1213
|
|
|
1213
1214
|
---
|
|
1214
1215
|
|
|
1216
|
+
## Budget
|
|
1217
|
+
|
|
1218
|
+
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.
|
|
1219
|
+
|
|
1220
|
+
### list_budget_accounts()
|
|
1221
|
+
|
|
1222
|
+
List budget accounts. One row per `(federal_account_symbol, fiscal_year)`.
|
|
1223
|
+
|
|
1224
|
+
```python
|
|
1225
|
+
accounts = client.list_budget_accounts(
|
|
1226
|
+
page=1,
|
|
1227
|
+
limit=25,
|
|
1228
|
+
shape=ShapeConfig.BUDGET_ACCOUNTS_MINIMAL,
|
|
1229
|
+
# Filter parameters (all optional)
|
|
1230
|
+
federal_account_symbol=None,
|
|
1231
|
+
fiscal_year=None,
|
|
1232
|
+
fiscal_year_gte=None,
|
|
1233
|
+
fiscal_year_lte=None,
|
|
1234
|
+
agency_code=None,
|
|
1235
|
+
bureau_name=None,
|
|
1236
|
+
account_title=None,
|
|
1237
|
+
bea_category=None,
|
|
1238
|
+
on_off_budget=None,
|
|
1239
|
+
subfunction_code=None,
|
|
1240
|
+
search=None,
|
|
1241
|
+
ordering=None,
|
|
1242
|
+
)
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
**Filter Parameters:**
|
|
1246
|
+
- `federal_account_symbol` - Exact federal account symbol (e.g., `"097-0100"`)
|
|
1247
|
+
- `fiscal_year` - Fiscal year (exact)
|
|
1248
|
+
- `fiscal_year_gte` / `fiscal_year_lte` - Fiscal year range
|
|
1249
|
+
- `agency_code` - Agency code (exact)
|
|
1250
|
+
- `bureau_name` - Bureau name (exact)
|
|
1251
|
+
- `account_title` - Account title (case-insensitive substring match)
|
|
1252
|
+
- `bea_category` - BEA category (exact)
|
|
1253
|
+
- `on_off_budget` - On/off budget flag (exact)
|
|
1254
|
+
- `subfunction_code` - Subfunction code (exact)
|
|
1255
|
+
- `search` - Full-text search over `account_title`, `agency_name`, `bureau_name`
|
|
1256
|
+
- `ordering` - Sort field; prefix with `-` for descending
|
|
1257
|
+
|
|
1258
|
+
**Returns:** [PaginatedResponse](#paginatedresponse) of `BudgetAccount` records (see [ShapeConfig](#shapeconfig-predefined-shapes) for the default shape).
|
|
1259
|
+
|
|
1260
|
+
**Example:**
|
|
1261
|
+
```python
|
|
1262
|
+
accounts = client.list_budget_accounts(
|
|
1263
|
+
agency_code="097",
|
|
1264
|
+
fiscal_year_gte=2023,
|
|
1265
|
+
ordering="-enacted_ba",
|
|
1266
|
+
limit=10,
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
for acct in accounts.results:
|
|
1270
|
+
print(f"{acct.federal_account_symbol} FY{acct.fiscal_year}: "
|
|
1271
|
+
f"enacted ${acct.enacted_ba:,}")
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
### get_budget_account()
|
|
1275
|
+
|
|
1276
|
+
Get a single budget account by id.
|
|
1277
|
+
|
|
1278
|
+
```python
|
|
1279
|
+
account = client.get_budget_account(
|
|
1280
|
+
12345,
|
|
1281
|
+
shape=ShapeConfig.BUDGET_ACCOUNTS_MINIMAL,
|
|
1282
|
+
)
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
**Parameters:**
|
|
1286
|
+
- `id` (str | int): Budget account id.
|
|
1287
|
+
- `shape` (str, optional): Response shape. Defaults to `BUDGET_ACCOUNTS_MINIMAL`.
|
|
1288
|
+
- `flat` / `flat_lists` / `joiner`: See [Shaping Guide](SHAPES.md).
|
|
1289
|
+
|
|
1290
|
+
**Returns:** A `BudgetAccount` record.
|
|
1291
|
+
|
|
1292
|
+
### get_budget_account_quarters()
|
|
1293
|
+
|
|
1294
|
+
Get quarterly TAS-grain flow for a budget account. FY21+ only.
|
|
1295
|
+
|
|
1296
|
+
```python
|
|
1297
|
+
quarters = client.get_budget_account_quarters(12345, limit=25)
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
**Parameters:**
|
|
1301
|
+
- `id` (str | int): Budget account id.
|
|
1302
|
+
- `tas` (str, optional): Narrow to a single Treasury Account Symbol.
|
|
1303
|
+
- `limit` (int): Results per page (max 100).
|
|
1304
|
+
|
|
1305
|
+
**Returns:** [PaginatedResponse](#paginatedresponse) of quarterly flow records.
|
|
1306
|
+
|
|
1307
|
+
### get_budget_account_recipients()
|
|
1308
|
+
|
|
1309
|
+
Get funding-office × recipient contract-flow detail for a budget account.
|
|
1310
|
+
|
|
1311
|
+
```python
|
|
1312
|
+
recipients = client.get_budget_account_recipients(
|
|
1313
|
+
12345,
|
|
1314
|
+
funding_organization_id=None,
|
|
1315
|
+
limit=25,
|
|
1316
|
+
)
|
|
1317
|
+
```
|
|
1318
|
+
|
|
1319
|
+
**Parameters:**
|
|
1320
|
+
- `id` (str | int): Budget account id.
|
|
1321
|
+
- `funding_organization_id` (str, optional): Narrow to a single funding office (Organization UUID).
|
|
1322
|
+
- `limit` (int): Results per page (max 100).
|
|
1323
|
+
|
|
1324
|
+
**Returns:** [PaginatedResponse](#paginatedresponse) of `(funding_office, recipient)` flow records.
|
|
1325
|
+
|
|
1326
|
+
---
|
|
1327
|
+
|
|
1215
1328
|
## Business Types
|
|
1216
1329
|
|
|
1217
1330
|
Business type classifications.
|
|
@@ -1461,6 +1574,26 @@ lcats = client.list_entity_lcats("ABCDEF123456", limit=25)
|
|
|
1461
1574
|
metrics = client.get_entity_metrics("ABCDEF123456", months=12, period_grouping="month")
|
|
1462
1575
|
```
|
|
1463
1576
|
|
|
1577
|
+
### get_entity_budget_flows()
|
|
1578
|
+
|
|
1579
|
+
Get budget flows for an entity (`/api/entities/{uei}/budget-flows/`) — the federal accounts that funded contracts and assistance awarded to this entity.
|
|
1580
|
+
|
|
1581
|
+
```python
|
|
1582
|
+
flows = client.get_entity_budget_flows("ABCDEF123456", fiscal_year=2024)
|
|
1583
|
+
for row in flows.results:
|
|
1584
|
+
print(row["federal_account_symbol"], row["contract_obligated"])
|
|
1585
|
+
# next page
|
|
1586
|
+
more = client.get_entity_budget_flows("ABCDEF123456", page=2, fiscal_year=2024)
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
**Parameters:**
|
|
1590
|
+
- `uei` (str): Entity UEI. Required.
|
|
1591
|
+
- `page` (int): Page number. Default 1.
|
|
1592
|
+
- `limit` (int): Results per page. Default 25, max 100.
|
|
1593
|
+
- `fiscal_year` (int | None): Optional fiscal year filter.
|
|
1594
|
+
|
|
1595
|
+
**Returns:** `PaginatedResponse[dict[str, Any]]` — standard `count / next / previous / results`. Result rows are raw dicts from the API (not shape-controlled).
|
|
1596
|
+
|
|
1464
1597
|
---
|
|
1465
1598
|
|
|
1466
1599
|
## IDV LCATs
|
|
@@ -1875,6 +2008,7 @@ entity = client.get_entity("UEI_KEY", shape=ShapeConfig.ENTITIES_COMPREHENSIVE)
|
|
|
1875
2008
|
| `SUBAWARDS_MINIMAL` | `list_subawards` | award_key, prime_recipient(uei,display_name), subaward_recipient(uei,display_name) |
|
|
1876
2009
|
| `GSA_ELIBRARY_CONTRACTS_MINIMAL` | `list_gsa_elibrary_contracts` | uuid, contract_number, schedule, recipient(display_name,uei), idv(key,award_date) |
|
|
1877
2010
|
| `PROTESTS_MINIMAL` | `list_protests` | case_id, case_number, title, source_system, outcome, filed_date |
|
|
2011
|
+
| `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 |
|
|
1878
2012
|
| `VEHICLE_ORDERS_MINIMAL` | `list_vehicle_orders` | key, piid, award_date, recipient(display_name,uei), total_contract_value, obligated |
|
|
1879
2013
|
| `ITDASHBOARD_INVESTMENTS_MINIMAL` | `list_itdashboard_investments` | Minimal IT Dashboard investment fields |
|
|
1880
2014
|
| `ITDASHBOARD_INVESTMENTS_COMPREHENSIVE` | `get_itdashboard_investment` | Full investment fields: uii, agency_code, agency_name, bureau_code, bureau_name, investment_title, type_of_investment, part_of_it_portfolio, updated_time, url |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tango-python"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.2"
|
|
8
8
|
description = "Python SDK for the Tango API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -37,10 +37,6 @@ dev = [
|
|
|
37
37
|
"python-dotenv>=1.0.0",
|
|
38
38
|
"pyyaml>=6.0",
|
|
39
39
|
]
|
|
40
|
-
notebooks = [
|
|
41
|
-
"jupyter>=1.0.0",
|
|
42
|
-
"ipykernel>=6.25.0",
|
|
43
|
-
]
|
|
44
40
|
webhooks = [
|
|
45
41
|
"click>=8.1",
|
|
46
42
|
]
|
|
@@ -2081,11 +2081,36 @@ class TangoClient:
|
|
|
2081
2081
|
data = self._get(f"/api/entities/{key}/", params)
|
|
2082
2082
|
return self._parse_response_with_shape(data, shape, Entity, flat, flat_lists)
|
|
2083
2083
|
|
|
2084
|
-
def get_entity_budget_flows(
|
|
2085
|
-
|
|
2084
|
+
def get_entity_budget_flows(
|
|
2085
|
+
self,
|
|
2086
|
+
uei: str,
|
|
2087
|
+
page: int = 1,
|
|
2088
|
+
limit: int = 25,
|
|
2089
|
+
fiscal_year: int | None = None,
|
|
2090
|
+
) -> PaginatedResponse[dict[str, Any]]:
|
|
2091
|
+
"""Get budget flows for an entity (`/api/entities/{uei}/budget-flows/`).
|
|
2092
|
+
|
|
2093
|
+
Standard page/limit pagination (default 25, max 100). Each result row
|
|
2094
|
+
is a hand-built dict from the backend (no shape system).
|
|
2095
|
+
|
|
2096
|
+
Args:
|
|
2097
|
+
uei: Entity UEI. Required.
|
|
2098
|
+
page: Page number.
|
|
2099
|
+
limit: Results per page (max 100).
|
|
2100
|
+
fiscal_year: Optional fiscal year filter.
|
|
2101
|
+
"""
|
|
2086
2102
|
if not uei:
|
|
2087
2103
|
raise TangoValidationError("UEI is required")
|
|
2088
|
-
|
|
2104
|
+
params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
|
|
2105
|
+
if fiscal_year is not None:
|
|
2106
|
+
params["fiscal_year"] = fiscal_year
|
|
2107
|
+
data = self._get(f"/api/entities/{uei}/budget-flows/", params)
|
|
2108
|
+
return PaginatedResponse(
|
|
2109
|
+
count=int(data.get("count", 0)),
|
|
2110
|
+
next=data.get("next"),
|
|
2111
|
+
previous=data.get("previous"),
|
|
2112
|
+
results=list(data.get("results") or []),
|
|
2113
|
+
)
|
|
2089
2114
|
|
|
2090
2115
|
# Forecast endpoints
|
|
2091
2116
|
def list_forecasts(
|
|
@@ -23,7 +23,7 @@ import logging
|
|
|
23
23
|
from collections.abc import Callable
|
|
24
24
|
from datetime import date, datetime
|
|
25
25
|
from decimal import Decimal
|
|
26
|
-
from typing import Any
|
|
26
|
+
from typing import Any, cast
|
|
27
27
|
|
|
28
28
|
from tango.exceptions import ModelInstantiationError
|
|
29
29
|
from tango.shapes.generator import TypeGenerator
|
|
@@ -542,38 +542,6 @@ class ModelFactory:
|
|
|
542
542
|
# Value is not a dict - might be a primitive or None
|
|
543
543
|
result[result_field_name] = value
|
|
544
544
|
|
|
545
|
-
elif field_spec.is_wildcard:
|
|
546
|
-
# Wildcard on nested field - use full model type
|
|
547
|
-
# This is handled at the top level, but we need to handle it here too
|
|
548
|
-
# for nested wildcards like recipient(*)
|
|
549
|
-
if field_schema.nested_model:
|
|
550
|
-
if field_schema.is_list:
|
|
551
|
-
if isinstance(value, list):
|
|
552
|
-
nested_instances = []
|
|
553
|
-
for item in value:
|
|
554
|
-
if isinstance(item, dict):
|
|
555
|
-
# Parse all fields from the nested model
|
|
556
|
-
nested_instance = self._parse_nested_wildcard(
|
|
557
|
-
item, field_schema.nested_model
|
|
558
|
-
)
|
|
559
|
-
nested_instances.append(nested_instance)
|
|
560
|
-
else:
|
|
561
|
-
nested_instances.append(item)
|
|
562
|
-
result[result_field_name] = nested_instances
|
|
563
|
-
else:
|
|
564
|
-
result[result_field_name] = value
|
|
565
|
-
else:
|
|
566
|
-
if isinstance(value, dict):
|
|
567
|
-
nested_instance = self._parse_nested_wildcard(
|
|
568
|
-
value, field_schema.nested_model
|
|
569
|
-
)
|
|
570
|
-
result[result_field_name] = nested_instance
|
|
571
|
-
else:
|
|
572
|
-
result[result_field_name] = value
|
|
573
|
-
else:
|
|
574
|
-
# Not a nested model, just use the value
|
|
575
|
-
result[result_field_name] = value
|
|
576
|
-
|
|
577
545
|
else:
|
|
578
546
|
# Simple field - parse using appropriate parser
|
|
579
547
|
parsed_value = self._parse_field(
|
|
@@ -661,7 +629,7 @@ class ModelFactory:
|
|
|
661
629
|
raise ModelInstantiationError(
|
|
662
630
|
f"Could not resolve nested model '{nested_model}'"
|
|
663
631
|
)
|
|
664
|
-
return model_class
|
|
632
|
+
return cast(type, model_class)
|
|
665
633
|
except ImportError as err:
|
|
666
634
|
raise ModelInstantiationError(
|
|
667
635
|
f"Could not import models module to resolve '{nested_model}'"
|
|
@@ -704,41 +672,6 @@ class ModelFactory:
|
|
|
704
672
|
# Recursively create nested instance
|
|
705
673
|
return self.create_instance(data, nested_shape, resolved_model, nested_type)
|
|
706
674
|
|
|
707
|
-
def _parse_nested_wildcard(
|
|
708
|
-
self, data: dict[str, Any], nested_model: type | str
|
|
709
|
-
) -> dict[str, Any]:
|
|
710
|
-
"""Parse nested object with wildcard (all fields)
|
|
711
|
-
|
|
712
|
-
Args:
|
|
713
|
-
data: Nested object data
|
|
714
|
-
nested_model: Model class or string name for the nested object
|
|
715
|
-
|
|
716
|
-
Returns:
|
|
717
|
-
Dictionary with all parsed fields
|
|
718
|
-
"""
|
|
719
|
-
# Resolve nested model if it's a string
|
|
720
|
-
resolved_model = self._resolve_nested_model(nested_model)
|
|
721
|
-
|
|
722
|
-
# Ensure model is registered
|
|
723
|
-
if not self.schema_registry.is_registered(resolved_model):
|
|
724
|
-
self.schema_registry.register(resolved_model)
|
|
725
|
-
|
|
726
|
-
# Get model schema
|
|
727
|
-
model_schema = self.schema_registry.get_schema(resolved_model)
|
|
728
|
-
|
|
729
|
-
# Parse all fields
|
|
730
|
-
result: dict[str, Any] = {}
|
|
731
|
-
for field_name, value in data.items():
|
|
732
|
-
if field_name in model_schema:
|
|
733
|
-
field_schema = model_schema[field_name]
|
|
734
|
-
parsed_value = self._parse_field(field_name, value, field_schema.type, field_schema)
|
|
735
|
-
result[field_name] = parsed_value
|
|
736
|
-
else:
|
|
737
|
-
# Field not in schema, include as-is
|
|
738
|
-
result[field_name] = value
|
|
739
|
-
|
|
740
|
-
return result
|
|
741
|
-
|
|
742
675
|
def _parse_field(self, field_name: str, value: Any, field_type: type, field_schema: Any) -> Any:
|
|
743
676
|
"""Parse a single field value using appropriate parser
|
|
744
677
|
|
|
@@ -778,7 +711,7 @@ class ModelFactory:
|
|
|
778
711
|
return value
|
|
779
712
|
|
|
780
713
|
def validate_data(
|
|
781
|
-
self, data: dict[str, Any], shape_spec: ShapeSpec, base_model: type
|
|
714
|
+
self, data: dict[str, Any], shape_spec: ShapeSpec, base_model: type | str
|
|
782
715
|
) -> list[str]:
|
|
783
716
|
"""Validate that data matches the shape specification
|
|
784
717
|
|
|
@@ -803,11 +736,15 @@ class ModelFactory:
|
|
|
803
736
|
errors: list[str] = []
|
|
804
737
|
|
|
805
738
|
if not isinstance(data, dict):
|
|
806
|
-
errors.append(
|
|
739
|
+
errors.append( # type: ignore[unreachable]
|
|
740
|
+
f"Expected dictionary data, got {type(data).__name__}"
|
|
741
|
+
)
|
|
807
742
|
return errors
|
|
808
743
|
|
|
809
|
-
# Ensure model is registered
|
|
810
|
-
|
|
744
|
+
# Ensure model is registered. String model names are expected to be
|
|
745
|
+
# pre-registered (explicit schemas); only concrete classes can be
|
|
746
|
+
# auto-registered via introspection.
|
|
747
|
+
if isinstance(base_model, type) and not self.schema_registry.is_registered(base_model):
|
|
811
748
|
self.schema_registry.register(base_model)
|
|
812
749
|
|
|
813
750
|
# Get model schema
|
|
@@ -826,9 +763,8 @@ class ModelFactory:
|
|
|
826
763
|
|
|
827
764
|
# Check if field exists in schema
|
|
828
765
|
if field_spec.name not in model_schema:
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
)
|
|
766
|
+
model_name = base_model.__name__ if isinstance(base_model, type) else base_model
|
|
767
|
+
errors.append(f"Field '{field_spec.name}' does not exist in {model_name} schema")
|
|
832
768
|
continue
|
|
833
769
|
|
|
834
770
|
field_schema = model_schema[field_spec.name]
|
|
@@ -20,7 +20,7 @@ Examples:
|
|
|
20
20
|
import logging
|
|
21
21
|
import threading
|
|
22
22
|
from collections import OrderedDict
|
|
23
|
-
from typing import Any, get_args, get_origin, get_type_hints
|
|
23
|
+
from typing import Any, cast, get_args, get_origin, get_type_hints
|
|
24
24
|
|
|
25
25
|
from tango.exceptions import TypeGenerationError
|
|
26
26
|
from tango.shapes.models import ShapeSpec
|
|
@@ -250,7 +250,10 @@ class TypeGenerator:
|
|
|
250
250
|
|
|
251
251
|
field_schema = model_schema[field_spec.name]
|
|
252
252
|
|
|
253
|
-
# Determine field type
|
|
253
|
+
# Determine field type. The value is a heterogeneous mix of type
|
|
254
|
+
# objects, parameterized generics (list[...]), and union objects,
|
|
255
|
+
# so it is intentionally typed as Any.
|
|
256
|
+
field_type: Any
|
|
254
257
|
if field_spec.nested_fields:
|
|
255
258
|
# Generate nested type
|
|
256
259
|
if not field_schema.nested_model:
|
|
@@ -275,25 +278,7 @@ class TypeGenerator:
|
|
|
275
278
|
|
|
276
279
|
# Handle optional types
|
|
277
280
|
if field_schema.is_optional:
|
|
278
|
-
field_type = field_type | None
|
|
279
|
-
|
|
280
|
-
annotations[field_name] = field_type
|
|
281
|
-
|
|
282
|
-
elif field_spec.is_wildcard:
|
|
283
|
-
# Wildcard on nested field - use full model type
|
|
284
|
-
if field_schema.nested_model:
|
|
285
|
-
# Resolve nested model if it's a string
|
|
286
|
-
field_type = self._resolve_nested_model(field_schema.nested_model)
|
|
287
|
-
else:
|
|
288
|
-
field_type = field_schema.type
|
|
289
|
-
|
|
290
|
-
# Handle list types
|
|
291
|
-
if field_schema.is_list:
|
|
292
|
-
field_type = list[field_type] # type: ignore
|
|
293
|
-
|
|
294
|
-
# Handle optional types
|
|
295
|
-
if field_schema.is_optional:
|
|
296
|
-
field_type = field_type | None # type: ignore
|
|
281
|
+
field_type = field_type | None
|
|
297
282
|
|
|
298
283
|
annotations[field_name] = field_type
|
|
299
284
|
|
|
@@ -303,11 +288,11 @@ class TypeGenerator:
|
|
|
303
288
|
|
|
304
289
|
# Handle list types
|
|
305
290
|
if field_schema.is_list:
|
|
306
|
-
field_type = list[field_type]
|
|
291
|
+
field_type = list[field_type]
|
|
307
292
|
|
|
308
293
|
# Handle optional types
|
|
309
294
|
if field_schema.is_optional:
|
|
310
|
-
field_type = field_type | None
|
|
295
|
+
field_type = field_type | None
|
|
311
296
|
|
|
312
297
|
annotations[field_name] = field_type
|
|
313
298
|
|
|
@@ -329,7 +314,7 @@ class TypeGenerator:
|
|
|
329
314
|
field_type = field_schema.type
|
|
330
315
|
# Handle optional types
|
|
331
316
|
if field_schema.is_optional:
|
|
332
|
-
field_type = field_type | None
|
|
317
|
+
field_type = field_type | None
|
|
333
318
|
annotations[auto_field] = field_type
|
|
334
319
|
|
|
335
320
|
# Create TypedDict dynamically
|
|
@@ -414,7 +399,7 @@ class TypeGenerator:
|
|
|
414
399
|
model_class = getattr(models, nested_model, None)
|
|
415
400
|
if model_class is None:
|
|
416
401
|
raise TypeGenerationError(f"Could not resolve nested model '{nested_model}'")
|
|
417
|
-
return model_class
|
|
402
|
+
return cast(type, model_class)
|
|
418
403
|
except ImportError as err:
|
|
419
404
|
raise TypeGenerationError(
|
|
420
405
|
f"Could not import models module to resolve '{nested_model}'"
|
|
@@ -555,7 +540,7 @@ class TypeGenerator:
|
|
|
555
540
|
|
|
556
541
|
# Handle basic types
|
|
557
542
|
if hasattr(type_annotation, "__name__"):
|
|
558
|
-
type_name = type_annotation.__name__
|
|
543
|
+
type_name = str(type_annotation.__name__)
|
|
559
544
|
else:
|
|
560
545
|
type_name = str(type_annotation)
|
|
561
546
|
|
|
@@ -576,7 +561,7 @@ class TypeGenerator:
|
|
|
576
561
|
if args:
|
|
577
562
|
formatted_args = [self._format_type_annotation(arg) for arg in args]
|
|
578
563
|
return f"{origin.__name__}[{', '.join(formatted_args)}]"
|
|
579
|
-
return origin.__name__
|
|
564
|
+
return str(origin.__name__)
|
|
580
565
|
|
|
581
566
|
return type_name
|
|
582
567
|
|