A modern Python SDK for the Tango API by MakeGov, featuring dynamic response shaping and comprehensive type hints.
- Dynamic Response Shaping - Request only the fields you need, reducing payload sizes by 60-80%
- Full Type Safety - Runtime-generated TypedDict types with accurate type hints for IDE autocomplete
- Comprehensive API Coverage - All major Tango API endpoints (contracts, IDVs, OTAs, entities, forecasts, opportunities, notices, grants, protests, webhooks, and more)
- Flexible Data Access - Dictionary-based response objects with validation
- Modern Python - Built for Python 3.12+ using modern async-ready patterns
- Production-Ready - Comprehensive test suite with VCR.py-based integration tests
Requirements: Python 3.12 or higher
pip install tango-pythonOr with uv:
uv pip install tango-pythonfromtangoimportTangoClient, ShapeConfig# Initialize the clientclient=TangoClient(api_key="your-api-key")
# List agenciesagencies=client.list_agencies()
print(f"Found {agencies.count} agencies")
# Get specific agencyagency=client.get_agency("GSA")
print(f"Agency: {agency.name}")
# Search contractscontracts=client.list_contracts(
limit=10
)Most endpoints require an API key. You can obtain one from the Tango API portal.
# With API keyclient=TangoClient(api_key="your-api-key")
# From environment variable (TANGO_API_KEY)client=TangoClient()Response shaping is the most powerful feature of the Tango SDK. It lets you request only the fields you need, dramatically reducing payload sizes and improving performance.
fromtangoimportTangoClient, ShapeConfigclient=TangoClient(api_key="your-api-key")
# Custom shape - only fields you needcontracts=client.list_contracts(
shape="key,piid,recipient(display_name,uei),total_contract_value",
limit=10
)
# Access fields using dictionary syntax OR as an attributeforcontractincontracts.results:
print(f"PIID: {contract['piid']}")
print(f"Recipient: {contract['recipient']['display_name']}")
forcontractincontracts.results:
print(f"PIID: {contract.piid}")
print(f"Recipient: {contract.recipient.display_name}")# List all agenciesagencies=client.list_agencies(page=1, limit=25)
# Get specific agency by codeagency=client.get_agency("GSA")# List/search contracts with filteringcontracts=client.list_contracts(
page=1,
limit=25,
# Filter parameterskeyword="software",
awarding_agency="4700", # GSA agency codeaward_date_gte="2023-01-01",
fiscal_year=2024,
naics_code="541511"
)
# Filter by specific agencycontracts=client.list_contracts(
awarding_agency="4700", # GSAlimit=50
)Available Filter Parameters:
Text Search:
keyword- Search contract descriptions (mapped to 'search' API param)
Date Filters:
award_date_gte,award_date_lte- Award date rangepop_start_date_gte,pop_start_date_lte- Period of performance start date rangepop_end_date_gte,pop_end_date_lte- Period of performance end date rangeexpiring_gte,expiring_lte- Contract expiration date range
Party Filters:
awarding_agency,funding_agency- Agency codes, names, abbreviations, or organization UUIDs. Multi-value OR via|.recipient_name,recipient_uei- Vendor/recipient filters
Agency values are resolved fuzzily, so a token can match an organization you did not intend — which silently scopes the query to that organization's subtree. A short result set is then indistinguishable from "no such records exist". Responses expose what actually happened:
response=client.list_contracts(awarding_agency="HUD|HUDD")
# Tokens that matched nothing and were ignored.ifresponse.unresolved_agency_tokens:
raiseSystemExit(f"dropped: {response.unresolved_agency_tokens}")
# {'awarding_agency': ['HUDD']}# What the tokens that DID match resolved to — the only way to catch a# plausible-but-wrong match, where nothing was dropped at all.fororginresponse.resolved_agencies.get("awarding_agency", []):
print(org["name"], org["cgac"])
# Department of Housing and Urban Development 086forwarninginresponse.agency_warnings:
print(warning)If every token for a filter fails to resolve, the API returns 400 and the SDK raises
TangoValidationError naming the offending value, rather than an empty page.
Classification:
naics_code,psc_code- Industry/product codesset_aside_type- Set-aside type
Type Filters:
fiscal_year,fiscal_year_gte,fiscal_year_lte- Fiscal year filtersaward_type- Award type code
Identifiers:
piid- Procurement Instrument Identifiersolicitation_identifier- Solicitation ID
Sorting:
sort,order- Sort results (e.g.,sort="award_date",order="desc")
Response Options:
shape,flat,flat_lists- Response shaping options
# List IDVs (keyset pagination)idvs=client.list_idvs(limit=25, awarding_agency="4700")
# Get single IDV with shapingidv=client.get_idv("IDV_KEY", shape=ShapeConfig.IDVS_COMPREHENSIVE)
# OTAs and OTIDVs follow the same patternotas=client.list_otas(limit=25)
otidvs=client.list_otidvs(limit=25)vehicles=client.list_vehicles(
search="GSA schedule",
ordering="-vehicle_obligations",
shape=ShapeConfig.VEHICLES_MINIMAL,
)
vehicle=client.get_vehicle("UUID", shape=ShapeConfig.VEHICLES_COMPREHENSIVE)
awardees=client.list_vehicle_awardees("UUID")
orders=client.list_vehicle_orders("UUID", ordering="-obligated")# List entities with filtersentities=client.list_entities(search="Booz Allen", state="VA", limit=25)
# Get specific entity by UEI or CAGE codeentity=client.get_entity("ZQGGHJH74DW7")forecasts=client.list_forecasts(agency="GSA", fiscal_year=2025, limit=25)opportunities=client.list_opportunities(agency="DOD", active=True, limit=25)notices=client.list_notices(agency="DOD", notice_type="Presolicitation", limit=25)grants=client.list_grants(agency="HHS", status="F", limit=25) # F = Forecastedprotests=client.list_protests(source_system="gao", outcome="Sustained", limit=25)
protest=client.get_protest("CASE_UUID")contracts=client.list_gsa_elibrary_contracts(schedule="MAS", limit=25)
contract=client.get_gsa_elibrary_contract("UUID")# Offices, organizations, NAICS, PSC, subawards, business typesoffices=client.list_offices(search="acquisitions")
organizations=client.list_organizations(level=1)
naics=client.list_naics(search="software")
get_naics=client.get_naics("541511")
psc=client.list_psc()
subawards=client.list_subawards(prime_uei="UEI123")
business_types=client.list_business_types()
mas_sins=client.list_mas_sins()
assistance=client.list_assistance_listings()
departments=client.list_departments()# Resolve a name to entity/org candidatesresult=client.resolve(name="Lockheed Martin", target_type="entity")
forcinresult.candidates:
print(c.identifier, c.display_name)
# Validate an identifierresult=client.validate(identifier_type="uei", value="ABCDEF123456")investments=client.list_itdashboard_investments(search="cloud", limit=25)
investment=client.get_itdashboard_investment("023-000001234")contracts=client.list_entity_contracts("ABCDEF123456", limit=25)
idvs=client.list_entity_idvs("ABCDEF123456")
otas=client.list_entity_otas("ABCDEF123456")
metrics=client.get_entity_metrics("ABCDEF123456", months=12, period_grouping="month")All list methods return a PaginatedResponse object with metadata:
response=client.list_contracts(limit=25)
print(f"Total results: {response.count}")
print(f"Next page URL: {response.next}")
print(f"Previous page URL: {response.previous}")
# Iterate through resultsforcontractinresponse.results:
print(contract['description'])
# Get next page (contracts use keyset/cursor pagination)ifresponse.next:
next_response=client.list_contracts(cursor=response.cursor, limit=25)The SDK provides specific exception types for different error scenarios:
fromtangoimport (
TangoClient,
TangoAPIError,
TangoAuthError,
TangoNotFoundError,
TangoRateLimitError,
TangoValidationError
)
client=TangoClient(api_key="your-api-key")
try:
contracts=client.list_contracts(limit=10)
exceptTangoAuthError:
print("Invalid API key or authentication required")
exceptTangoNotFoundError:
print("Resource not found")
exceptTangoValidationErrorase:
print(f"Invalid parameters: {e.message}")
print(f"Details: {e.response_data}")
exceptTangoRateLimitError:
print("Rate limit exceeded")
exceptTangoAPIErrorase:
print(f"API error: {e.message}")Create custom shapes to request exactly the fields you need:
# Simple fieldscontracts=client.list_contracts(
shape="key,piid,description,total_contract_value"
)
# Nested relationshipscontracts=client.list_contracts(
shape="key,piid,recipient(display_name,uei),place_of_performance(*))"
)
# Wildcards for all fieldscontracts=client.list_contracts(
shape="key,piid,recipient(*)"
)The flat=True parameter is passed to the API, which returns dot-notation keys in the raw response. The SDK still wraps the result in a ShapedModel — access nested fields via attribute or dict syntax, not dot-notation string keys:
contracts=client.list_contracts(
shape="key,piid,recipient(display_name,uei)",
flat=True
)
forcontractincontracts.results:
# Attribute accessprint(contract.recipient.display_name)
# Dict access (nested, not flat string keys)print(contract['recipient']['display_name'])The SDK ships first-class tooling for building and testing webhook integrations against the Tango API — including signing helpers, a local receiver, and a command-line tool covering the full lifecycle:
pip install 'tango-python[webhooks]'This adds a tango console script with subcommands for the full webhook lifecycle:
# Discover what's available
tango webhooks list-event-types
tango webhooks fetch-sample --event-type entities.updated
# Local development
tango webhooks listen --port 8011 --secret $SECRET# receiver
tango webhooks simulate --secret $SECRET --event-type entities.updated # sign + print
tango webhooks simulate --secret $SECRET --event-type entities.updated \
--to http://127.0.0.1:8011/tango/webhooks # also POST# Manage delivery endpoints
tango webhooks endpoints create|list|get|delete
# Force a real test delivery from Tango
tango webhooks triggerThe signing helpers (verify_signature, generate_signature) are pure stdlib and importable from the default install — your receiver code doesn't need the extra:
fromtango.webhooksimportverify_signatureifnotverify_signature(raw_body, secret, request.headers.get("X-Tango-Signature")):
return401, "invalid signature"For the full guide — workflow, CLI reference, and programmatic patterns for pytest fixtures — see docs/WEBHOOKS.md.
Import TypedDict types for IDE autocomplete:
fromtangoimportTangoClient, ShapeConfigfromtango.shapesimportContractMinimalShapedclient=TangoClient(api_key="your-api-key")
contracts=client.list_contracts(shape=ShapeConfig.CONTRACTS_MINIMAL)
# Type hint enables IDE autocompletecontract: ContractMinimalShaped=contracts.results[0]
print(contract["piid"]) # IDE knows this field existsprint(contract["recipient"]["display_name"]) # Nested fields tooThis project uses uv for dependency management and tooling.
# Clone the repository
git clone https://github.com/makegov/tango-python.git
cd tango-python
# Install dependencies with uv
uv sync --all-extras
# Or install dev dependencies only
uv sync --group devThe SDK includes a comprehensive test suite with:
- Unit tests - Fast tests for core functionality
- Integration tests - Real API validation using VCR.py cassettes
# Run all tests
uv run pytest
# Run only unit tests
uv run pytest tests/ -m "not integration"# Run only integration tests
uv run pytest tests/integration/
# Run integration tests with live API (requires TANGO_API_KEY)export TANGO_API_KEY=your-api-key
export TANGO_USE_LIVE_API=true
uv run pytest tests/integration/
# Refresh cassettes with fresh API responsesexport TANGO_API_KEY=your-api-key
export TANGO_REFRESH_CASSETTES=true
uv run pytest tests/integration/See tests/integration/README.md for detailed testing documentation.
# Format code
uv run ruff format tango/
# Lint code
uv run ruff check tango/
# Type checking
uv run mypy tango/
# Run all checks
uv run ruff format tango/ && uv run ruff check tango/ && uv run mypy tango/tango-python/
├── tango/ # Main SDK package
│ ├── __init__.py # Public API exports
│ ├── client.py # TangoClient implementation
│ ├── models.py # Data models and shape configs
│ ├── exceptions.py # Exception classes
│ └── shapes/ # Dynamic model system
│ ├── __init__.py # Shapes package exports
│ ├── parser.py # Shape string parser
│ ├── generator.py # TypedDict generator
│ ├── factory.py # Instance factory
│ ├── schema.py # Schema registry
│ ├── explicit_schemas.py # Predefined schemas (Contract, Entity, Grant, etc.)
│ ├── models.py # Shape specification models
│ └── types.py # TypedDict exports
├── tests/ # Test suite
│ ├── __init__.py
│ ├── conftest.py # Pytest configuration
│ ├── test_client.py # Unit tests for client
│ ├── test_models.py # Model tests
│ ├── test_shapes.py # Shape system tests
│ ├── cassettes/ # VCR.py HTTP cassettes
│ └── integration/ # Integration tests
│ ├── __init__.py
│ ├── README.md # Integration test docs
│ ├── conftest.py # Integration test fixtures
│ ├── validation.py # Validation utilities
│ ├── test_agencies_integration.py
│ ├── test_contracts_integration.py
│ ├── test_entities_integration.py
│ ├── test_forecasts_integration.py
│ ├── test_grants_integration.py
│ ├── test_naics_integration.py
│ ├── test_notices_integration.py
│ ├── test_offices_integration.py
│ ├── test_opportunities_integration.py
│ ├── test_organizations_integration.py
│ ├── test_otas_otidvs_integration.py
│ ├── test_protests_integration.py
│ ├── test_reference_data_integration.py
│ ├── test_subawards_integration.py
│ ├── test_vehicles_idvs_integration.py
│ └── test_edge_cases_integration.py
├── docs/ # Documentation
│ ├── API_REFERENCE.md # Complete API reference
│ ├── DEVELOPERS.md # Developer guide
│ ├── SHAPES.md # Shape system guide
│ └── quick_start.ipynb # Interactive quick start
├── scripts/ # Utility scripts
│ ├── README.md
│ ├── check_filter_shape_conformance.py # Filter + shape conformance (CI)
│ ├── fetch_api_schema.py
│ ├── generate_schemas_from_api.py
│ └── pr_review.py # PR validation (lint, types, tests, conformance)
├── pyproject.toml # Project configuration
├── uv.lock # Dependency lock file
├── LICENSE # MIT License
├── CHANGELOG.md # Version history
└── README.md # This file
- Shape System Guide - Comprehensive guide to response shaping
- API Reference - Detailed API documentation
- Developer Guide - Technical documentation for developers
- Webhooks Guide - Workflow, CLI reference, and programmatic patterns for webhook integrations
- Quick Start Notebook - Interactive Jupyter notebook with examples
- Python 3.12 or higher
- httpx >= 0.27.0
MIT License - see LICENSE for details.
For questions, issues, or feature requests:
- Email: tango@makegov.com
- Issues: GitHub Issues
- Documentation: https://docs.makegov.com/tango-python
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Run lint and format:
uv run ruff format tango/ && uv run ruff check tango/ - Run type checking:
uv run mypy tango/ - Run tests:
uv run pytest - (Optional) Run filter and shape conformance if you have the tango API manifest; CI will run it on push/PR
- Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
For a single command that runs formatting, linting, type checking, and tests (and conformance when the manifest is present), use: uv run python scripts/pr_review.py --mode full