tango-python 1.18.1__py3-none-any.whl → 1.19.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
@@ -62,7 +62,7 @@ from .webhooks import (
62
62
  )
63
63
  from .webhooks.receiver import Delivery, WebhookReceiver
64
64
 
65
- __version__ = "1.18.1"
65
+ __version__ = "1.19.0"
66
66
  __all__ = [
67
67
  "TangoClient",
68
68
  "TangoAPIError",
tango/client.py CHANGED
@@ -100,6 +100,10 @@ def _error_reason(error_data: dict[str, Any]) -> str | None:
100
100
  return None
101
101
 
102
102
 
103
+ # Funding-organization keys the metrics endpoints accept in `group_by`.
104
+ MetricsGroupBy = Literal["agency", "department"]
105
+
106
+
103
107
  class TangoClient:
104
108
  """Tango API Client"""
105
109
 
@@ -5615,7 +5619,15 @@ class TangoClient:
5615
5619
  params["flat_lists"] = "true"
5616
5620
  return self._get(f"/api/psc/{code}/", params)
5617
5621
 
5618
- def get_psc_metrics(self, code: str, months: int, period_grouping: str) -> dict[str, Any]:
5622
+ def get_psc_metrics(
5623
+ self,
5624
+ code: str,
5625
+ months: int,
5626
+ period_grouping: str,
5627
+ group_by: MetricsGroupBy | list[MetricsGroupBy] | None = None,
5628
+ fiscal_year: bool = False,
5629
+ rolling: bool = False,
5630
+ ) -> dict[str, Any]:
5619
5631
  """Get rolling PSC metrics (`/api/psc/{code}/metrics/{months}/{period_grouping}/`).
5620
5632
 
5621
5633
  Args:
@@ -5623,10 +5635,32 @@ class TangoClient:
5623
5635
  months: Window size in months (e.g. 6, 12, 24, 36).
5624
5636
  period_grouping: ``"month"``, ``"quarter"``, or ``"year"`` (the
5625
5637
  values the API accepts on the path segment).
5638
+ group_by: Split each period by the funding ``"agency"`` (an L2 short code such as ``"2100"``) or ``"department"`` (the CGAC code as an int, e.g. ``97`` for DoD), or both.
5639
+ fiscal_year: Bucket by federal fiscal year (October-September) instead of calendar periods.
5640
+ rolling: Add rolling averages. The API skips them, with a ``warning``, when ``group_by`` is set.
5626
5641
  """
5627
5642
  if not code:
5628
5643
  raise TangoValidationError("PSC code is required")
5629
- return self._get(f"/api/psc/{code}/metrics/{months}/{period_grouping}/")
5644
+ return self._get(
5645
+ f"/api/psc/{code}/metrics/{months}/{period_grouping}/",
5646
+ self._metrics_params(group_by, fiscal_year, rolling),
5647
+ )
5648
+
5649
+ @staticmethod
5650
+ def _metrics_params(
5651
+ group_by: MetricsGroupBy | list[MetricsGroupBy] | None,
5652
+ fiscal_year: bool,
5653
+ rolling: bool,
5654
+ ) -> dict[str, Any]:
5655
+ """Query params shared by the entity, NAICS and PSC metrics endpoints."""
5656
+ params: dict[str, Any] = {}
5657
+ if group_by:
5658
+ params["group_by"] = group_by if isinstance(group_by, str) else ",".join(group_by)
5659
+ if fiscal_year:
5660
+ params["fiscal_year"] = "true"
5661
+ if rolling:
5662
+ params["rolling"] = "true"
5663
+ return params
5630
5664
 
5631
5665
  def get_naics(
5632
5666
  self,
@@ -5647,11 +5681,25 @@ class TangoClient:
5647
5681
  params["flat_lists"] = "true"
5648
5682
  return self._get(f"/api/naics/{code}/", params)
5649
5683
 
5650
- def get_naics_metrics(self, code: str, months: int, period_grouping: str) -> dict[str, Any]:
5651
- """Get rolling NAICS metrics (`/api/naics/{code}/metrics/{months}/{period_grouping}/`)."""
5684
+ def get_naics_metrics(
5685
+ self,
5686
+ code: str,
5687
+ months: int,
5688
+ period_grouping: str,
5689
+ group_by: MetricsGroupBy | list[MetricsGroupBy] | None = None,
5690
+ fiscal_year: bool = False,
5691
+ rolling: bool = False,
5692
+ ) -> dict[str, Any]:
5693
+ """Get rolling NAICS metrics (`/api/naics/{code}/metrics/{months}/{period_grouping}/`).
5694
+
5695
+ Takes the same ``group_by``, ``fiscal_year`` and ``rolling`` options as :meth:`get_psc_metrics`.
5696
+ """
5652
5697
  if not code:
5653
5698
  raise TangoValidationError("NAICS code is required")
5654
- return self._get(f"/api/naics/{code}/metrics/{months}/{period_grouping}/")
5699
+ return self._get(
5700
+ f"/api/naics/{code}/metrics/{months}/{period_grouping}/",
5701
+ self._metrics_params(group_by, fiscal_year, rolling),
5702
+ )
5655
5703
 
5656
5704
  def get_business_type(
5657
5705
  self,
@@ -6026,13 +6074,26 @@ class TangoClient:
6026
6074
  meta=data.get("meta"),
6027
6075
  )
6028
6076
 
6029
- def get_entity_metrics(self, uei: str, months: int, period_grouping: str) -> dict[str, Any]:
6077
+ def get_entity_metrics(
6078
+ self,
6079
+ uei: str,
6080
+ months: int,
6081
+ period_grouping: str,
6082
+ group_by: MetricsGroupBy | list[MetricsGroupBy] | None = None,
6083
+ fiscal_year: bool = False,
6084
+ rolling: bool = False,
6085
+ ) -> dict[str, Any]:
6030
6086
  """Get rolling metrics for an entity
6031
6087
  (`/api/entities/{uei}/metrics/{months}/{period_grouping}/`).
6088
+
6089
+ Takes the same ``group_by``, ``fiscal_year`` and ``rolling`` options as :meth:`get_psc_metrics`.
6032
6090
  """
6033
6091
  if not uei:
6034
6092
  raise TangoValidationError("UEI is required")
6035
- return self._get(f"/api/entities/{uei}/metrics/{months}/{period_grouping}/")
6093
+ return self._get(
6094
+ f"/api/entities/{uei}/metrics/{months}/{period_grouping}/",
6095
+ self._metrics_params(group_by, fiscal_year, rolling),
6096
+ )
6036
6097
 
6037
6098
  # ============================================================================
6038
6099
  # IDV sub-resources
tango/exceptions.py CHANGED
@@ -1,7 +1,11 @@
1
1
  """Tango API exceptions"""
2
2
 
3
+ import re
3
4
  from typing import Any
4
5
 
6
+ # The API names the tripped rate in its 429 detail: "Rate limit exceeded for daily. Please try again in 16 hours, 58 minutes."
7
+ _RATE_NAME = re.compile(r"exceeded for ([\w-]+)", re.IGNORECASE)
8
+
5
9
 
6
10
  class TangoAPIError(Exception):
7
11
  """Base exception for Tango API errors"""
@@ -88,11 +92,13 @@ class TangoRateLimitError(TangoAPIError):
88
92
  d = self.detail
89
93
  if not d:
90
94
  return None
91
- lower = d.lower()
92
- if "burst" in lower or "minute" in lower:
93
- return "burst"
94
- if "daily" in lower or "day" in lower:
95
+ # Read the rate's name, not the whole sentence: a daily lockout's wait reads "16 hours, 58 minutes", which a match on "minute" took for burst.
96
+ match = _RATE_NAME.search(d)
97
+ text = match.group(1).lower() if match else d.lower()
98
+ if "daily" in text or re.search(r"\bday\b", text):
95
99
  return "daily"
100
+ if "burst" in text or "minute" in text:
101
+ return "burst"
96
102
  return None
97
103
 
98
104
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: tango-python
3
- Version: 1.18.1
3
+ Version: 1.19.0
4
4
  Summary: Python SDK for the Tango API
5
5
  Project-URL: Homepage, https://github.com/makegov/tango-python
6
6
  Project-URL: Documentation, https://docs.makegov.com/tango-python
@@ -1,6 +1,6 @@
1
- tango/__init__.py,sha256=VqhIYcfNI56Lx8iBaT3qGYKpjaHxLWPpTC-fIujYxQA,2739
2
- tango/client.py,sha256=awauFCq-gd-dCBAvDPeEBBgxwhuc_2ZzIhK-I7ki0U8,245711
3
- tango/exceptions.py,sha256=oPiLCccBoWLK5SjHkzP-oI6mUD9twEeKSvEuLV5h01I,4224
1
+ tango/__init__.py,sha256=EM6Jq21IN9wUKCXR7DsHJFuseDT8AqktTOS59qaXm9w,2739
2
+ tango/client.py,sha256=2zQb486CL9sClQOJlua1nI_AnRFT1zUjeEikv_t_Cc4,247811
3
+ tango/exceptions.py,sha256=TL2MrSbcy99ogrJugLiP1I5J5qNih770knISycDTx-g,4661
4
4
  tango/models.py,sha256=D9e8_AAAwVi1C4nOqdhWz-8ARBLPRsLGi8TDjucOMEY,72897
5
5
  tango/shapes/__init__.py,sha256=7ea1WU74jp4znhNw-gXruag6m6eyPZtbVgbDFmFUWro,1072
6
6
  tango/shapes/explicit_schemas.py,sha256=fwjHiFLOMpFyNLNM4M6kdK603s-t8gxHYqcSeL3jz7g,85674
@@ -16,8 +16,8 @@ tango/webhooks/cli.py,sha256=f_vQbJ2AeSQjKnQo7MxHFyUB2SAKcgHYmGQULS0isqQ,13858
16
16
  tango/webhooks/receiver.py,sha256=5yhsVhlLyoxmOCGvmbynWAIlDB2OaCPVf1H4GA1SxmU,9279
17
17
  tango/webhooks/signing.py,sha256=92Ee-0B6PR7ZkvY3Np3gzl88-mtfKkh-I7lxqCe2lGw,2374
18
18
  tango/webhooks/simulate.py,sha256=g2Osa0FYU5mJuon07T2aUCtmkUoTEzsY261tlp76fF0,3165
19
- tango_python-1.18.1.dist-info/METADATA,sha256=WL8McHrCjg-DuQ2-9FLyb9DyQZge8oSMnNXFCQhY_Nw,22824
20
- tango_python-1.18.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
21
- tango_python-1.18.1.dist-info/entry_points.txt,sha256=kGLUbglUjuaAqEFvOZ1QuSW0vWb6VeSpCIFKaOFkKoQ,50
22
- tango_python-1.18.1.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
23
- tango_python-1.18.1.dist-info/RECORD,,
19
+ tango_python-1.19.0.dist-info/METADATA,sha256=IBq0ckYVquUbejqTc115sets4upLdf9aZOIKtJWEYBo,22824
20
+ tango_python-1.19.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
21
+ tango_python-1.19.0.dist-info/entry_points.txt,sha256=kGLUbglUjuaAqEFvOZ1QuSW0vWb6VeSpCIFKaOFkKoQ,50
22
+ tango_python-1.19.0.dist-info/licenses/LICENSE,sha256=j2kYVHMwTkoGn3ZNScnrdIueG0k2XzB_LCPFoyBc2wk,1064
23
+ tango_python-1.19.0.dist-info/RECORD,,