commodutil 3.9.0__tar.gz → 3.10.0__tar.gz
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.
- {commodutil-3.9.0 → commodutil-3.10.0}/PKG-INFO +1 -1
- commodutil-3.10.0/commodutil/__init__.py +80 -0
- commodutil-3.10.0/commodutil/standards/analysis_types.py +59 -0
- commodutil-3.10.0/commodutil/standards/commodities.py +71 -0
- commodutil-3.10.0/commodutil/standards/commodity_groups.py +40 -0
- commodutil-3.10.0/commodutil/standards/units.py +64 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/PKG-INFO +1 -1
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/SOURCES.txt +6 -0
- commodutil-3.10.0/tests/test_standards_analysis_types.py +60 -0
- commodutil-3.10.0/tests/test_standards_commodities.py +71 -0
- commodutil-3.10.0/tests/test_standards_commodity_groups.py +93 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_standards_regions.py +65 -11
- commodutil-3.10.0/tests/test_standards_units.py +86 -0
- commodutil-3.9.0/commodutil/__init__.py +0 -84
- commodutil-3.9.0/commodutil/standards/units.py +0 -30
- commodutil-3.9.0/tests/test_standards_units.py +0 -31
- {commodutil-3.9.0 → commodutil-3.10.0}/.coveragerc +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/.github/workflows/1_tests.yml +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/.github/workflows/2_coverage.yml +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/.github/workflows/3_linting.yml +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/.github/workflows/4_release.yml +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/.gitignore +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/.pypirc +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/azure-build-pipelines.yml +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/arb.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/convfactors.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/dates.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/__init__.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/calendar.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/continuous.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/fly.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/quarterly.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/spreads.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/structure.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/util.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forwards.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/pandasutil.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/standards/__init__.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/standards/currency.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/standards/regions.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/stats.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/transforms.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/dependency_links.txt +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/requires.txt +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/top_level.txt +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/pyproject.toml +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/requirements-test.txt +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/requirements.txt +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/requirements_dev.txt +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/scripts/rbw_structure_scan.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/setup.cfg +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/__init__.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/conftest.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/__init__.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/conftest.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_calendar.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_continuous.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_fly.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_quarterly.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_spreads.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_structure.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_util.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_arb.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_cl.csv +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_conv.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_dates.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_forwards.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_pandasutils.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_price_conv.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_standards_currency.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_stats.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_transforms.py +0 -0
- {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_weekly.csv +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""commodutil: Commodity market standards backbone.
|
|
2
|
+
|
|
3
|
+
Public API for unit conversions, forward curve transforms, date utilities,
|
|
4
|
+
and pandas helpers. Symbols are loaded lazily on first access via PEP 562
|
|
5
|
+
`__getattr__` so `import commodutil` stays cheap (~10ms vs. ~3s if the
|
|
6
|
+
facade eagerly imported convfactors and its pint registry).
|
|
7
|
+
|
|
8
|
+
Cheap paths:
|
|
9
|
+
import commodutil # near-instant
|
|
10
|
+
from commodutil.standards.regions import ... # stdlib only
|
|
11
|
+
from commodutil.standards.currency import ... # stdlib only
|
|
12
|
+
|
|
13
|
+
Heavier paths (lazy-load their sub-module on first attribute access):
|
|
14
|
+
from commodutil import convert_price # triggers convfactors
|
|
15
|
+
from commodutil import curyear # triggers dates
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
# Map exported name -> dotted submodule path. Single source of truth for the
|
|
21
|
+
# public facade. Keys in this dict are what `from commodutil import X` will
|
|
22
|
+
# resolve via __getattr__ below.
|
|
23
|
+
_LAZY_EXPORTS = {
|
|
24
|
+
# convfactors (heaviest -- pint registry + Commodity dataclass init)
|
|
25
|
+
"ALIASES": "commodutil.convfactors",
|
|
26
|
+
"COMMODITIES": "commodutil.convfactors",
|
|
27
|
+
"Commodity": "commodutil.convfactors",
|
|
28
|
+
"FRACTIONAL_TO_MAJOR": "commodutil.convfactors",
|
|
29
|
+
"VALID_CURRENCY_TOKENS": "commodutil.convfactors",
|
|
30
|
+
"convert": "commodutil.convfactors",
|
|
31
|
+
"convert_price": "commodutil.convfactors",
|
|
32
|
+
"convfactor": "commodutil.convfactors",
|
|
33
|
+
"fractional_to_major": "commodutil.convfactors",
|
|
34
|
+
"is_fractional_currency": "commodutil.convfactors",
|
|
35
|
+
"list_commodities": "commodutil.convfactors",
|
|
36
|
+
"list_units": "commodutil.convfactors",
|
|
37
|
+
"split_currency_unit": "commodutil.convfactors",
|
|
38
|
+
# dates
|
|
39
|
+
"curmon": "commodutil.dates",
|
|
40
|
+
"curmonyear": "commodutil.dates",
|
|
41
|
+
"curmonyear_str": "commodutil.dates",
|
|
42
|
+
"curyear": "commodutil.dates",
|
|
43
|
+
"find_year": "commodutil.dates",
|
|
44
|
+
"last_day_of_prev_month": "commodutil.dates",
|
|
45
|
+
"nextyear": "commodutil.dates",
|
|
46
|
+
"prevmon": "commodutil.dates",
|
|
47
|
+
"prevmon_str": "commodutil.dates",
|
|
48
|
+
"prevyear": "commodutil.dates",
|
|
49
|
+
"start_day_of_prev_month": "commodutil.dates",
|
|
50
|
+
# forwards
|
|
51
|
+
"all_spread_combinations": "commodutil.forwards",
|
|
52
|
+
"time_spreads": "commodutil.forwards",
|
|
53
|
+
# pandasutil
|
|
54
|
+
"fillna_downbet": "commodutil.pandasutil",
|
|
55
|
+
"mergets": "commodutil.pandasutil",
|
|
56
|
+
# transforms
|
|
57
|
+
"seasonalize": "commodutil.transforms",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
__all__ = sorted(_LAZY_EXPORTS.keys())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def __getattr__(name: str):
|
|
64
|
+
"""PEP 562 lazy attribute access — loads the source submodule on first
|
|
65
|
+
use of a facade-exported symbol, caches the resolved value back into
|
|
66
|
+
the module's globals so subsequent accesses skip the dispatch.
|
|
67
|
+
"""
|
|
68
|
+
module_path = _LAZY_EXPORTS.get(name)
|
|
69
|
+
if module_path is None:
|
|
70
|
+
raise AttributeError(f"module 'commodutil' has no attribute {name!r}")
|
|
71
|
+
import importlib
|
|
72
|
+
|
|
73
|
+
module = importlib.import_module(module_path)
|
|
74
|
+
value = getattr(module, name)
|
|
75
|
+
globals()[name] = value
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def __dir__():
|
|
80
|
+
return sorted(set(globals()) | set(_LAZY_EXPORTS))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""commodutil.standards.analysis_types: canonical analysis-type vocabulary.
|
|
2
|
+
|
|
3
|
+
Owns the inference function for classifying a curve as one of:
|
|
4
|
+
- 'crack': cracking spread
|
|
5
|
+
- 'arb': geographic arbitrage spread
|
|
6
|
+
- 'diff': differential / generic spread
|
|
7
|
+
- 'outright': none of the above (flat price)
|
|
8
|
+
|
|
9
|
+
Previously duplicated in curvemetadata.ice._infer_analysis_type (substring
|
|
10
|
+
matching) and curvemetadata.cme._infer_analysis_type (regex word-boundary).
|
|
11
|
+
Consolidated here using the stricter regex approach -- substring 'crack' in
|
|
12
|
+
'crackers' would have been a false positive in the ICE version.
|
|
13
|
+
|
|
14
|
+
Future subtypes (hi5, regrade, freight) are NOT included in this round --
|
|
15
|
+
they require validation against actual Curve.Definitions.Analysis values
|
|
16
|
+
in MetadataDB2, which is a separate piece of work.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
# Closed set of types recognised by infer_analysis_type. Callers downstream
|
|
25
|
+
# may treat the return as a canonical tag.
|
|
26
|
+
ANALYSIS_TYPES = ("crack", "arb", "diff", "outright")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def infer_analysis_type(
|
|
30
|
+
text: Optional[str],
|
|
31
|
+
extra_text: Optional[str] = None,
|
|
32
|
+
category: Optional[str] = None,
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Infer analysis type from product metadata.
|
|
35
|
+
|
|
36
|
+
Returns one of: 'crack', 'arb', 'diff', 'outright'. Defaults to
|
|
37
|
+
'outright' when no pattern matches.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
text: Primary product name or description.
|
|
41
|
+
extra_text: Optional secondary text (e.g. CME sub-category) --
|
|
42
|
+
appended to `text` for matching.
|
|
43
|
+
category: Optional explicit category field. If equals 'arb'
|
|
44
|
+
(case-insensitive), short-circuits to 'arb' regardless of
|
|
45
|
+
other text content (CME convention).
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(category, str) and category.strip().lower() == "arb":
|
|
48
|
+
return "arb"
|
|
49
|
+
combined = f"{text or ''} {extra_text or ''}".lower()
|
|
50
|
+
if re.search(r"\bcrack\b", combined):
|
|
51
|
+
return "crack"
|
|
52
|
+
if re.search(r"\barb\b", combined):
|
|
53
|
+
return "arb"
|
|
54
|
+
if re.search(r"\bdiff\b", combined) or re.search(r"\bspread\b", combined):
|
|
55
|
+
return "diff"
|
|
56
|
+
return "outright"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ["ANALYSIS_TYPES", "infer_analysis_type"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""commodutil.standards.commodities: canonical commodity vocabulary.
|
|
2
|
+
|
|
3
|
+
Owns:
|
|
4
|
+
- COMMODITY_KEYWORDS: ordered list of (display_name, group, [keywords])
|
|
5
|
+
used by free-text inference. Ordering matters — "Natural Gasoline"
|
|
6
|
+
must precede "Natural Gas" so the substring "natural gas" inside
|
|
7
|
+
"natural gasoline" doesn't win.
|
|
8
|
+
- COMMODITY_CONVERSION_MAP: display_name -> commodutil.convfactors.COMMODITIES
|
|
9
|
+
key, for downstream conversion routing.
|
|
10
|
+
|
|
11
|
+
Previously lived in curvemetadata.common_maps; relocated to eliminate
|
|
12
|
+
divergence risk between curvemetadata and commodutil's commodity lists.
|
|
13
|
+
curvemetadata.common_maps re-exports for backwards compatibility.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
COMMODITY_KEYWORDS = [
|
|
19
|
+
("Brent", "Crude Oil", ["brent"]),
|
|
20
|
+
("WTI", "Crude Oil", ["wti"]),
|
|
21
|
+
("Crude Oil", "Crude Oil", ["crude oil", "crude"]),
|
|
22
|
+
# NB: 'Natural Gasoline' MUST come before 'Natural Gas' — the substring
|
|
23
|
+
# "natural gas" is contained in "natural gasoline" and would otherwise win.
|
|
24
|
+
("Natural Gasoline", "NGL", ["natural gasoline"]),
|
|
25
|
+
("Natural Gas", "Natural Gas", ["natural gas", "nat gas", "natgas"]),
|
|
26
|
+
("Jet", "Refined Products", ["jet fuel", "jet"]),
|
|
27
|
+
("Diesel", "Refined Products", ["diesel", "ulsd", "gasoil", "heating oil"]),
|
|
28
|
+
("Gasoline", "Refined Products", ["gasoline", "rbob", "cbob", "mogas", "eurobob"]),
|
|
29
|
+
("Fuel Oil", "Refined Products", ["fuel oil", "hsfo", "lsfo", "marine fuel"]),
|
|
30
|
+
("Naphtha", "Refined Products", ["naphtha"]),
|
|
31
|
+
("Product Basket", "Refined Products", ["refined products", "product basket"]),
|
|
32
|
+
("VGO", "Refined Products", ["vgo"]),
|
|
33
|
+
("FAME", "Biofuel", ["fame"]),
|
|
34
|
+
("HVO", "Biofuel", ["hvo"]),
|
|
35
|
+
("Isobutane", "NGL", ["isobutane"]),
|
|
36
|
+
("Butane", "NGL", ["butane"]),
|
|
37
|
+
("Ethane", "NGL", ["ethane"]),
|
|
38
|
+
("Propane", "NGL", ["propane"]),
|
|
39
|
+
("NGL", "NGL", ["ngl"]),
|
|
40
|
+
("FFA", "Freight", ["freight", "ffa"]),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
COMMODITY_CONVERSION_MAP = {
|
|
44
|
+
"Crude Oil": "crude",
|
|
45
|
+
"Brent": "crude",
|
|
46
|
+
"WTI": "crude",
|
|
47
|
+
"Natural Gas": "natgas",
|
|
48
|
+
"Jet": "jet",
|
|
49
|
+
"Diesel": "diesel",
|
|
50
|
+
"Gasoline": "gasoline",
|
|
51
|
+
"Fuel Oil": "fuel_oil",
|
|
52
|
+
"Naphtha": "naphtha",
|
|
53
|
+
"Product Basket": "product_basket",
|
|
54
|
+
"VGO": "vgo",
|
|
55
|
+
"FAME": "fame",
|
|
56
|
+
"HVO": "hvo",
|
|
57
|
+
# NGL species — switched from the generic 'lpg' blend to first-class species
|
|
58
|
+
# in commodutil 2026-05 (each has its own density / HHV for $/gal<->$/MMBtu).
|
|
59
|
+
# Keep the generic 'NGL' bucket on 'lpg' as a safe blend default.
|
|
60
|
+
"Natural Gasoline": "natural_gasoline",
|
|
61
|
+
"Isobutane": "isobutane",
|
|
62
|
+
"Butane": "butane",
|
|
63
|
+
"Propane": "propane",
|
|
64
|
+
"NGL": "lpg",
|
|
65
|
+
"Ethane": "ethane",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"COMMODITY_KEYWORDS",
|
|
70
|
+
"COMMODITY_CONVERSION_MAP",
|
|
71
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""commodutil.standards.commodity_groups: canonical commodity group taxonomy.
|
|
2
|
+
|
|
3
|
+
Mirrors the CommodityGroup CHECK constraint on Curve.Definitions in
|
|
4
|
+
MetadataDB2 (see curvemetadata/sql/001_create_curvemetadata_tables.sql).
|
|
5
|
+
Single source of truth for the Python side -- curvemetadata.ice and
|
|
6
|
+
curvemetadata.cme currently hard-code these strings; they can adopt
|
|
7
|
+
this constant in a follow-up.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
# Ordered to match the SQL constraint order (preserves what consumers see
|
|
13
|
+
# when iterating). Treat as a closed set -- additions require a coordinated
|
|
14
|
+
# SQL migration.
|
|
15
|
+
COMMODITY_GROUPS = (
|
|
16
|
+
"Agriculture",
|
|
17
|
+
"Biofuel",
|
|
18
|
+
"Crude Oil",
|
|
19
|
+
"Freight",
|
|
20
|
+
"LNG",
|
|
21
|
+
"Natural Gas",
|
|
22
|
+
"NGL",
|
|
23
|
+
"Petrochemical",
|
|
24
|
+
"Refined Products",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Set for O(1) membership checks
|
|
28
|
+
VALID_COMMODITY_GROUPS = frozenset(COMMODITY_GROUPS)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_valid_commodity_group(value: str) -> bool:
|
|
32
|
+
"""Return True if `value` is a recognised commodity group string."""
|
|
33
|
+
return value in VALID_COMMODITY_GROUPS
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"COMMODITY_GROUPS",
|
|
38
|
+
"VALID_COMMODITY_GROUPS",
|
|
39
|
+
"is_valid_commodity_group",
|
|
40
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""commodutil.standards.units: canonical unit vocabulary.
|
|
2
|
+
|
|
3
|
+
Owns:
|
|
4
|
+
- UNIT_MAP: alias -> canonical unit map for normalising free-form unit
|
|
5
|
+
strings from vendor contract specs ("barrel", "Barrels", "BBL" -> "bbl").
|
|
6
|
+
- default_unit_for_commodity(): returns the canonical quoted unit for a
|
|
7
|
+
commodity (volume basis).
|
|
8
|
+
|
|
9
|
+
Pure vocab -- no pint, no pandas. The pint registry in
|
|
10
|
+
commodutil.convfactors handles unit algebra; this module handles the
|
|
11
|
+
string-normalisation layer that runs BEFORE algebra.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---- Alias -> canonical normalisation ----
|
|
20
|
+
|
|
21
|
+
# Maps lowercase aliases (singular / plural / abbreviated forms) to the
|
|
22
|
+
# canonical unit token used by downstream code. Used by vendor-spec
|
|
23
|
+
# parsers (e.g. curvemetadata.ice_util.parse_unit). Keys are matched
|
|
24
|
+
# case-insensitively at call time -- callers should lowercase input.
|
|
25
|
+
UNIT_MAP = {
|
|
26
|
+
"barrel": "bbl",
|
|
27
|
+
"barrels": "bbl",
|
|
28
|
+
"bbl": "bbl",
|
|
29
|
+
"bbls": "bbl",
|
|
30
|
+
"gallon": "gal",
|
|
31
|
+
"gallons": "gal",
|
|
32
|
+
"gal": "gal",
|
|
33
|
+
"metric ton": "mt",
|
|
34
|
+
"metric tons": "mt",
|
|
35
|
+
"metric tonne": "mt",
|
|
36
|
+
"metric tonnes": "mt",
|
|
37
|
+
"tonne": "mt",
|
|
38
|
+
"tonnes": "mt",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---- Default unit per commodity ----
|
|
43
|
+
|
|
44
|
+
_DEFAULT_UNIT = {
|
|
45
|
+
"natgas": "mmbtu",
|
|
46
|
+
"natural_gas": "mmbtu",
|
|
47
|
+
"gasoline": "gal",
|
|
48
|
+
"diesel": "gal",
|
|
49
|
+
"jet": "gal",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def default_unit_for_commodity(commodity: Optional[str]) -> str:
|
|
54
|
+
"""Return the canonical quoted unit for a commodity (volume basis).
|
|
55
|
+
|
|
56
|
+
Falls back to 'bbl' for any commodity not in the explicit map (covers
|
|
57
|
+
crude / fuel oil / naphtha / VGO / NGL species etc.).
|
|
58
|
+
"""
|
|
59
|
+
if not commodity:
|
|
60
|
+
return "bbl"
|
|
61
|
+
return _DEFAULT_UNIT.get(str(commodity).lower(), "bbl")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = ["UNIT_MAP", "default_unit_for_commodity"]
|
|
@@ -32,6 +32,9 @@ commodutil/forward/spreads.py
|
|
|
32
32
|
commodutil/forward/structure.py
|
|
33
33
|
commodutil/forward/util.py
|
|
34
34
|
commodutil/standards/__init__.py
|
|
35
|
+
commodutil/standards/analysis_types.py
|
|
36
|
+
commodutil/standards/commodities.py
|
|
37
|
+
commodutil/standards/commodity_groups.py
|
|
35
38
|
commodutil/standards/currency.py
|
|
36
39
|
commodutil/standards/regions.py
|
|
37
40
|
commodutil/standards/units.py
|
|
@@ -45,6 +48,9 @@ tests/test_dates.py
|
|
|
45
48
|
tests/test_forwards.py
|
|
46
49
|
tests/test_pandasutils.py
|
|
47
50
|
tests/test_price_conv.py
|
|
51
|
+
tests/test_standards_analysis_types.py
|
|
52
|
+
tests/test_standards_commodities.py
|
|
53
|
+
tests/test_standards_commodity_groups.py
|
|
48
54
|
tests/test_standards_currency.py
|
|
49
55
|
tests/test_standards_regions.py
|
|
50
56
|
tests/test_standards_units.py
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Tests for commodutil.standards.analysis_types."""
|
|
2
|
+
|
|
3
|
+
from commodutil.standards.analysis_types import ANALYSIS_TYPES, infer_analysis_type
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_recognises_crack():
|
|
7
|
+
assert infer_analysis_type("Brent-Diesel Crack") == "crack"
|
|
8
|
+
assert infer_analysis_type("WTI Crack Spread") == "crack"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_recognises_arb():
|
|
12
|
+
assert infer_analysis_type("Singapore-LA Gasoline Arb") == "arb"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_recognises_diff():
|
|
16
|
+
assert infer_analysis_type("Brent-WTI Diff") == "diff"
|
|
17
|
+
assert infer_analysis_type("Gasoil-Heating Oil Spread") == "diff"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_default_outright():
|
|
21
|
+
assert infer_analysis_type("Brent Front Month") == "outright"
|
|
22
|
+
assert infer_analysis_type("WTI Cushing") == "outright"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_empty_and_none():
|
|
26
|
+
assert infer_analysis_type("") == "outright"
|
|
27
|
+
assert infer_analysis_type(None) == "outright"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_word_boundary_prevents_false_match():
|
|
31
|
+
"""Substring matching (old ICE behavior) would have wrongly returned
|
|
32
|
+
'crack' for 'crackers' or 'arb' for 'arbitrary'. Word boundary fixes."""
|
|
33
|
+
assert infer_analysis_type("crackers party") == "outright"
|
|
34
|
+
assert infer_analysis_type("arbitrary value") == "outright"
|
|
35
|
+
assert infer_analysis_type("trackers") == "outright"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_category_short_circuit():
|
|
39
|
+
# CME convention: category="arb" wins regardless of text
|
|
40
|
+
assert infer_analysis_type("WTI Front Month", category="arb") == "arb"
|
|
41
|
+
assert infer_analysis_type("WTI Front Month", category="ARB") == "arb"
|
|
42
|
+
# Other category values don't override
|
|
43
|
+
assert infer_analysis_type("WTI Brent Diff", category="energy") == "diff"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_extra_text_appended():
|
|
47
|
+
# ICE-style single-arg call still works (extra_text defaults to None)
|
|
48
|
+
assert infer_analysis_type("WTI Crack Spread") == "crack"
|
|
49
|
+
# CME-style multi-arg: sub_category contributes
|
|
50
|
+
assert infer_analysis_type("WTI Brent", "crack") == "crack"
|
|
51
|
+
assert infer_analysis_type("WTI Brent", None, None) == "outright"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_crack_priority_over_arb_diff():
|
|
55
|
+
"""Order matters: crack short-circuits before arb/diff checks."""
|
|
56
|
+
assert infer_analysis_type("Gasoline Crack Arb") == "crack"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_analysis_types_constant():
|
|
60
|
+
assert ANALYSIS_TYPES == ("crack", "arb", "diff", "outright")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Tests for commodutil.standards.commodities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from commodutil.standards.commodities import (
|
|
8
|
+
COMMODITY_CONVERSION_MAP,
|
|
9
|
+
COMMODITY_KEYWORDS,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_commodity_keywords_sanity_length():
|
|
14
|
+
assert len(COMMODITY_KEYWORDS) >= 20
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_commodity_keywords_entry_shape():
|
|
18
|
+
for entry in COMMODITY_KEYWORDS:
|
|
19
|
+
assert isinstance(entry, tuple)
|
|
20
|
+
assert len(entry) == 3
|
|
21
|
+
display, group, kws = entry
|
|
22
|
+
assert isinstance(display, str)
|
|
23
|
+
assert isinstance(group, str)
|
|
24
|
+
assert isinstance(kws, list)
|
|
25
|
+
assert all(isinstance(k, str) for k in kws)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_natural_gasoline_before_natural_gas():
|
|
29
|
+
displays = [d for d, _, _ in COMMODITY_KEYWORDS]
|
|
30
|
+
assert "Natural Gasoline" in displays
|
|
31
|
+
assert "Natural Gas" in displays
|
|
32
|
+
assert displays.index("Natural Gasoline") < displays.index("Natural Gas")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_brent_entry_present():
|
|
36
|
+
assert ("Brent", "Crude Oil", ["brent"]) in COMMODITY_KEYWORDS
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_conversion_map_brent_is_crude():
|
|
40
|
+
assert COMMODITY_CONVERSION_MAP["Brent"] == "crude"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_conversion_map_natural_gas_is_natgas():
|
|
44
|
+
assert COMMODITY_CONVERSION_MAP["Natural Gas"] == "natgas"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_conversion_map_ngl_default_lpg_blend():
|
|
48
|
+
assert COMMODITY_CONVERSION_MAP["NGL"] == "lpg"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_conversion_map_no_orphan_entries():
|
|
52
|
+
"""Every commodity in COMMODITY_CONVERSION_MAP appears as a display_name
|
|
53
|
+
in COMMODITY_KEYWORDS — consistency check."""
|
|
54
|
+
displays = {d for d, _, _ in COMMODITY_KEYWORDS}
|
|
55
|
+
orphans = set(COMMODITY_CONVERSION_MAP.keys()) - displays
|
|
56
|
+
assert not orphans, f"Orphan conversion map entries: {orphans}"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_parity_with_curvemetadata():
|
|
60
|
+
"""Verify COMMODITY_KEYWORDS / COMMODITY_CONVERSION_MAP are identical to
|
|
61
|
+
the curvemetadata copies — guards against divergence during the migration."""
|
|
62
|
+
try:
|
|
63
|
+
from curvemetadata.common_maps import (
|
|
64
|
+
COMMODITY_CONVERSION_MAP as cm_MAP,
|
|
65
|
+
COMMODITY_KEYWORDS as cm_KW,
|
|
66
|
+
)
|
|
67
|
+
except ImportError:
|
|
68
|
+
pytest.skip("curvemetadata not available")
|
|
69
|
+
|
|
70
|
+
assert COMMODITY_KEYWORDS == cm_KW
|
|
71
|
+
assert COMMODITY_CONVERSION_MAP == cm_MAP
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Tests for commodutil.standards.commodity_groups."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from commodutil.standards.commodity_groups import (
|
|
8
|
+
COMMODITY_GROUPS,
|
|
9
|
+
VALID_COMMODITY_GROUPS,
|
|
10
|
+
is_valid_commodity_group,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
EXPECTED_GROUPS = (
|
|
15
|
+
"Agriculture",
|
|
16
|
+
"Biofuel",
|
|
17
|
+
"Crude Oil",
|
|
18
|
+
"Freight",
|
|
19
|
+
"LNG",
|
|
20
|
+
"Natural Gas",
|
|
21
|
+
"NGL",
|
|
22
|
+
"Petrochemical",
|
|
23
|
+
"Refined Products",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_count_is_nine():
|
|
28
|
+
assert len(COMMODITY_GROUPS) == 9
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_exact_tuple_equality_preserves_sql_order():
|
|
32
|
+
# COMMODITY_GROUPS docstring promises SQL constraint order is preserved.
|
|
33
|
+
# A future maintainer reordering the tuple would silently change what
|
|
34
|
+
# downstream consumers see when iterating.
|
|
35
|
+
assert COMMODITY_GROUPS == EXPECTED_GROUPS
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_valid_set_matches_expected():
|
|
39
|
+
assert VALID_COMMODITY_GROUPS == frozenset(EXPECTED_GROUPS)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_all_expected_groups_present():
|
|
43
|
+
for group in EXPECTED_GROUPS:
|
|
44
|
+
assert group in VALID_COMMODITY_GROUPS, f"Missing: {group}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_is_valid_crude_oil():
|
|
48
|
+
assert is_valid_commodity_group("Crude Oil") is True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_is_valid_case_sensitive():
|
|
52
|
+
# Matches SQL semantics -- the CHECK constraint uses exact-cased
|
|
53
|
+
# N'Crude Oil' literals.
|
|
54
|
+
assert is_valid_commodity_group("crude oil") is False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_is_valid_unknown_returns_false():
|
|
58
|
+
assert is_valid_commodity_group("Unknown") is False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_is_valid_empty_string_returns_false():
|
|
62
|
+
assert is_valid_commodity_group("") is False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_commodity_groups_is_tuple():
|
|
66
|
+
assert isinstance(COMMODITY_GROUPS, tuple)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_valid_commodity_groups_is_frozenset():
|
|
70
|
+
assert isinstance(VALID_COMMODITY_GROUPS, frozenset)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Cross-check: every CommodityGroup string used by curvemetadata.cme and
|
|
74
|
+
# curvemetadata.ice must round-trip through VALID_COMMODITY_GROUPS. If
|
|
75
|
+
# curvemetadata adds a new group string before the SQL constraint is
|
|
76
|
+
# extended, this test flags the divergence loudly.
|
|
77
|
+
CURVEMETADATA_USED_GROUPS = (
|
|
78
|
+
"Crude Oil",
|
|
79
|
+
"Refined Products",
|
|
80
|
+
"Petrochemical",
|
|
81
|
+
"Freight",
|
|
82
|
+
"NGL",
|
|
83
|
+
"Biofuel",
|
|
84
|
+
# Natural Gas / LNG / Agriculture are not currently used by ice.py /
|
|
85
|
+
# cme.py but are reserved by the SQL constraint.
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize("group", CURVEMETADATA_USED_GROUPS)
|
|
90
|
+
def test_curvemetadata_group_is_recognised(group):
|
|
91
|
+
assert is_valid_commodity_group(group), (
|
|
92
|
+
f"curvemetadata uses {group!r} but it is not in VALID_COMMODITY_GROUPS"
|
|
93
|
+
)
|
|
@@ -102,10 +102,12 @@ def test_parity_with_curvemetadata_long_patterns_only():
|
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
|
|
105
|
-
def
|
|
106
|
-
"""
|
|
107
|
-
curvemetadata.infer_region
|
|
108
|
-
|
|
105
|
+
def test_short_pattern_parity_with_curvemetadata():
|
|
106
|
+
"""Short-pattern (len <= 3) regions used to diverge between
|
|
107
|
+
normalize_region and curvemetadata.taxonomy.infer_region because
|
|
108
|
+
the latter had a `\\b` regex bug (literal backslash-b instead of
|
|
109
|
+
a word boundary). The bug was fixed in curvemetadata 2026-05;
|
|
110
|
+
this test is now a regression guard against the bug returning.
|
|
109
111
|
"""
|
|
110
112
|
try:
|
|
111
113
|
from curvemetadata.taxonomy import infer_region
|
|
@@ -114,27 +116,29 @@ def test_short_pattern_divergence_from_curvemetadata():
|
|
|
114
116
|
|
|
115
117
|
pytest.skip("curvemetadata not available in this environment")
|
|
116
118
|
|
|
117
|
-
# Only patterns of len <= 3 hit the broken regex branch.
|
|
118
|
-
# configured patterns those are: "nyh", "ara", "med", "nwe".
|
|
119
|
+
# Only patterns of len <= 3 hit the previously-broken regex branch.
|
|
120
|
+
# Among the configured patterns those are: "nyh", "ara", "med", "nwe".
|
|
119
121
|
short_pattern_cases = [
|
|
120
122
|
("Brent NYH", "NYH"),
|
|
121
123
|
("NWE jet", "NWE"),
|
|
122
124
|
("ARA gasoil", "ARA"),
|
|
123
125
|
("Med fuel oil", "Med"),
|
|
126
|
+
# Word-boundary protection: "ara" must NOT match inside "Saharan"
|
|
127
|
+
("Saharan crude", None),
|
|
124
128
|
]
|
|
125
129
|
for text, expected in short_pattern_cases:
|
|
126
130
|
assert normalize_region(text) == expected, (
|
|
127
131
|
f"normalize_region({text!r}) should match {expected!r}"
|
|
128
132
|
)
|
|
129
|
-
assert infer_region(text)
|
|
130
|
-
f"curvemetadata.infer_region({text!r})
|
|
131
|
-
f"
|
|
132
|
-
f"
|
|
133
|
+
assert infer_region(text) == expected, (
|
|
134
|
+
f"curvemetadata.infer_region({text!r}) should match {expected!r} "
|
|
135
|
+
f"(got {infer_region(text)!r}). If you see None for one of the "
|
|
136
|
+
f"short patterns, the curvemetadata `\\b` regex bug has regressed."
|
|
133
137
|
)
|
|
134
138
|
|
|
135
139
|
|
|
136
140
|
def test_facade_reexports_visible_at_top_level():
|
|
137
|
-
# Smoke check: the
|
|
141
|
+
# Smoke check: the lazy facade exposes key symbols via dir().
|
|
138
142
|
import commodutil
|
|
139
143
|
|
|
140
144
|
names = set(dir(commodutil))
|
|
@@ -150,3 +154,53 @@ def test_facade_unknown_attribute_raises():
|
|
|
150
154
|
|
|
151
155
|
with pytest.raises(AttributeError):
|
|
152
156
|
_ = commodutil.this_symbol_does_not_exist
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_facade_is_lazy_bare_import_does_not_load_convfactors():
|
|
160
|
+
"""Regression guard for the PEP 562 lazy facade. `import commodutil`
|
|
161
|
+
must NOT eagerly load convfactors (pint registry + Commodity
|
|
162
|
+
dataclass init). Reverting the facade to eager would trip this and
|
|
163
|
+
the cost evidence is documented in the commit message that
|
|
164
|
+
introduced the lazy pattern (~3.3s -> ~2ms speedup)."""
|
|
165
|
+
import importlib
|
|
166
|
+
import sys
|
|
167
|
+
|
|
168
|
+
# Force a clean import to measure the bare cost.
|
|
169
|
+
for mod in [
|
|
170
|
+
"commodutil",
|
|
171
|
+
"commodutil.convfactors",
|
|
172
|
+
"commodutil.dates",
|
|
173
|
+
"commodutil.forwards",
|
|
174
|
+
"commodutil.pandasutil",
|
|
175
|
+
"commodutil.transforms",
|
|
176
|
+
]:
|
|
177
|
+
sys.modules.pop(mod, None)
|
|
178
|
+
|
|
179
|
+
importlib.import_module("commodutil")
|
|
180
|
+
assert "commodutil.convfactors" not in sys.modules, (
|
|
181
|
+
"Lazy facade is broken — `import commodutil` triggered convfactors "
|
|
182
|
+
"load. Restore the PEP 562 __getattr__ pattern in commodutil/__init__.py."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_facade_lazy_load_resolves_and_caches():
|
|
187
|
+
"""First access of a facade symbol loads the source submodule and
|
|
188
|
+
caches the resolved value back into globals."""
|
|
189
|
+
import importlib
|
|
190
|
+
import sys
|
|
191
|
+
|
|
192
|
+
for mod in [
|
|
193
|
+
"commodutil",
|
|
194
|
+
"commodutil.convfactors",
|
|
195
|
+
]:
|
|
196
|
+
sys.modules.pop(mod, None)
|
|
197
|
+
|
|
198
|
+
commodutil = importlib.import_module("commodutil")
|
|
199
|
+
assert "commodutil.convfactors" not in sys.modules
|
|
200
|
+
|
|
201
|
+
fn = commodutil.convert_price # triggers lazy load
|
|
202
|
+
assert "commodutil.convfactors" in sys.modules
|
|
203
|
+
assert callable(fn)
|
|
204
|
+
|
|
205
|
+
# Second access hits the cache (still works, same object)
|
|
206
|
+
assert commodutil.convert_price is fn
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Tests for commodutil.standards.units."""
|
|
2
|
+
|
|
3
|
+
from commodutil.standards.units import default_unit_for_commodity
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_natgas_defaults_to_mmbtu():
|
|
7
|
+
assert default_unit_for_commodity("natgas") == "mmbtu"
|
|
8
|
+
assert default_unit_for_commodity("natural_gas") == "mmbtu"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_refined_products_default_to_gal():
|
|
12
|
+
assert default_unit_for_commodity("gasoline") == "gal"
|
|
13
|
+
assert default_unit_for_commodity("diesel") == "gal"
|
|
14
|
+
assert default_unit_for_commodity("jet") == "gal"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_crude_and_unknown_default_to_bbl():
|
|
18
|
+
assert default_unit_for_commodity("crude") == "bbl"
|
|
19
|
+
# Not in map — fallback to bbl
|
|
20
|
+
assert default_unit_for_commodity("butane") == "bbl"
|
|
21
|
+
assert default_unit_for_commodity("fuel_oil") == "bbl"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_empty_and_none_default_to_bbl():
|
|
25
|
+
assert default_unit_for_commodity("") == "bbl"
|
|
26
|
+
assert default_unit_for_commodity(None) == "bbl"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_case_insensitive():
|
|
30
|
+
assert default_unit_for_commodity("NATGAS") == "mmbtu"
|
|
31
|
+
assert default_unit_for_commodity("Gasoline") == "gal"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---- UNIT_MAP tests ----
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_unit_map_canonical_values():
|
|
38
|
+
from commodutil.standards.units import UNIT_MAP
|
|
39
|
+
|
|
40
|
+
assert UNIT_MAP["barrel"] == "bbl"
|
|
41
|
+
assert UNIT_MAP["barrels"] == "bbl"
|
|
42
|
+
assert UNIT_MAP["bbl"] == "bbl"
|
|
43
|
+
assert UNIT_MAP["bbls"] == "bbl"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_unit_map_gallon_variants():
|
|
47
|
+
from commodutil.standards.units import UNIT_MAP
|
|
48
|
+
|
|
49
|
+
assert UNIT_MAP["gallon"] == "gal"
|
|
50
|
+
assert UNIT_MAP["gallons"] == "gal"
|
|
51
|
+
assert UNIT_MAP["gal"] == "gal"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_unit_map_metric_ton_variants():
|
|
55
|
+
from commodutil.standards.units import UNIT_MAP
|
|
56
|
+
|
|
57
|
+
assert UNIT_MAP["metric ton"] == "mt"
|
|
58
|
+
assert UNIT_MAP["metric tons"] == "mt"
|
|
59
|
+
assert UNIT_MAP["metric tonne"] == "mt"
|
|
60
|
+
assert UNIT_MAP["metric tonnes"] == "mt"
|
|
61
|
+
assert UNIT_MAP["tonne"] == "mt"
|
|
62
|
+
assert UNIT_MAP["tonnes"] == "mt"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_unit_map_canonical_set_only_three():
|
|
66
|
+
"""UNIT_MAP normalises to exactly the canonical units bbl / gal / mt."""
|
|
67
|
+
from commodutil.standards.units import UNIT_MAP
|
|
68
|
+
|
|
69
|
+
assert set(UNIT_MAP.values()) == {"bbl", "gal", "mt"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_unit_map_parity_with_curvemetadata():
|
|
73
|
+
"""Regression guard: UNIT_MAP must match curvemetadata's previous local
|
|
74
|
+
copy byte-for-byte until curvemetadata's re-export PR lands. Skipped if
|
|
75
|
+
curvemetadata is unavailable."""
|
|
76
|
+
try:
|
|
77
|
+
from curvemetadata.common_maps import UNIT_MAP as cm_UNIT_MAP
|
|
78
|
+
except ImportError:
|
|
79
|
+
import pytest
|
|
80
|
+
|
|
81
|
+
pytest.skip("curvemetadata not available in this environment")
|
|
82
|
+
|
|
83
|
+
from commodutil.standards.units import UNIT_MAP
|
|
84
|
+
|
|
85
|
+
# Parity: every key/value in the curvemetadata copy is in commodutil's
|
|
86
|
+
assert cm_UNIT_MAP == UNIT_MAP
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
"""commodutil: Commodity market standards backbone.
|
|
2
|
-
|
|
3
|
-
Public API for unit conversions, forward curve transforms, date utilities,
|
|
4
|
-
and pandas helpers. Re-exports the most-used symbols from sub-modules so
|
|
5
|
-
callers can write `from commodutil import convert_price` instead of
|
|
6
|
-
`from commodutil.convfactors import convert_price`.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from commodutil.convfactors import (
|
|
10
|
-
ALIASES,
|
|
11
|
-
COMMODITIES,
|
|
12
|
-
Commodity,
|
|
13
|
-
FRACTIONAL_TO_MAJOR,
|
|
14
|
-
VALID_CURRENCY_TOKENS,
|
|
15
|
-
convert,
|
|
16
|
-
convert_price,
|
|
17
|
-
convfactor,
|
|
18
|
-
fractional_to_major,
|
|
19
|
-
is_fractional_currency,
|
|
20
|
-
list_commodities,
|
|
21
|
-
list_units,
|
|
22
|
-
split_currency_unit,
|
|
23
|
-
)
|
|
24
|
-
from commodutil.dates import (
|
|
25
|
-
curmon,
|
|
26
|
-
curmonyear,
|
|
27
|
-
curmonyear_str,
|
|
28
|
-
curyear,
|
|
29
|
-
find_year,
|
|
30
|
-
last_day_of_prev_month,
|
|
31
|
-
nextyear,
|
|
32
|
-
prevmon,
|
|
33
|
-
prevmon_str,
|
|
34
|
-
prevyear,
|
|
35
|
-
start_day_of_prev_month,
|
|
36
|
-
)
|
|
37
|
-
from commodutil.forwards import (
|
|
38
|
-
all_spread_combinations,
|
|
39
|
-
time_spreads,
|
|
40
|
-
)
|
|
41
|
-
from commodutil.pandasutil import (
|
|
42
|
-
fillna_downbet,
|
|
43
|
-
mergets,
|
|
44
|
-
)
|
|
45
|
-
from commodutil.transforms import (
|
|
46
|
-
seasonalize,
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
__all__ = [
|
|
50
|
-
# convfactors
|
|
51
|
-
"ALIASES",
|
|
52
|
-
"COMMODITIES",
|
|
53
|
-
"Commodity",
|
|
54
|
-
"FRACTIONAL_TO_MAJOR",
|
|
55
|
-
"VALID_CURRENCY_TOKENS",
|
|
56
|
-
"convert",
|
|
57
|
-
"convert_price",
|
|
58
|
-
"convfactor",
|
|
59
|
-
"fractional_to_major",
|
|
60
|
-
"is_fractional_currency",
|
|
61
|
-
"list_commodities",
|
|
62
|
-
"list_units",
|
|
63
|
-
"split_currency_unit",
|
|
64
|
-
# dates
|
|
65
|
-
"curmon",
|
|
66
|
-
"curmonyear",
|
|
67
|
-
"curmonyear_str",
|
|
68
|
-
"curyear",
|
|
69
|
-
"find_year",
|
|
70
|
-
"last_day_of_prev_month",
|
|
71
|
-
"nextyear",
|
|
72
|
-
"prevmon",
|
|
73
|
-
"prevmon_str",
|
|
74
|
-
"prevyear",
|
|
75
|
-
"start_day_of_prev_month",
|
|
76
|
-
# forwards
|
|
77
|
-
"all_spread_combinations",
|
|
78
|
-
"time_spreads",
|
|
79
|
-
# pandasutil
|
|
80
|
-
"fillna_downbet",
|
|
81
|
-
"mergets",
|
|
82
|
-
# transforms
|
|
83
|
-
"seasonalize",
|
|
84
|
-
]
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"""commodutil.standards.units: default-unit helpers per commodity.
|
|
2
|
-
|
|
3
|
-
Lifted from pyoilprice/conversion.py's inline fallback. Pure vocab — no
|
|
4
|
-
pint, no pandas.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
_DEFAULT_UNIT = {
|
|
11
|
-
"natgas": "mmbtu",
|
|
12
|
-
"natural_gas": "mmbtu",
|
|
13
|
-
"gasoline": "gal",
|
|
14
|
-
"diesel": "gal",
|
|
15
|
-
"jet": "gal",
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def default_unit_for_commodity(commodity: str) -> str:
|
|
20
|
-
"""Return the canonical quoted unit for a commodity (volume basis).
|
|
21
|
-
|
|
22
|
-
Falls back to 'bbl' for any commodity not in the explicit map (covers
|
|
23
|
-
crude / fuel oil / naphtha / VGO / NGL species etc.).
|
|
24
|
-
"""
|
|
25
|
-
if not commodity:
|
|
26
|
-
return "bbl"
|
|
27
|
-
return _DEFAULT_UNIT.get(str(commodity).lower(), "bbl")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
__all__ = ["default_unit_for_commodity"]
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
"""Tests for commodutil.standards.units."""
|
|
2
|
-
|
|
3
|
-
from commodutil.standards.units import default_unit_for_commodity
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def test_natgas_defaults_to_mmbtu():
|
|
7
|
-
assert default_unit_for_commodity("natgas") == "mmbtu"
|
|
8
|
-
assert default_unit_for_commodity("natural_gas") == "mmbtu"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_refined_products_default_to_gal():
|
|
12
|
-
assert default_unit_for_commodity("gasoline") == "gal"
|
|
13
|
-
assert default_unit_for_commodity("diesel") == "gal"
|
|
14
|
-
assert default_unit_for_commodity("jet") == "gal"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_crude_and_unknown_default_to_bbl():
|
|
18
|
-
assert default_unit_for_commodity("crude") == "bbl"
|
|
19
|
-
# Not in map — fallback to bbl
|
|
20
|
-
assert default_unit_for_commodity("butane") == "bbl"
|
|
21
|
-
assert default_unit_for_commodity("fuel_oil") == "bbl"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_empty_and_none_default_to_bbl():
|
|
25
|
-
assert default_unit_for_commodity("") == "bbl"
|
|
26
|
-
assert default_unit_for_commodity(None) == "bbl"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def test_case_insensitive():
|
|
30
|
-
assert default_unit_for_commodity("NATGAS") == "mmbtu"
|
|
31
|
-
assert default_unit_for_commodity("Gasoline") == "gal"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|