Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions infra_cost_model/pricing/sources/infracost.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ def sync_to_cache(self, cache, usage_metric: str, region: str,
return self._upsert_region_pair_representative(
cache, prices, usage_metric, region, unit_match, descriptor, now)

if descriptor.get("regionless_usagetype"):
return self._upsert_regionless_usagetype(
cache, prices, usage_metric, region, unit_match, descriptor, now)

count = 0
for p in prices:
if unit_match and p.get("unit") != unit_match:
Expand Down Expand Up @@ -331,6 +335,44 @@ def _upsert_region_pair_representative(self, cache, prices, usage_metric,
))
return 1

def _upsert_regionless_usagetype(self, cache, prices, usage_metric,
region, unit_match, descriptor, now) -> int:
"""Store a globally-catalogued, single-usagetype metric under the region.

Unlike inter-region transfer, internet egress and inter-AZ transfer have
ONE usagetype per source region (not per region pair), but still live in
the global (region="") ``AWSDataTransfer`` catalogue. This keeps every row
for the region's usagetype — preserving tiers (internet egress is tiered
$0.09 / $0.085 / $0.07 / $0.05) — and stores them under the sync ``region``.

us-east-1 data-transfer usagetypes are unprefixed (an AWS legacy quirk);
every other region prepends its short prefix (e.g. ``USW1-``).

Returns the number of rows upserted.
"""
from infra_cost_model.pricing.cache import Price

base = descriptor["usagetype_base"]
prefix = _region_usagetype_prefix(region)
target = base if region == "us-east-1" else f"{prefix}-{base}"

count = 0
for p in prices:
if unit_match and p.get("unit") != unit_match:
continue
if (p.get("attributes") or {}).get("usagetype") != target:
continue
cache.upsert(Price(
vendor=p["vendor"], service=p["service"], region=region,
product_family=p.get("product_family"), attributes=p.get("attributes", {}),
usage_metric=usage_metric, unit=p["unit"], price_usd=p["price_usd"],
start_usage_amount=p["start_usage_amount"],
end_usage_amount=p["end_usage_amount"],
source="infracost", effective_date=now, fetched_at=now,
))
count += 1
return count


# Map each catalog usage_metric to the Infracost product query that prices it.
# Validated against the live Cloud Pricing API; extend per service as needed.
Expand Down Expand Up @@ -494,6 +536,27 @@ def _upsert_region_pair_representative(self, cache, prices, usage_metric,
"region_pair_source": True,
"usagetype_suffix": "-AWS-Out-Bytes",
},
# Internet egress (#211): transferType "AWS Outbound", one usagetype per
# source region (us-east-1 is the unprefixed "DataTransfer-Out-Bytes"),
# tiered $0.09 / $0.085 / $0.07 / $0.05 across the 10/50/150 TB breakpoints.
"DataTransfer-Internet-Out-GB": {
"service": "AWSDataTransfer",
"query_region": "",
"attribute_filters": [{"key": "transferType", "value": "AWS Outbound"}],
"unit": "GB",
"regionless_usagetype": True,
"usagetype_base": "DataTransfer-Out-Bytes",
},
# Regional inter-AZ transfer (#211): transferType "IntraRegion", flat $0.01/GB,
# usagetype "<region>-DataTransfer-Regional-Bytes" (bare for us-east-1).
"DataTransfer-InterAZ-GB": {
"service": "AWSDataTransfer",
"query_region": "",
"attribute_filters": [{"key": "transferType", "value": "IntraRegion"}],
"unit": "GB",
"regionless_usagetype": True,
"usagetype_base": "DataTransfer-Regional-Bytes",
},
}


Expand Down
107 changes: 107 additions & 0 deletions tests/test_infracost_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,110 @@ def test_data_transfer_unknown_region_upserts_nothing(monkeypatch):
cache, "DataTransfer-InterRegion-GB", "moon-base-1")
assert n == 0
cache.upsert.assert_not_called()


# --- Regionless single-usagetype: internet egress (tiered) + inter-AZ ----------

def _dt_tiered_product(usagetype, tiers, transfer_type="AWS Outbound", unit="GB"):
"""tiers: list of (usd, start, end) — one price row each (preserves tiers)."""
return {
"productFamily": "",
"attributes": [
{"key": "usagetype", "value": usagetype},
{"key": "transferType", "value": transfer_type},
],
"prices": [{"USD": str(u), "unit": unit,
"startUsageAmount": str(s),
"endUsageAmount": (None if e is None else str(e))}
for (u, s, e) in tiers],
}


def test_internet_egress_preserves_tiers_us_east_1(monkeypatch):
"""us-east-1 internet egress uses the unprefixed usagetype and keeps all tiers."""
_set_creds(monkeypatch)
products = [
_dt_tiered_product("DataTransfer-Out-Bytes", [
(0.09, 0, 10240), (0.085, 10240, 51200),
(0.07, 51200, 153600), (0.05, 153600, None),
]),
_dt_tiered_product("USW1-DataTransfer-Out-Bytes", [(0.09, 0, None)]), # other region
_dt_product("USE1-USW2-AWS-Out-Bytes", 0.02), # wrong metric
]
upserted = []
cache = MagicMock()
cache.upsert.side_effect = lambda p: upserted.append(p)
with patch.object(ic.requests, "post", return_value=_graphql_response(products)):
n = ic.InfracostClient().sync_to_cache(cache, "DataTransfer-Internet-Out-GB", "us-east-1")
assert n == 4 # four tiers of the unprefixed us-east-1 usagetype only
assert all(p.region == "us-east-1" and p.usage_metric == "DataTransfer-Internet-Out-GB"
for p in upserted)
assert sorted(p.price_usd for p in upserted) == [0.05, 0.07, 0.085, 0.09]
first = next(p for p in upserted if p.start_usage_amount == 0)
assert first.price_usd == pytest.approx(0.09) and first.end_usage_amount == pytest.approx(10240)


def test_internet_egress_prefixes_non_us_east_1(monkeypatch):
"""A non-us-east-1 region selects the prefixed usagetype, not the bare one."""
_set_creds(monkeypatch)
products = [
_dt_tiered_product("DataTransfer-Out-Bytes", [(0.09, 0, None)]), # us-east-1 bare
_dt_tiered_product("USW1-DataTransfer-Out-Bytes", [(0.09, 0, None)]), # us-west-1
]
upserted = []
cache = MagicMock()
cache.upsert.side_effect = lambda p: upserted.append(p)
with patch.object(ic.requests, "post", return_value=_graphql_response(products)):
n = ic.InfracostClient().sync_to_cache(cache, "DataTransfer-Internet-Out-GB", "us-west-1")
assert n == 1
assert upserted[0].region == "us-west-1"
assert upserted[0].attributes["usagetype"] == "USW1-DataTransfer-Out-Bytes"


def test_inter_az_flat_rate_us_east_1(monkeypatch):
"""Inter-AZ is a single flat $0.01/GB row under the unprefixed usagetype."""
_set_creds(monkeypatch)
products = [
_dt_product("DataTransfer-Regional-Bytes", 0.01, transfer_type="IntraRegion"),
_dt_product("APS4-DataTransfer-Regional-Bytes", 0.01, transfer_type="IntraRegion"),
]
upserted = []
cache = MagicMock()
cache.upsert.side_effect = lambda p: upserted.append(p)
with patch.object(ic.requests, "post", return_value=_graphql_response(products)):
n = ic.InfracostClient().sync_to_cache(cache, "DataTransfer-InterAZ-GB", "us-east-1")
assert n == 1
assert upserted[0].price_usd == pytest.approx(0.01)
assert upserted[0].region == "us-east-1"


def test_data_transfer_internet_and_interaz_descriptors_present():
d1 = ic.METRIC_DESCRIPTORS["DataTransfer-Internet-Out-GB"]
assert d1["query_region"] == "" and d1["regionless_usagetype"] is True
assert d1["usagetype_base"] == "DataTransfer-Out-Bytes"
d2 = ic.METRIC_DESCRIPTORS["DataTransfer-InterAZ-GB"]
assert d2["regionless_usagetype"] is True
assert d2["usagetype_base"] == "DataTransfer-Regional-Bytes"


def test_regionless_usagetype_queries_global_region(monkeypatch):
"""Internet-egress / inter-AZ descriptors must issue the global (region="") query."""
_set_creds(monkeypatch)
cache = MagicMock()
prod = _dt_tiered_product("DataTransfer-Out-Bytes", [(0.09, 0, None)])
with patch.object(ic.requests, "post", return_value=_graphql_response([prod])) as post:
ic.InfracostClient().sync_to_cache(cache, "DataTransfer-Internet-Out-GB", "us-east-1")
assert post.call_args.kwargs["json"]["variables"]["region"] == ""


def test_regionless_usagetype_unknown_region_upserts_nothing(monkeypatch):
"""An unmapped region resolves to the 'REGION_PREFIX' fallback target, which
matches no usagetype → nothing stored (falls back to seed)."""
_set_creds(monkeypatch)
products = [_dt_tiered_product("DataTransfer-Out-Bytes", [(0.09, 0, None)])]
cache = MagicMock()
with patch.object(ic.requests, "post", return_value=_graphql_response(products)):
n = ic.InfracostClient().sync_to_cache(
cache, "DataTransfer-Internet-Out-GB", "moon-base-1")
assert n == 0
cache.upsert.assert_not_called()
Loading