Skip to main content

Tango Python SDK

PyPI Python Versions License: MIT

A modern Python SDK for the Tango API by MakeGov, featuring dynamic response shaping and comprehensive type hints.

Features

  • 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

Installation

Requirements: Python 3.12 or higher

pip install tango-python

Or with uv:

uv pip install tango-python

Quick Start

from tango import TangoClient, ShapeConfig

# Initialize the client
client = TangoClient(api_key="your-api-key")

# List agencies
agencies = client.list_agencies()
print(f"Found {agencies.count} agencies")

# Get specific agency
agency = client.get_agency("GSA")
print(f"Agency: {agency.name}")

# Search contracts
contracts = client.list_contracts(
    limit=10
)

Authentication

Most endpoints require an API key. You can obtain one from the Tango API portal.

# With API key
client = TangoClient(api_key="your-api-key")

# From environment variable (TANGO_API_KEY)
client = TangoClient()

Core Concepts

Dynamic Response Shaping

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.

from tango import TangoClient, ShapeConfig

client = TangoClient(api_key="your-api-key")

# Custom shape - only fields you need
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name,uei),total_contract_value",
    limit=10
)

# Access fields using dictionary syntax OR as an attribute
for contract in contracts.results:
    print(f"PIID: {contract['piid']}")
    print(f"Recipient: {contract['recipient']['display_name']}")

for contract in contracts.results:
    print(f"PIID: {contract.piid}")
    print(f"Recipient: {contract.recipient.display_name}")

API Methods

Agencies

# List all agencies
agencies = client.list_agencies(page=1, limit=25)

# Get specific agency by code
agency = client.get_agency("GSA")

Contracts

# List/search contracts with filtering
contracts = client.list_contracts(
    page=1,
    limit=25,
    # Filter parameters
    keyword="software",
    awarding_agency="4700",  # GSA agency code
    award_date_gte="2023-01-01",
    fiscal_year=2024,
    naics_code="541511"
)

# Filter by specific agency
contracts = client.list_contracts(
    awarding_agency="4700",  # GSA
    limit=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 range
  • pop_start_date_gte, pop_start_date_lte - Period of performance start date range
  • pop_end_date_gte, pop_end_date_lte - Period of performance end date range
  • expiring_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

Checking how agency filters resolved

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.
if response.unresolved_agency_tokens:
    raise SystemExit(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.
for org in response.resolved_agencies.get("awarding_agency", []):
    print(org["name"], org["cgac"])
    # Department of Housing and Urban Development 086

for warning in response.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 codes
  • set_aside_type - Set-aside type

Type Filters:

  • fiscal_year, fiscal_year_gte, fiscal_year_lte - Fiscal year filters
  • award_type - Award type code

Identifiers:

  • piid - Procurement Instrument Identifier
  • solicitation_identifier - Solicitation ID

Sorting:

  • sort, order - Sort results (e.g., sort="award_date", order="desc")

Response Options:

  • shape, flat, flat_lists - Response shaping options

IDVs, OTAs, OTIDVs

# List IDVs (keyset pagination)
idvs = client.list_idvs(limit=25, awarding_agency="4700")

# Get single IDV with shaping
idv = client.get_idv("IDV_KEY", shape=ShapeConfig.IDVS_COMPREHENSIVE)

# OTAs and OTIDVs follow the same pattern
otas = client.list_otas(limit=25)
otidvs = client.list_otidvs(limit=25)

Vehicles

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")

Entities (Vendors/Recipients)

# List entities with filters
entities = client.list_entities(search="Booz Allen", state="VA", limit=25)

# Get specific entity by UEI or CAGE code
entity = client.get_entity("ZQGGHJH74DW7")

Forecasts

forecasts = client.list_forecasts(agency="GSA", fiscal_year=2025, limit=25)

Opportunities

opportunities = client.list_opportunities(agency="DOD", active=True, limit=25)

Notices

notices = client.list_notices(agency="DOD", notice_type="Presolicitation", limit=25)

Grants

grants = client.list_grants(agency="HHS", status="F", limit=25)  # F = Forecasted

Protests

protests = client.list_protests(source_system="gao", outcome="Sustained", limit=25)
protest = client.get_protest("CASE_UUID")

GSA eLibrary Contracts

contracts = client.list_gsa_elibrary_contracts(schedule="MAS", limit=25)
contract = client.get_gsa_elibrary_contract("UUID")

Reference Data

# Offices, organizations, NAICS, PSC, subawards, business types
offices = 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 / Validate

# Resolve a name to entity/org candidates
result = client.resolve(name="Lockheed Martin", target_type="entity")
for c in result.candidates:
    print(c.identifier, c.display_name)

# Validate an identifier
result = client.validate(identifier_type="uei", value="ABCDEF123456")

IT Dashboard

investments = client.list_itdashboard_investments(search="cloud", limit=25)
investment = client.get_itdashboard_investment("023-000001234")

Entity Sub-resources

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")

Pagination

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 results
for contract in response.results:
    print(contract['description'])

# Get next page (contracts use keyset/cursor pagination)
if response.next:
    next_response = client.list_contracts(cursor=response.cursor, limit=25)

Error Handling

The SDK provides specific exception types for different error scenarios:

from tango import (
    TangoClient,
    TangoAPIError,
    TangoAuthError,
    TangoNotFoundError,
    TangoRateLimitError,
    TangoValidationError
)

client = TangoClient(api_key="your-api-key")

try:
    contracts = client.list_contracts(limit=10)
except TangoAuthError:
    print("Invalid API key or authentication required")
except TangoNotFoundError:
    print("Resource not found")
except TangoValidationError as e:
    print(f"Invalid parameters: {e.message}")
    print(f"Details: {e.response_data}")
except TangoRateLimitError:
    print("Rate limit exceeded")
except TangoAPIError as e:
    print(f"API error: {e.message}")

Advanced Features

Custom Shapes

Create custom shapes to request exactly the fields you need:

# Simple fields
contracts = client.list_contracts(
    shape="key,piid,description,total_contract_value"
)

# Nested relationships
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name,uei),place_of_performance(*))"
)

# Wildcards for all fields
contracts = client.list_contracts(
    shape="key,piid,recipient(*)"
)

Flattened Responses

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
)
for contract in contracts.results:
    # Attribute access
    print(contract.recipient.display_name)
    # Dict access (nested, not flat string keys)
    print(contract['recipient']['display_name'])

Webhook Tooling

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 trigger

The signing helpers (verify_signature, generate_signature) are pure stdlib and importable from the default install — your receiver code doesn't need the extra:

from tango.webhooks import verify_signature

if not verify_signature(raw_body, secret, request.headers.get("X-Tango-Signature")):
    return 401, "invalid signature"

For the full guide — workflow, CLI reference, and programmatic patterns for pytest fixtures — see docs/WEBHOOKS.md.

Type Hints with IDE Support

Import TypedDict types for IDE autocomplete:

from tango import TangoClient, ShapeConfig
from tango.shapes import ContractMinimalShaped

client = TangoClient(api_key="your-api-key")
contracts = client.list_contracts(shape=ShapeConfig.CONTRACTS_MINIMAL)

# Type hint enables IDE autocomplete
contract: ContractMinimalShaped = contracts.results[0]
print(contract["piid"])  # IDE knows this field exists
print(contract["recipient"]["display_name"])  # Nested fields too

Development

This project uses uv for dependency management and tooling.

Setup

# 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 dev

Testing

The 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 responses
export 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.

Code Quality

# 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/

Project Structure

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

Documentation

Requirements

  • Python 3.12 or higher
  • httpx >= 0.27.0

License

MIT License - see LICENSE for details.

Support

For questions, issues, or feature requests:

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Run lint and format: uv run ruff format tango/ && uv run ruff check tango/
  4. Run type checking: uv run mypy tango/
  5. Run tests: uv run pytest
  6. (Optional) Run filter and shape conformance if you have the tango API manifest; CI will run it on push/PR
  7. Commit your changes (git commit -m 'Add amazing feature')
  8. Push to the branch (git push origin feature/amazing-feature)
  9. 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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tango_python-1.5.0.tar.gz (414.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tango_python-1.5.0-py3-none-any.whl (104.4 kB view details)

Uploaded Python 3

File details

Details for the file tango_python-1.5.0.tar.gz.

File metadata

  • Download URL: tango_python-1.5.0.tar.gz
  • Upload date:
  • Size: 414.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for tango_python-1.5.0.tar.gz
Algorithm Hash digest
SHA256 13694bb06bcbb0ecd10f3d3533fa77f909948bc99d6b4e70def3e0cdc0964b46
MD5 8442711750d31e9e8a0e3c70176917a6
BLAKE2b-256 f6d3f06b059ba49762312832015c7b4ea368010bd34d19e7be016ba196428667

See more details on using hashes here.

File details

Details for the file tango_python-1.5.0-py3-none-any.whl.

File metadata

  • Download URL: tango_python-1.5.0-py3-none-any.whl
  • Upload date:
  • Size: 104.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.12.1 {"installer":{"name":"uv","version":"0.12.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for tango_python-1.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 fc5daf462bc1c1a65e2d5b4e10492ea15fd81d3bd2ae37d35af87726743d7770
MD5 eacdaa4542f93c04b2ed60c391945870
BLAKE2b-256 91387978c6684b5e68e9e8c8854a94d509e002b91431729ca3afc52a08a42062

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page