tango-python 1.5.0__py3-none-any.whl → 1.6.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -24,6 +24,9 @@ from .models import (
24
24
  SbirTopic,
25
25
  SearchFilters,
26
26
  ShapeConfig,
27
+ SledForecast,
28
+ SledOpportunity,
29
+ SledOpportunityRevision,
27
30
  ValidateResult,
28
31
  Vehicle,
29
32
  VehicleMetrics,
@@ -50,7 +53,7 @@ from .webhooks import (
50
53
  )
51
54
  from .webhooks.receiver import Delivery, WebhookReceiver
52
55
 
53
- __version__ = "1.5.0"
56
+ __version__ = "1.6.0"
54
57
  __all__ = [
55
58
  "TangoClient",
56
59
  "TangoAPIError",
@@ -73,6 +76,9 @@ __all__ = [
73
76
  "PaginatedResponse",
74
77
  "SearchFilters",
75
78
  "ShapeConfig",
79
+ "SledForecast",
80
+ "SledOpportunity",
81
+ "SledOpportunityRevision",
76
82
  "ValidateResult",
77
83
  "Vehicle",
78
84
  "VehicleMetrics",
tango/client.py CHANGED
@@ -46,6 +46,9 @@ from tango.models import (
46
46
  SbirTopic,
47
47
  SearchFilters,
48
48
  ShapeConfig,
49
+ SledForecast,
50
+ SledOpportunity,
51
+ SledOpportunityRevision,
49
52
  Subaward,
50
53
  ValidateResult,
51
54
  Vehicle,
@@ -1596,12 +1599,15 @@ class TangoClient:
1596
1599
  cio_rating: int | None = None,
1597
1600
  cio_rating_max: int | None = None,
1598
1601
  performance_risk: bool | None = None,
1602
+ previous_uii: str | None = None,
1599
1603
  ) -> PaginatedResponse:
1600
1604
  """List federal IT investments from the IT Dashboard (`/api/itdashboard/`).
1601
1605
 
1602
1606
  Filters are tier-gated by the API:
1603
1607
 
1604
- - **Free**: ``search`` (full-text across UII, title, description, agency, bureau)
1608
+ - **Free**: ``search`` (full-text across UII, title, description, agency,
1609
+ bureau) and ``previous_uii`` — following a retired identifier forward to
1610
+ whatever superseded it is recovery, not analysis, so it is ungated
1605
1611
  - **Pro**: ``agency_code``, ``type_of_investment``,
1606
1612
  ``updated_time_after`` / ``updated_time_before``
1607
1613
  - **Business+**: ``agency_name`` (text), ``cio_rating``,
@@ -1633,6 +1639,7 @@ class TangoClient:
1633
1639
  ("cio_rating", cio_rating),
1634
1640
  ("cio_rating_max", cio_rating_max),
1635
1641
  ("performance_risk", performance_risk),
1642
+ ("previous_uii", previous_uii),
1636
1643
  ):
1637
1644
  if val is None:
1638
1645
  continue
@@ -2081,7 +2088,7 @@ class TangoClient:
2081
2088
  name: Entity name filter
2082
2089
  psc: PSC code filter
2083
2090
  purpose_of_registration_code: Purpose of registration code
2084
- socioeconomic: Socioeconomic status filter
2091
+ socioeconomic: Socioeconomic status filter; takes SAM business-type codes (e.g. "OY" Black American Owned, "A6" SBA-certified 8(a), "A2" Woman Owned — see `GET /api/business_types/`), not set-aside codes, and accepts pipe-separated values for OR semantics, e.g. ``socioeconomic="OY|A2"``
2085
2092
  state: State filter
2086
2093
  total_awards_obligated_gte: Total awards obligated >=
2087
2094
  total_awards_obligated_lte: Total awards obligated <=
@@ -3464,6 +3471,450 @@ class TangoClient:
3464
3471
  data = self._get(f"/api/sbir/solicitations/{solicitation_id}/", params)
3465
3472
  return self._parse_response_with_shape(data, shape, SbirSolicitation, flat, flat_lists)
3466
3473
 
3474
+ # ============================================================================
3475
+ # State, local and education (SLED) procurement
3476
+ # ============================================================================
3477
+
3478
+ def list_sled_opportunities(
3479
+ self,
3480
+ page: int = 1,
3481
+ limit: int = 25,
3482
+ shape: str | None = None,
3483
+ flat: bool = False,
3484
+ flat_lists: bool = False,
3485
+ state: str | None = None,
3486
+ jurisdiction: str | None = None,
3487
+ status: str | None = None,
3488
+ active: bool | None = None,
3489
+ agency: str | None = None,
3490
+ solicitation_number: str | None = None,
3491
+ solicitation_type: str | None = None,
3492
+ has_documents: bool | None = None,
3493
+ revision_kind: str | None = None,
3494
+ naics: str | None = None,
3495
+ nigp: str | None = None,
3496
+ unspsc: str | None = None,
3497
+ category: str | None = None,
3498
+ category_code: str | None = None,
3499
+ posted_after: str | None = None,
3500
+ posted_before: str | None = None,
3501
+ response_deadline_after: str | None = None,
3502
+ response_deadline_before: str | None = None,
3503
+ first_seen_after: str | None = None,
3504
+ first_seen_before: str | None = None,
3505
+ change_seen_after: str | None = None,
3506
+ modified_after: str | None = None,
3507
+ modified_before: str | None = None,
3508
+ platform: str | None = None,
3509
+ native_id: str | None = None,
3510
+ external_id: str | None = None,
3511
+ search: str | None = None,
3512
+ ordering: str | None = None,
3513
+ ) -> PaginatedResponse:
3514
+ """
3515
+ List state, local and education (SLED) solicitations.
3516
+
3517
+ API reference: https://tango.makegov.com/docs/api-reference/sled.md
3518
+
3519
+ Beta. Coverage is partial and grows one jurisdiction at a time, so a thin
3520
+ per-state result is at least as likely to be a portal Tango does not read
3521
+ as a quiet market. Call :meth:`get_sled_coverage` before treating a
3522
+ per-state count as market size.
3523
+
3524
+ Args:
3525
+ page: Page number
3526
+ limit: Results per page (max 100)
3527
+ shape: Response shape string (defaults to minimal shape)
3528
+ flat: If True, flatten nested objects in shaped response
3529
+ flat_lists: If True, flatten arrays using indexed keys
3530
+ state: Two-letter state or territory code. Multi-value: ``TX|OK``
3531
+ jurisdiction: Level of government — ``state``, ``local``,
3532
+ ``education``, or ``unknown`` for aggregator rows that cannot
3533
+ tell state from local
3534
+ status: Tango-derived liveness — ``open``, ``closed``, ``awarded``,
3535
+ ``cancelled`` or ``unknown``. **Passing neither this nor**
3536
+ ``active`` **returns open solicitations only.** ``unknown``
3537
+ (standing rosters, dateless RFIs) is hidden by that default —
3538
+ reach it with ``status="open|unknown"``
3539
+ active: Sugar for federal-shaped callers. ``True`` is
3540
+ ``status="open"``; ``False`` is its complement, so it includes
3541
+ ``unknown``
3542
+ agency: Substring match on the buyer's published text (min 2
3543
+ characters). There is no code resolution behind it — state
3544
+ agencies have no entry in the federal organization tree
3545
+ solicitation_number: The number a human would quote. Null on roughly
3546
+ a third of the corpus, where the portal publishes none
3547
+ solicitation_type: ``rfp``, ``ifb``, ``rfq``, ``rfi``, ``itb``,
3548
+ ``sole_source``, ``grant`` or ``other``. ``null`` (the portal
3549
+ states no type) is a distinct answer from ``other``
3550
+ has_documents: Whether the solicitation advertises at least one
3551
+ document
3552
+ revision_kind: Kind of the most recent substantive revision —
3553
+ ``deadline_change``, ``status_change``, ``documents_added``,
3554
+ ``documents_removed``, ``documents_replaced``, ``title_change``
3555
+ or ``content_change``
3556
+ naics: Exact match within the ``naics`` category scheme. Thin on
3557
+ purpose: scheme tagging is mid-migration, so only a small share
3558
+ of entries are tagged NAICS. Use ``category_code`` instead unless
3559
+ you need scheme precision
3560
+ nigp: Exact match within the ``nigp`` scheme
3561
+ unspsc: Exact match within the ``unspsc`` scheme
3562
+ category: Exact match within the ``text`` scheme, where the code is
3563
+ the portal's own human label
3564
+ category_code: Match a code under ANY scheme, including the untagged
3565
+ pre-migration strings. The escape hatch when a scheme-specific
3566
+ filter returns less than you expected
3567
+ posted_after: Posted date on or after (YYYY-MM-DD)
3568
+ posted_before: Posted date on or before (YYYY-MM-DD)
3569
+ response_deadline_after: Response deadline on or after (YYYY-MM-DD)
3570
+ response_deadline_before: Response deadline on or before (YYYY-MM-DD)
3571
+ first_seen_after: When Tango FIRST OBSERVED the solicitation, on or
3572
+ after (YYYY-MM-DD). The polling primitive
3573
+ first_seen_before: First observed on or before (YYYY-MM-DD)
3574
+ change_seen_after: When Tango OBSERVED the last substantive change,
3575
+ on or after (YYYY-MM-DD). A scrape date, not an amendment date
3576
+ modified_after: Tango row last changed on or after (YYYY-MM-DD)
3577
+ modified_before: Tango row last changed on or before (YYYY-MM-DD)
3578
+ platform: Support filter identifying the source portal's platform
3579
+ family. Not in any response shape, and not a stable value
3580
+ native_id: Support filter — the portal's own identifier
3581
+ external_id: Support filter — Tango's opaque lake key. At most 500
3582
+ values
3583
+ search: Ranked full-text search over title, agency, identifiers,
3584
+ category labels and description, widened by the solicitations
3585
+ whose ATTACHMENT text matched (min 2 characters). Adds a
3586
+ ``snippet`` to rows that matched on their description
3587
+ ordering: Sort field — ``rank``, ``response_deadline``,
3588
+ ``posted_date``, ``first_seen_at``, ``last_seen_at``,
3589
+ ``last_change_seen_at``, ``modified``. ``rank`` requires a
3590
+ non-empty ``search``
3591
+ """
3592
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
3593
+
3594
+ if shape is None:
3595
+ shape = ShapeConfig.SLED_OPPORTUNITIES_MINIMAL
3596
+ if shape:
3597
+ params["shape"] = shape
3598
+ if flat:
3599
+ params["flat"] = "true"
3600
+ if flat_lists:
3601
+ params["flat_lists"] = "true"
3602
+
3603
+ for key, val in (
3604
+ ("state", state),
3605
+ ("jurisdiction", jurisdiction),
3606
+ ("status", status),
3607
+ ("active", active),
3608
+ ("agency", agency),
3609
+ ("solicitation_number", solicitation_number),
3610
+ ("solicitation_type", solicitation_type),
3611
+ ("has_documents", has_documents),
3612
+ ("revision_kind", revision_kind),
3613
+ ("naics", naics),
3614
+ ("nigp", nigp),
3615
+ ("unspsc", unspsc),
3616
+ ("category", category),
3617
+ ("category_code", category_code),
3618
+ ("posted_after", posted_after),
3619
+ ("posted_before", posted_before),
3620
+ ("response_deadline_after", response_deadline_after),
3621
+ ("response_deadline_before", response_deadline_before),
3622
+ ("first_seen_after", first_seen_after),
3623
+ ("first_seen_before", first_seen_before),
3624
+ ("change_seen_after", change_seen_after),
3625
+ ("modified_after", modified_after),
3626
+ ("modified_before", modified_before),
3627
+ ("platform", platform),
3628
+ ("native_id", native_id),
3629
+ ("external_id", external_id),
3630
+ ("search", search),
3631
+ ("ordering", ordering),
3632
+ ):
3633
+ if val is not None:
3634
+ params[key] = val
3635
+
3636
+ data = self._get("/api/sled/opportunities/", params)
3637
+
3638
+ results = [
3639
+ self._parse_response_with_shape(item, shape, SledOpportunity, flat, flat_lists)
3640
+ for item in data["results"]
3641
+ ]
3642
+
3643
+ return PaginatedResponse(
3644
+ count=data["count"],
3645
+ next=data.get("next"),
3646
+ previous=data.get("previous"),
3647
+ results=results,
3648
+ meta=data.get("meta"),
3649
+ )
3650
+
3651
+ def get_sled_opportunity(
3652
+ self,
3653
+ opportunity_id: str,
3654
+ shape: str | None = None,
3655
+ flat: bool = False,
3656
+ flat_lists: bool = False,
3657
+ ) -> Any:
3658
+ """
3659
+ Get a single SLED solicitation by opportunity_id, whatever its status.
3660
+
3661
+ API reference: https://tango.makegov.com/docs/api-reference/sled.md
3662
+
3663
+ The open-only default applies to the list endpoint, not here — a closed
3664
+ solicitation still resolves on its own URL.
3665
+
3666
+ Args:
3667
+ opportunity_id: Solicitation UUID
3668
+ shape: Response shape string (defaults to the comprehensive shape)
3669
+ flat: If True, flatten nested objects in shaped response
3670
+ flat_lists: If True, flatten arrays using indexed keys
3671
+ """
3672
+ params: dict[str, Any] = {}
3673
+ if shape is None:
3674
+ shape = ShapeConfig.SLED_OPPORTUNITIES_COMPREHENSIVE
3675
+ if shape:
3676
+ params["shape"] = shape
3677
+ if flat:
3678
+ params["flat"] = "true"
3679
+ if flat_lists:
3680
+ params["flat_lists"] = "true"
3681
+
3682
+ data = self._get(f"/api/sled/opportunities/{opportunity_id}/", params)
3683
+ return self._parse_response_with_shape(data, shape, SledOpportunity, flat, flat_lists)
3684
+
3685
+ def list_sled_opportunity_revisions(
3686
+ self,
3687
+ opportunity_id: str,
3688
+ page: int = 1,
3689
+ limit: int = 25,
3690
+ shape: str | None = None,
3691
+ flat: bool = False,
3692
+ flat_lists: bool = False,
3693
+ kind: str | None = None,
3694
+ source_declared: bool | None = None,
3695
+ observed_after: str | None = None,
3696
+ observed_before: str | None = None,
3697
+ ) -> PaginatedResponse:
3698
+ """
3699
+ List one SLED solicitation's observed revision history.
3700
+
3701
+ API reference: https://tango.makegov.com/docs/api-reference/sled.md
3702
+
3703
+ ``observed_at`` is the scrape that saw the change, not the date the agency
3704
+ made it: no state portal emits amendment notices, so resolution is that
3705
+ state's crawl cadence and history starts when Tango began reading the
3706
+ jurisdiction rather than when the solicitation was posted.
3707
+
3708
+ Unlike the ``revisions(*)`` expand on the solicitation, this route serves
3709
+ ``enrichment`` rows — Tango's own detail fetch filling in coverage rather
3710
+ than an agency amendment. Pass ``kind="enrichment"`` to see only those.
3711
+
3712
+ Args:
3713
+ opportunity_id: Solicitation UUID
3714
+ page: Page number
3715
+ limit: Results per page (max 100)
3716
+ shape: Response shape string (defaults to minimal shape). ``changes``
3717
+ — the per-field before and after — needs a Small plan; name it
3718
+ explicitly when the caller has one
3719
+ flat: If True, flatten nested objects in shaped response
3720
+ flat_lists: If True, flatten arrays using indexed keys
3721
+ kind: Revision kind, plus ``enrichment``. Multi-value: use ``|``
3722
+ source_declared: Whether the portal's own amendment marker moved at
3723
+ this emission. True on about 5% of revisions; everything else is
3724
+ Tango inferring the change from the diff
3725
+ observed_after: Observed on or after (YYYY-MM-DD)
3726
+ observed_before: Observed on or before (YYYY-MM-DD)
3727
+ """
3728
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
3729
+
3730
+ if shape is None:
3731
+ shape = ShapeConfig.SLED_REVISIONS_MINIMAL
3732
+ if shape:
3733
+ params["shape"] = shape
3734
+ if flat:
3735
+ params["flat"] = "true"
3736
+ if flat_lists:
3737
+ params["flat_lists"] = "true"
3738
+
3739
+ for key, val in (
3740
+ ("kind", kind),
3741
+ ("source_declared", source_declared),
3742
+ ("observed_after", observed_after),
3743
+ ("observed_before", observed_before),
3744
+ ):
3745
+ if val is not None:
3746
+ params[key] = val
3747
+
3748
+ data = self._get(f"/api/sled/opportunities/{opportunity_id}/revisions/", params)
3749
+
3750
+ results = [
3751
+ self._parse_response_with_shape(item, shape, SledOpportunityRevision, flat, flat_lists)
3752
+ for item in data["results"]
3753
+ ]
3754
+
3755
+ return PaginatedResponse(
3756
+ count=data["count"],
3757
+ next=data.get("next"),
3758
+ previous=data.get("previous"),
3759
+ results=results,
3760
+ meta=data.get("meta"),
3761
+ )
3762
+
3763
+ def get_sled_coverage(self) -> dict[str, Any]:
3764
+ """
3765
+ Get the per-state SLED coverage rollup.
3766
+
3767
+ API reference: https://tango.makegov.com/docs/api-reference/sled.md
3768
+
3769
+ Returns corpus totals plus one row per jurisdiction — the total, the count
3770
+ in each of the five statuses, the jurisdiction levels present, and when a
3771
+ solicitation there last changed. It answers one question: whether a thin
3772
+ result for a state is a thin market or a portal Tango does not read.
3773
+
3774
+ Every state row carries all five status buckets whether or not they have
3775
+ rows, so a total and two buckets never invite subtraction. Takes no
3776
+ parameters and is not shaped or paginated.
3777
+ """
3778
+ return self._get("/api/sled/opportunities/coverage/", {})
3779
+
3780
+ def list_sled_forecasts(
3781
+ self,
3782
+ page: int = 1,
3783
+ limit: int = 25,
3784
+ shape: str | None = None,
3785
+ flat: bool = False,
3786
+ flat_lists: bool = False,
3787
+ state: str | None = None,
3788
+ agency: str | None = None,
3789
+ procurement_category: str | None = None,
3790
+ procurement_method: str | None = None,
3791
+ contract_number: str | None = None,
3792
+ incumbent_name: str | None = None,
3793
+ advertisement_after: str | None = None,
3794
+ advertisement_before: str | None = None,
3795
+ first_seen_after: str | None = None,
3796
+ first_seen_before: str | None = None,
3797
+ modified_after: str | None = None,
3798
+ modified_before: str | None = None,
3799
+ search: str | None = None,
3800
+ ordering: str | None = None,
3801
+ ) -> PaginatedResponse:
3802
+ """
3803
+ List planned state procurements (SLED forecasts).
3804
+
3805
+ API reference: https://tango.makegov.com/docs/api-reference/sled.md
3806
+
3807
+ Forecasts carry no liveness at all — there is no deadline to have passed,
3808
+ so there is no ``status`` filter and no open-only default. Currency is the
3809
+ caller's call from ``estimated_advertisement_date``, which is the START of
3810
+ the published quarter rather than a posting date.
3811
+
3812
+ Args:
3813
+ page: Page number
3814
+ limit: Results per page (max 100)
3815
+ shape: Response shape string (defaults to minimal shape)
3816
+ flat: If True, flatten nested objects in shaped response
3817
+ flat_lists: If True, flatten arrays using indexed keys
3818
+ state: Two-letter state code. Multi-value: use ``|``
3819
+ agency: Substring match on the buyer's published text (min 2
3820
+ characters)
3821
+ procurement_category: The portal's own category word, verbatim
3822
+ procurement_method: The portal's own method word, verbatim
3823
+ contract_number: The number the state expects to award under, where it
3824
+ publishes one in advance
3825
+ incumbent_name: Substring match on the incumbent vendor's name as
3826
+ published. NOT resolved to a Tango entity
3827
+ advertisement_after: Estimated advertisement date on or after
3828
+ (YYYY-MM-DD). Remember the date is a quarter start
3829
+ advertisement_before: Estimated advertisement date on or before
3830
+ (YYYY-MM-DD)
3831
+ first_seen_after: When Tango first observed the forecast, on or after
3832
+ (YYYY-MM-DD)
3833
+ first_seen_before: First observed on or before (YYYY-MM-DD)
3834
+ modified_after: Tango row last changed on or after (YYYY-MM-DD)
3835
+ modified_before: Tango row last changed on or before (YYYY-MM-DD)
3836
+ search: Ranked full-text search over title, agency and description
3837
+ (min 2 characters)
3838
+ ordering: Sort field — ``rank``, ``estimated_advertisement_date``,
3839
+ ``first_seen_at``, ``last_seen_at``, ``modified``. ``rank``
3840
+ requires a non-empty ``search``
3841
+ """
3842
+ params: dict[str, Any] = {"page": page, "limit": min(limit, 100)}
3843
+
3844
+ if shape is None:
3845
+ shape = ShapeConfig.SLED_FORECASTS_MINIMAL
3846
+ if shape:
3847
+ params["shape"] = shape
3848
+ if flat:
3849
+ params["flat"] = "true"
3850
+ if flat_lists:
3851
+ params["flat_lists"] = "true"
3852
+
3853
+ for key, val in (
3854
+ ("state", state),
3855
+ ("agency", agency),
3856
+ ("procurement_category", procurement_category),
3857
+ ("procurement_method", procurement_method),
3858
+ ("contract_number", contract_number),
3859
+ ("incumbent_name", incumbent_name),
3860
+ ("advertisement_after", advertisement_after),
3861
+ ("advertisement_before", advertisement_before),
3862
+ ("first_seen_after", first_seen_after),
3863
+ ("first_seen_before", first_seen_before),
3864
+ ("modified_after", modified_after),
3865
+ ("modified_before", modified_before),
3866
+ ("search", search),
3867
+ ("ordering", ordering),
3868
+ ):
3869
+ if val is not None:
3870
+ params[key] = val
3871
+
3872
+ data = self._get("/api/sled/forecasts/", params)
3873
+
3874
+ results = [
3875
+ self._parse_response_with_shape(item, shape, SledForecast, flat, flat_lists)
3876
+ for item in data["results"]
3877
+ ]
3878
+
3879
+ return PaginatedResponse(
3880
+ count=data["count"],
3881
+ next=data.get("next"),
3882
+ previous=data.get("previous"),
3883
+ results=results,
3884
+ meta=data.get("meta"),
3885
+ )
3886
+
3887
+ def get_sled_forecast(
3888
+ self,
3889
+ forecast_id: str,
3890
+ shape: str | None = None,
3891
+ flat: bool = False,
3892
+ flat_lists: bool = False,
3893
+ ) -> Any:
3894
+ """
3895
+ Get a single SLED forecast by forecast_id.
3896
+
3897
+ API reference: https://tango.makegov.com/docs/api-reference/sled.md
3898
+
3899
+ Args:
3900
+ forecast_id: Forecast UUID
3901
+ shape: Response shape string (defaults to the comprehensive shape)
3902
+ flat: If True, flatten nested objects in shaped response
3903
+ flat_lists: If True, flatten arrays using indexed keys
3904
+ """
3905
+ params: dict[str, Any] = {}
3906
+ if shape is None:
3907
+ shape = ShapeConfig.SLED_FORECASTS_COMPREHENSIVE
3908
+ if shape:
3909
+ params["shape"] = shape
3910
+ if flat:
3911
+ params["flat"] = "true"
3912
+ if flat_lists:
3913
+ params["flat_lists"] = "true"
3914
+
3915
+ data = self._get(f"/api/sled/forecasts/{forecast_id}/", params)
3916
+ return self._parse_response_with_shape(data, shape, SledForecast, flat, flat_lists)
3917
+
3467
3918
  # ============================================================================
3468
3919
  # Budget (federal account x fiscal year rollups)
3469
3920
  # ============================================================================