05 — Tariff packs¶
Concept: a tariff pack is a folder of data that gives a contract its meaning: which fields a contract has and their allowed values, the checks that apply (errors or warnings), how the contract's off-peak hours become its network schedule, which supply options exist, and the dated rates. Nothing about a country or an offer is in the code: the French household pack is one pack among others, and anyone can write their own, from scratch or by extending another. Free tags (country, sector, purpose, …) describe a pack.
You will learn: what the built-in pack holds, how a contract is checked, how to write a research pack that changes one tax, and a pack for somewhere else entirely.
The built-in pack¶
import tempfile
import warnings
from datetime import date
from pathlib import Path
from myenergy.adapters.packs.toml_files import TomlPackSource, builtin_packs
from myenergy.domain.errors import ContractError
from myenergy.domain.packs import tagged
from myenergy.domain.rates import RateKey
from myenergy.domain.schedules import TimeWindows
packs = TomlPackSource([builtin_packs()])
for pack in packs.catalog():
print(pack.name, "-", pack.title)
print(" tags:", dict(pack.tags))
france = packs.pack("fr-household")
for field in france.fields:
default = f" (default {field.default})" if field.default else ""
print(f"{field.name:14} {', '.join(field.values)}{default}")
fr-household - France, households on low voltage up to 36 kVA (TURPE, CTA, excise, VAT)
tags: {'country': 'FR', 'sector': 'household', 'source': 'official', 'voltage': 'BT<=36kVA'}
segment BT<=36kVA (default BT<=36kVA)
fta CU, CU4, MU4, MUDT, LU, CU4-AC, MU4-AC
status consumer, collective, producer-injection (default consumer)
contract supplier, user (default supplier)
meter metered, unmetered (default metered)
supply_option base, hp-hc
Its pack.toml is plain TOML; the checks are data too:
print((builtin_packs() / "fr-household" / "pack.toml").read_text().split("[[checks]]", 1)[1][:600])
level = "error"
message = "CU4-AC and MU4-AC are only open to collective self-consumption participants"
when = { fta = ["CU4-AC", "MU4-AC"] }
require = { status = ["collective"] }
[[checks]]
level = "warning"
message = "TURPE gives a BT <= 36 kVA connection 8 off-peak hours a day; priced as given"
when = { fta = ["CU4", "MU4", "MUDT", "CU4-AC", "MU4-AC"] }
off_peak_hours = 8
[[checks]]
level = "warning"
message = "the peak/off-peak option normally follows the connection's 8 off-peak hours; priced as given"
when = { supply_option = ["hp-hc"] }
off_peak_hours = 8
A contract under a pack¶
pack.contract(...) fills defaults and applies every check. Fields double as rate selectors: fta = "CU4" selects the rates keyed fta=CU4.
contract = france.contract(subscribed_kva=6, fields={"fta": "CU4", "supply_option": "hp-hc"},
off_peak=TimeWindows.parse("22:00-06:00"))
print(contract.selectors)
for fields in ({"fta": "CU5"}, {"fta": "CU4-AC"}, {"colour": "red"}):
try:
france.contract(subscribed_kva=6, fields={"fta": "CU4", "supply_option": "base", **fields},
off_peak=TimeWindows.parse("22:00-06:00"))
except ContractError as error:
print("error:", error)
{'contract': 'supplier', 'fta': 'CU4', 'meter': 'metered', 'segment': 'BT<=36kVA', 'status': 'consumer', 'supply_option': 'hp-hc'}
error: pack 'fr-household': fta = 'CU5' is not one of ['CU', 'CU4', 'MU4', 'MUDT', 'LU', 'CU4-AC', 'MU4-AC']
error: pack 'fr-household': CU4-AC and MU4-AC are only open to collective self-consumption participants
error: pack 'fr-household' has no field ['colour'] (fields: segment, fta, status, contract, meter, supply_option)
Rates live with the pack¶
rates = packs.rates("fr-household")
print(len(rates.catalog()), "rate keys")
for version in rates.history(RateKey.of("vat", base="subscription")):
print(f"VAT on subscriptions from {version.valid_from}: {version.value:.1%}")
56 rate keys VAT on subscriptions from 2024-08-01: 5.5% VAT on subscriptions from 2025-08-01: 20.0%
Your own pack: a research scenario¶
Suppose a study asks what households would pay with VAT at 5.5 % on everything. The pack extends fr-household and redefines only the vat rates; it also drops the 8-hour warnings. Everything else (fields, network schedule, TURPE, CTA, excise) is inherited. Packs are found in folders listed in the settings (MYENERGY_PACK_ROOTS); here, a temporary folder.
root = Path(tempfile.mkdtemp())
study = root / "vat-study"
(study / "rates").mkdir(parents=True)
(study / "pack.toml").write_text('''
checks = []
[pack]
name = "vat-study"
title = "French households, VAT at 5.5 % on everything"
extends = "fr-household"
tags = { purpose = "research", source = "hypothetical" }
''')
(study / "rates" / "vat.toml").write_text('''
[sources.study]
title = "Research hypothesis"
url = "https://example.org/vat-study"
[[rates]]
component = "vat"
selectors = { base = "subscription" }
unit = "%"
[[rates.versions]]
from = 2024-08-01
source = "study"
status = "official"
value = 5.5
[[rates]]
component = "vat"
selectors = { base = "consumption" }
unit = "%"
[[rates.versions]]
from = 2024-08-01
source = "study"
status = "official"
value = 5.5
''')
packs = TomlPackSource([builtin_packs(), root])
print([pack.name for pack in tagged(packs.catalog(), country="FR")])
print([pack.name for pack in tagged(packs.catalog(), purpose="research")])
['fr-household', 'vat-study'] ['vat-study']
day = date(2026, 1, 1)
for name in ("fr-household", "vat-study"):
rates = packs.rates(name)
vat = rates.history(RateKey.of("vat", base="consumption")).on(day).value
cta = rates.history(RateKey.of("cta", network="distribution")).on(day).value
print(f"{name:13} VAT on energy {vat:.1%}, CTA {cta:.2%}")
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
packs.pack("vat-study").contract(subscribed_kva=6, fields={"fta": "CU4", "supply_option": "base"},
off_peak=TimeWindows.parse("22:00-05:00"))
print(len(caught), "warnings: the study dropped the checks")
fr-household VAT on energy 20.0%, CTA 21.93% vat-study VAT on energy 5.5%, CTA 21.93% 0 warnings: the study dropped the checks
A pack for somewhere else¶
A made-up place with one contract field, a year-round day/night network tariff and two supply options. No line of code changes; its rates would go in its rates/ folder.
elsewhere = root / "elsewhere"
elsewhere.mkdir()
(elsewhere / "pack.toml").write_text('''
[pack]
name = "elsewhere"
title = "A made-up country with day and night network prices"
max_kva = 50
tags = { country = "XX", sector = "household", purpose = "example" }
[fields.plan]
values = ["standard", "ev"]
default = "standard"
[network]
peak = "day"
off_peak = "night"
[supply]
field = "plan"
options.standard = { single = "flat" }
options.ev = { peak = "normal", off_peak = "charging" }
''')
packs = TomlPackSource([builtin_packs(), root])
pack = packs.pack("elsewhere")
home = pack.contract(subscribed_kva=12, fields={"plan": "ev"}, off_peak=TimeWindows.parse("23:00-07:00"))
print("network:", pack.network_schedule(home).periods)
print("supply: ", pack.supply_schedule(home).periods)
print([p.name for p in tagged(packs.catalog(), sector="household")])
network: ('night', 'day')
supply: ('charging', 'normal')
['elsewhere', 'fr-household', 'vat-study']
Next: the anatomy of a bill (M2) prices a contract's network and supply periods from its pack's rates and the user's price book.