commodutil 3.9.0__tar.gz → 3.10.1__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 (75) hide show
  1. {commodutil-3.9.0 → commodutil-3.10.1}/PKG-INFO +1 -1
  2. commodutil-3.10.1/commodutil/__init__.py +80 -0
  3. commodutil-3.10.1/commodutil/standards/__init__.py +70 -0
  4. commodutil-3.10.1/commodutil/standards/analysis_types.py +59 -0
  5. commodutil-3.10.1/commodutil/standards/commodities.py +71 -0
  6. commodutil-3.10.1/commodutil/standards/commodity_groups.py +40 -0
  7. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/standards/regions.py +9 -2
  8. commodutil-3.10.1/commodutil/standards/units.py +64 -0
  9. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil.egg-info/PKG-INFO +1 -1
  10. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil.egg-info/SOURCES.txt +6 -0
  11. commodutil-3.10.1/tests/test_standards_analysis_types.py +60 -0
  12. commodutil-3.10.1/tests/test_standards_commodities.py +71 -0
  13. commodutil-3.10.1/tests/test_standards_commodity_groups.py +93 -0
  14. commodutil-3.10.1/tests/test_standards_regions.py +236 -0
  15. commodutil-3.10.1/tests/test_standards_units.py +86 -0
  16. commodutil-3.9.0/commodutil/__init__.py +0 -84
  17. commodutil-3.9.0/commodutil/standards/__init__.py +0 -1
  18. commodutil-3.9.0/commodutil/standards/units.py +0 -30
  19. commodutil-3.9.0/tests/test_standards_regions.py +0 -152
  20. commodutil-3.9.0/tests/test_standards_units.py +0 -31
  21. {commodutil-3.9.0 → commodutil-3.10.1}/.coveragerc +0 -0
  22. {commodutil-3.9.0 → commodutil-3.10.1}/.github/workflows/1_tests.yml +0 -0
  23. {commodutil-3.9.0 → commodutil-3.10.1}/.github/workflows/2_coverage.yml +0 -0
  24. {commodutil-3.9.0 → commodutil-3.10.1}/.github/workflows/3_linting.yml +0 -0
  25. {commodutil-3.9.0 → commodutil-3.10.1}/.github/workflows/4_release.yml +0 -0
  26. {commodutil-3.9.0 → commodutil-3.10.1}/.gitignore +0 -0
  27. {commodutil-3.9.0 → commodutil-3.10.1}/.pypirc +0 -0
  28. {commodutil-3.9.0 → commodutil-3.10.1}/azure-build-pipelines.yml +0 -0
  29. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/arb.py +0 -0
  30. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/convfactors.py +0 -0
  31. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/dates.py +0 -0
  32. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/forward/__init__.py +0 -0
  33. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/forward/calendar.py +0 -0
  34. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/forward/continuous.py +0 -0
  35. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/forward/fly.py +0 -0
  36. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/forward/quarterly.py +0 -0
  37. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/forward/spreads.py +0 -0
  38. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/forward/structure.py +0 -0
  39. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/forward/util.py +0 -0
  40. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/forwards.py +0 -0
  41. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/pandasutil.py +0 -0
  42. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/standards/currency.py +0 -0
  43. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/stats.py +0 -0
  44. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil/transforms.py +0 -0
  45. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil.egg-info/dependency_links.txt +0 -0
  46. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil.egg-info/requires.txt +0 -0
  47. {commodutil-3.9.0 → commodutil-3.10.1}/commodutil.egg-info/top_level.txt +0 -0
  48. {commodutil-3.9.0 → commodutil-3.10.1}/pyproject.toml +0 -0
  49. {commodutil-3.9.0 → commodutil-3.10.1}/requirements-test.txt +0 -0
  50. {commodutil-3.9.0 → commodutil-3.10.1}/requirements.txt +0 -0
  51. {commodutil-3.9.0 → commodutil-3.10.1}/requirements_dev.txt +0 -0
  52. {commodutil-3.9.0 → commodutil-3.10.1}/scripts/rbw_structure_scan.py +0 -0
  53. {commodutil-3.9.0 → commodutil-3.10.1}/setup.cfg +0 -0
  54. {commodutil-3.9.0 → commodutil-3.10.1}/tests/__init__.py +0 -0
  55. {commodutil-3.9.0 → commodutil-3.10.1}/tests/conftest.py +0 -0
  56. {commodutil-3.9.0 → commodutil-3.10.1}/tests/forward/__init__.py +0 -0
  57. {commodutil-3.9.0 → commodutil-3.10.1}/tests/forward/conftest.py +0 -0
  58. {commodutil-3.9.0 → commodutil-3.10.1}/tests/forward/test_calendar.py +0 -0
  59. {commodutil-3.9.0 → commodutil-3.10.1}/tests/forward/test_continuous.py +0 -0
  60. {commodutil-3.9.0 → commodutil-3.10.1}/tests/forward/test_fly.py +0 -0
  61. {commodutil-3.9.0 → commodutil-3.10.1}/tests/forward/test_quarterly.py +0 -0
  62. {commodutil-3.9.0 → commodutil-3.10.1}/tests/forward/test_spreads.py +0 -0
  63. {commodutil-3.9.0 → commodutil-3.10.1}/tests/forward/test_structure.py +0 -0
  64. {commodutil-3.9.0 → commodutil-3.10.1}/tests/forward/test_util.py +0 -0
  65. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_arb.py +0 -0
  66. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_cl.csv +0 -0
  67. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_conv.py +0 -0
  68. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_dates.py +0 -0
  69. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_forwards.py +0 -0
  70. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_pandasutils.py +0 -0
  71. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_price_conv.py +0 -0
  72. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_standards_currency.py +0 -0
  73. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_stats.py +0 -0
  74. {commodutil-3.9.0 → commodutil-3.10.1}/tests/test_transforms.py +0 -0
  75. {commodutil-3.9.0 → commodutil-3.10.1}/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.1
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,70 @@
1
+ """commodutil.standards: canonical vocabularies for commodity trading.
2
+
3
+ Re-exports the public surface of each submodule so callers can write
4
+ `from commodutil.standards import normalize_region` instead of reaching
5
+ into the submodule directly.
6
+ """
7
+
8
+ from commodutil.standards.analysis_types import (
9
+ ANALYSIS_TYPES,
10
+ infer_analysis_type,
11
+ )
12
+ from commodutil.standards.commodities import (
13
+ COMMODITY_CONVERSION_MAP,
14
+ COMMODITY_KEYWORDS,
15
+ )
16
+ from commodutil.standards.commodity_groups import (
17
+ COMMODITY_GROUPS,
18
+ VALID_COMMODITY_GROUPS,
19
+ is_valid_commodity_group,
20
+ )
21
+ from commodutil.standards.currency import (
22
+ FRACTIONAL_CURRENCY_DIVISORS,
23
+ FRACTIONAL_TO_MAJOR,
24
+ VALID_CURRENCY_TOKENS,
25
+ fractional_to_major,
26
+ is_fractional_currency,
27
+ required_fx_pair,
28
+ split_currency_unit,
29
+ to_symbol,
30
+ )
31
+ from commodutil.standards.regions import (
32
+ REGION_PATTERNS,
33
+ VALID_REGIONS,
34
+ is_valid_region,
35
+ normalize_region,
36
+ )
37
+ from commodutil.standards.units import (
38
+ UNIT_MAP,
39
+ default_unit_for_commodity,
40
+ )
41
+
42
+ __all__ = [
43
+ # analysis_types
44
+ "ANALYSIS_TYPES",
45
+ "infer_analysis_type",
46
+ # commodities
47
+ "COMMODITY_CONVERSION_MAP",
48
+ "COMMODITY_KEYWORDS",
49
+ # commodity_groups
50
+ "COMMODITY_GROUPS",
51
+ "VALID_COMMODITY_GROUPS",
52
+ "is_valid_commodity_group",
53
+ # currency
54
+ "FRACTIONAL_CURRENCY_DIVISORS",
55
+ "FRACTIONAL_TO_MAJOR",
56
+ "VALID_CURRENCY_TOKENS",
57
+ "fractional_to_major",
58
+ "is_fractional_currency",
59
+ "required_fx_pair",
60
+ "split_currency_unit",
61
+ "to_symbol",
62
+ # regions
63
+ "REGION_PATTERNS",
64
+ "VALID_REGIONS",
65
+ "is_valid_region",
66
+ "normalize_region",
67
+ # units
68
+ "UNIT_MAP",
69
+ "default_unit_for_commodity",
70
+ ]
@@ -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
+ ]
@@ -60,8 +60,15 @@ def normalize_region(text: Optional[str]) -> Optional[str]:
60
60
 
61
61
  lower = text.lower()
62
62
 
63
- # RBOB convention: always NY Harbor
64
- if "rbob" in lower:
63
+ # CARBOB short-circuit: California Reformulated Blendstock for Oxygenate
64
+ # Blending Los Angeles / US West Coast, NOT NY Harbor. Must run BEFORE
65
+ # the RBOB check so "carbob" doesn't fall through to the NYH heuristic.
66
+ if re.search(r"\bcarbob\b", lower):
67
+ return "LA"
68
+
69
+ # RBOB convention: always NY Harbor. Use word boundary so "carbob" (which
70
+ # has 'a' before 'rbob' — no word boundary) does not match here.
71
+ if re.search(r"\brbob\b", lower):
65
72
  return "NYH"
66
73
 
67
74
  # Pattern-match against REGION_PATTERNS
@@ -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.1
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
+ )