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.
Files changed (73) hide show
  1. {commodutil-3.9.0 → commodutil-3.10.0}/PKG-INFO +1 -1
  2. commodutil-3.10.0/commodutil/__init__.py +80 -0
  3. commodutil-3.10.0/commodutil/standards/analysis_types.py +59 -0
  4. commodutil-3.10.0/commodutil/standards/commodities.py +71 -0
  5. commodutil-3.10.0/commodutil/standards/commodity_groups.py +40 -0
  6. commodutil-3.10.0/commodutil/standards/units.py +64 -0
  7. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/PKG-INFO +1 -1
  8. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/SOURCES.txt +6 -0
  9. commodutil-3.10.0/tests/test_standards_analysis_types.py +60 -0
  10. commodutil-3.10.0/tests/test_standards_commodities.py +71 -0
  11. commodutil-3.10.0/tests/test_standards_commodity_groups.py +93 -0
  12. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_standards_regions.py +65 -11
  13. commodutil-3.10.0/tests/test_standards_units.py +86 -0
  14. commodutil-3.9.0/commodutil/__init__.py +0 -84
  15. commodutil-3.9.0/commodutil/standards/units.py +0 -30
  16. commodutil-3.9.0/tests/test_standards_units.py +0 -31
  17. {commodutil-3.9.0 → commodutil-3.10.0}/.coveragerc +0 -0
  18. {commodutil-3.9.0 → commodutil-3.10.0}/.github/workflows/1_tests.yml +0 -0
  19. {commodutil-3.9.0 → commodutil-3.10.0}/.github/workflows/2_coverage.yml +0 -0
  20. {commodutil-3.9.0 → commodutil-3.10.0}/.github/workflows/3_linting.yml +0 -0
  21. {commodutil-3.9.0 → commodutil-3.10.0}/.github/workflows/4_release.yml +0 -0
  22. {commodutil-3.9.0 → commodutil-3.10.0}/.gitignore +0 -0
  23. {commodutil-3.9.0 → commodutil-3.10.0}/.pypirc +0 -0
  24. {commodutil-3.9.0 → commodutil-3.10.0}/azure-build-pipelines.yml +0 -0
  25. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/arb.py +0 -0
  26. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/convfactors.py +0 -0
  27. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/dates.py +0 -0
  28. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/__init__.py +0 -0
  29. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/calendar.py +0 -0
  30. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/continuous.py +0 -0
  31. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/fly.py +0 -0
  32. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/quarterly.py +0 -0
  33. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/spreads.py +0 -0
  34. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/structure.py +0 -0
  35. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forward/util.py +0 -0
  36. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/forwards.py +0 -0
  37. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/pandasutil.py +0 -0
  38. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/standards/__init__.py +0 -0
  39. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/standards/currency.py +0 -0
  40. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/standards/regions.py +0 -0
  41. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/stats.py +0 -0
  42. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil/transforms.py +0 -0
  43. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/dependency_links.txt +0 -0
  44. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/requires.txt +0 -0
  45. {commodutil-3.9.0 → commodutil-3.10.0}/commodutil.egg-info/top_level.txt +0 -0
  46. {commodutil-3.9.0 → commodutil-3.10.0}/pyproject.toml +0 -0
  47. {commodutil-3.9.0 → commodutil-3.10.0}/requirements-test.txt +0 -0
  48. {commodutil-3.9.0 → commodutil-3.10.0}/requirements.txt +0 -0
  49. {commodutil-3.9.0 → commodutil-3.10.0}/requirements_dev.txt +0 -0
  50. {commodutil-3.9.0 → commodutil-3.10.0}/scripts/rbw_structure_scan.py +0 -0
  51. {commodutil-3.9.0 → commodutil-3.10.0}/setup.cfg +0 -0
  52. {commodutil-3.9.0 → commodutil-3.10.0}/tests/__init__.py +0 -0
  53. {commodutil-3.9.0 → commodutil-3.10.0}/tests/conftest.py +0 -0
  54. {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/__init__.py +0 -0
  55. {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/conftest.py +0 -0
  56. {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_calendar.py +0 -0
  57. {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_continuous.py +0 -0
  58. {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_fly.py +0 -0
  59. {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_quarterly.py +0 -0
  60. {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_spreads.py +0 -0
  61. {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_structure.py +0 -0
  62. {commodutil-3.9.0 → commodutil-3.10.0}/tests/forward/test_util.py +0 -0
  63. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_arb.py +0 -0
  64. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_cl.csv +0 -0
  65. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_conv.py +0 -0
  66. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_dates.py +0 -0
  67. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_forwards.py +0 -0
  68. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_pandasutils.py +0 -0
  69. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_price_conv.py +0 -0
  70. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_standards_currency.py +0 -0
  71. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_stats.py +0 -0
  72. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_transforms.py +0 -0
  73. {commodutil-3.9.0 → commodutil-3.10.0}/tests/test_weekly.csv +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commodutil
3
- Version: 3.9.0
3
+ Version: 3.10.0
4
4
  Summary: common commodity/oil analytics utils
5
5
  Author-email: aeorxc <author@example.com>
6
6
  Project-URL: Homepage, https://dev.azure.com/RWEST-MFI-TE/Oil/_git/commodutil
@@ -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"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commodutil
3
- Version: 3.9.0
3
+ Version: 3.10.0
4
4
  Summary: common commodity/oil analytics utils
5
5
  Author-email: aeorxc <author@example.com>
6
6
  Project-URL: Homepage, https://dev.azure.com/RWEST-MFI-TE/Oil/_git/commodutil
@@ -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 test_short_pattern_divergence_from_curvemetadata():
106
- """Pin the known bug fix: normalize_region matches short codes that
107
- curvemetadata.infer_region (broken) does not. If curvemetadata ever
108
- fixes its `\\b` typo, this test will fail and should be deleted.
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. Among the
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) is None, (
130
- f"curvemetadata.infer_region({text!r}) is expected to return "
131
- f"None (broken). Got {infer_region(text)!r}. If the bug is "
132
- f"fixed, delete this test."
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 eager facade exposes key symbols at top level.
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