commodutil 4.2.0__tar.gz → 4.3.2__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 (69) hide show
  1. {commodutil-4.2.0 → commodutil-4.3.2}/PKG-INFO +1 -1
  2. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/__init__.py +4 -0
  3. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/convfactors.py +1 -2
  4. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/standards/__init__.py +6 -0
  5. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/standards/currency.py +45 -0
  6. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/standards/units.py +54 -1
  7. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil.egg-info/PKG-INFO +1 -1
  8. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_price_conv.py +5 -0
  9. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_standards_currency.py +17 -0
  10. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_standards_units.py +43 -4
  11. {commodutil-4.2.0 → commodutil-4.3.2}/.coveragerc +0 -0
  12. {commodutil-4.2.0 → commodutil-4.3.2}/.github/workflows/1_tests.yml +0 -0
  13. {commodutil-4.2.0 → commodutil-4.3.2}/.github/workflows/2_coverage.yml +0 -0
  14. {commodutil-4.2.0 → commodutil-4.3.2}/.github/workflows/3_linting.yml +0 -0
  15. {commodutil-4.2.0 → commodutil-4.3.2}/.github/workflows/4_release.yml +0 -0
  16. {commodutil-4.2.0 → commodutil-4.3.2}/.gitignore +0 -0
  17. {commodutil-4.2.0 → commodutil-4.3.2}/.pypirc +0 -0
  18. {commodutil-4.2.0 → commodutil-4.3.2}/azure-build-pipelines.yml +0 -0
  19. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/arb.py +0 -0
  20. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/dates.py +0 -0
  21. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/forward/__init__.py +0 -0
  22. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/forward/calendar.py +0 -0
  23. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/forward/continuous.py +0 -0
  24. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/forward/fly.py +0 -0
  25. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/forward/quarterly.py +0 -0
  26. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/forward/spreads.py +0 -0
  27. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/forward/structure.py +0 -0
  28. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/forward/util.py +0 -0
  29. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/forwards.py +0 -0
  30. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/pandasutil.py +0 -0
  31. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/standards/analysis_types.py +0 -0
  32. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/standards/commodities.py +0 -0
  33. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/standards/commodity_groups.py +0 -0
  34. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/standards/regions.py +0 -0
  35. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/stats.py +0 -0
  36. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil/transforms.py +0 -0
  37. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil.egg-info/SOURCES.txt +0 -0
  38. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil.egg-info/dependency_links.txt +0 -0
  39. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil.egg-info/requires.txt +0 -0
  40. {commodutil-4.2.0 → commodutil-4.3.2}/commodutil.egg-info/top_level.txt +0 -0
  41. {commodutil-4.2.0 → commodutil-4.3.2}/pyproject.toml +0 -0
  42. {commodutil-4.2.0 → commodutil-4.3.2}/requirements-test.txt +0 -0
  43. {commodutil-4.2.0 → commodutil-4.3.2}/requirements.txt +0 -0
  44. {commodutil-4.2.0 → commodutil-4.3.2}/requirements_dev.txt +0 -0
  45. {commodutil-4.2.0 → commodutil-4.3.2}/setup.cfg +0 -0
  46. {commodutil-4.2.0 → commodutil-4.3.2}/tests/__init__.py +0 -0
  47. {commodutil-4.2.0 → commodutil-4.3.2}/tests/conftest.py +0 -0
  48. {commodutil-4.2.0 → commodutil-4.3.2}/tests/forward/__init__.py +0 -0
  49. {commodutil-4.2.0 → commodutil-4.3.2}/tests/forward/conftest.py +0 -0
  50. {commodutil-4.2.0 → commodutil-4.3.2}/tests/forward/test_calendar.py +0 -0
  51. {commodutil-4.2.0 → commodutil-4.3.2}/tests/forward/test_continuous.py +0 -0
  52. {commodutil-4.2.0 → commodutil-4.3.2}/tests/forward/test_fly.py +0 -0
  53. {commodutil-4.2.0 → commodutil-4.3.2}/tests/forward/test_quarterly.py +0 -0
  54. {commodutil-4.2.0 → commodutil-4.3.2}/tests/forward/test_spreads.py +0 -0
  55. {commodutil-4.2.0 → commodutil-4.3.2}/tests/forward/test_structure.py +0 -0
  56. {commodutil-4.2.0 → commodutil-4.3.2}/tests/forward/test_util.py +0 -0
  57. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_arb.py +0 -0
  58. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_cl.csv +0 -0
  59. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_conv.py +0 -0
  60. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_dates.py +0 -0
  61. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_forwards.py +0 -0
  62. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_pandasutils.py +0 -0
  63. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_standards_analysis_types.py +0 -0
  64. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_standards_commodities.py +0 -0
  65. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_standards_commodity_groups.py +0 -0
  66. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_standards_regions.py +0 -0
  67. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_stats.py +0 -0
  68. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_transforms.py +0 -0
  69. {commodutil-4.2.0 → commodutil-4.3.2}/tests/test_weekly.csv +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commodutil
3
- Version: 4.2.0
3
+ Version: 4.3.2
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
@@ -35,7 +35,11 @@ _LAZY_EXPORTS = {
35
35
  "VALID_CURRENCY_TOKENS": "commodutil.standards.currency",
36
36
  "fractional_to_major": "commodutil.standards.currency",
37
37
  "is_fractional_currency": "commodutil.standards.currency",
38
+ "normalize_currency_token": "commodutil.standards.currency",
38
39
  "split_currency_unit": "commodutil.standards.currency",
40
+ # standards.units (stdlib-only -- cheap, no pint)
41
+ "canonical_quantity_unit": "commodutil.standards.units",
42
+ "quantity_unit_from_price_unit": "commodutil.standards.units",
39
43
  # dates
40
44
  "curmon": "commodutil.dates",
41
45
  "curmonyear": "commodutil.dates",
@@ -11,6 +11,7 @@ from dataclasses import dataclass
11
11
  import pandas as pd
12
12
  from functools import lru_cache
13
13
 
14
+ from commodutil.standards import currency as _currency
14
15
  from commodutil.standards.units import to_pint_token as _to_pint_token
15
16
 
16
17
  logger = logging.getLogger(__name__)
@@ -542,8 +543,6 @@ def convfactor(from_unit: str, to_unit: str, commodity: Optional[str] = None) ->
542
543
  # above) and reads currency vocabulary directly from _currency. Callers
543
544
  # wanting currency vocabulary should import from commodutil.standards.currency.
544
545
 
545
- from commodutil.standards import currency as _currency
546
-
547
546
 
548
547
  def convert_price(
549
548
  value: Union[float, pd.Series],
@@ -28,6 +28,7 @@ from commodutil.standards.currency import (
28
28
  VALID_CURRENCY_TOKENS,
29
29
  fractional_to_major,
30
30
  is_fractional_currency,
31
+ normalize_currency_token,
31
32
  required_fx_pair,
32
33
  split_currency_unit,
33
34
  to_symbol,
@@ -43,7 +44,9 @@ from commodutil.standards.regions import (
43
44
  )
44
45
  from commodutil.standards.units import (
45
46
  UNIT_MAP,
47
+ canonical_quantity_unit,
46
48
  default_unit_for_commodity,
49
+ quantity_unit_from_price_unit,
47
50
  to_pint_token,
48
51
  )
49
52
 
@@ -68,6 +71,7 @@ __all__ = [
68
71
  "VALID_CURRENCY_TOKENS",
69
72
  "fractional_to_major",
70
73
  "is_fractional_currency",
74
+ "normalize_currency_token",
71
75
  "required_fx_pair",
72
76
  "split_currency_unit",
73
77
  "to_symbol",
@@ -81,6 +85,8 @@ __all__ = [
81
85
  "normalize_region",
82
86
  # units
83
87
  "UNIT_MAP",
88
+ "canonical_quantity_unit",
84
89
  "default_unit_for_commodity",
90
+ "quantity_unit_from_price_unit",
85
91
  "to_pint_token",
86
92
  ]
@@ -80,12 +80,56 @@ _VALID_CURRENCY_TOKENS = {
80
80
  "$",
81
81
  }
82
82
 
83
+ _CURRENCY_TOKEN_LOOKUP = {token.upper(): token for token in _VALID_CURRENCY_TOKENS}
84
+ _CURRENCY_TOKEN_ALIASES = {
85
+ "USC": "USc",
86
+ "US CENTS": "USc",
87
+ "U S CENTS": "USc",
88
+ "GBX": "GBp",
89
+ "GBPENCE": "GBp",
90
+ "GB PENCE": "GBp",
91
+ "PENCE": "GBp",
92
+ "RMB": "CNY",
93
+ }
94
+
83
95
  # Public aliases — these are the import names downstream code should use.
84
96
  VALID_CURRENCY_TOKENS = _VALID_CURRENCY_TOKENS
85
97
  FRACTIONAL_TO_MAJOR = _FRACTIONAL_TO_MAJOR
86
98
  FRACTIONAL_CURRENCY_DIVISORS = _FRACTIONAL_CURRENCY_DIVISORS
87
99
 
88
100
 
101
+ def _normalized_currency_lookup_key(token: str) -> str:
102
+ return " ".join(str(token).replace(".", " ").strip().upper().split())
103
+
104
+
105
+ def normalize_currency_token(token: Optional[str]) -> Optional[str]:
106
+ """Return the canonical commodutil currency token for common aliases.
107
+
108
+ The canonical tokens remain ``VALID_CURRENCY_TOKENS``. This helper accepts
109
+ mixed case and legacy/fractional aliases used in vendor specs, such as
110
+ ``USC``/``US cents`` -> ``USc`` and ``GBX``/``pence`` -> ``GBp``. Unknown
111
+ values return ``None`` so callers can distinguish currency-like tokens
112
+ from physical units.
113
+ """
114
+ if token is None:
115
+ return None
116
+ cleaned = str(token).strip()
117
+ if not cleaned:
118
+ return None
119
+ if cleaned in _VALID_CURRENCY_TOKENS:
120
+ return cleaned
121
+
122
+ lower = cleaned.lower()
123
+ mapped = CURRENCY_MAP.get(lower)
124
+ if mapped:
125
+ return mapped
126
+
127
+ lookup_key = _normalized_currency_lookup_key(cleaned)
128
+ return _CURRENCY_TOKEN_LOOKUP.get(lookup_key) or _CURRENCY_TOKEN_ALIASES.get(
129
+ lookup_key
130
+ )
131
+
132
+
89
133
  def is_fractional_currency(token: str) -> bool:
90
134
  """Return True if `token` is a recognised fractional currency
91
135
  (e.g. 'GBp', 'USc'). Useful for detecting pure-scale conversions that
@@ -240,6 +284,7 @@ __all__ = [
240
284
  "FRACTIONAL_TO_MAJOR",
241
285
  "FRACTIONAL_CURRENCY_DIVISORS",
242
286
  "CURRENCY_MAP",
287
+ "normalize_currency_token",
243
288
  "is_fractional_currency",
244
289
  "fractional_to_major",
245
290
  "split_currency_unit",
@@ -26,6 +26,8 @@ from __future__ import annotations
26
26
 
27
27
  from typing import Optional
28
28
 
29
+ from commodutil.standards.currency import split_currency_unit
30
+
29
31
 
30
32
  # ---- Alias -> canonical normalisation ----
31
33
 
@@ -45,8 +47,13 @@ UNIT_MAP = {
45
47
  "metric tons": "mt",
46
48
  "metric tonne": "mt",
47
49
  "metric tonnes": "mt",
50
+ "mt": "mt",
48
51
  "tonne": "mt",
49
52
  "tonnes": "mt",
53
+ "pound": "lb",
54
+ "pounds": "lb",
55
+ "lb": "lb",
56
+ "lbs": "lb",
50
57
  }
51
58
 
52
59
 
@@ -72,6 +79,43 @@ def default_unit_for_commodity(commodity: Optional[str]) -> str:
72
79
  return _DEFAULT_UNIT.get(str(commodity).lower(), "bbl")
73
80
 
74
81
 
82
+ def canonical_quantity_unit(unit: object) -> str | None:
83
+ """Return the canonical quantity unit token for free-form unit text.
84
+
85
+ This intentionally covers quoted physical units only: ``bbl``, ``gal``,
86
+ and ``mt``. Rate periods and currencies are outside this vocabulary.
87
+ """
88
+ if unit is None:
89
+ return None
90
+ text = str(unit).strip().lower()
91
+ if not text:
92
+ return None
93
+ return UNIT_MAP.get(text)
94
+
95
+
96
+ def quantity_unit_from_price_unit(price_unit: object) -> str | None:
97
+ """Return the denominator quantity unit from a price-unit string.
98
+
99
+ Examples:
100
+ ``USD/MT`` -> ``mt``
101
+ ``USc/GAL`` -> ``gal``
102
+ ``$/BBL`` -> ``bbl``
103
+
104
+ Bare quantity units are also accepted. Bare rate units such as
105
+ ``bbl/day`` return ``None`` because the denominator is a time period, not
106
+ a physical quantity.
107
+ """
108
+ if price_unit is None:
109
+ return None
110
+ text = str(price_unit).strip()
111
+ if not text:
112
+ return None
113
+ _, unit_text = split_currency_unit(text)
114
+ if "/" in unit_text:
115
+ _, _, unit_text = text.partition("/")
116
+ return canonical_quantity_unit(unit_text)
117
+
118
+
75
119
  # ---- Pint-token normalisation ----
76
120
 
77
121
 
@@ -89,6 +133,7 @@ def to_pint_token(unit: Optional[str]) -> Optional[str]:
89
133
  / ``CUBIC_METER`` / ``m3`` (case-insensitive exact match) -> ``m^3``.
90
134
  - Rate forms: ``m3/...`` / ``M3/...`` -> ``m^3/...``.
91
135
  - Energy casing: ``BTU`` -> ``Btu``; ``MMBTU`` -> ``MMBtu``.
136
+ - Pound casing/plurals: ``LBS`` / ``lbs`` -> ``lb``.
92
137
 
93
138
  Other tokens pass through unchanged. Aliases like ``barrel``, ``tonne``,
94
139
  ``gallon`` are not handled here -- they are registered as pint aliases
@@ -119,7 +164,15 @@ def to_pint_token(unit: Optional[str]) -> Optional[str]:
119
164
  u = "Btu"
120
165
  if u == "MMBTU":
121
166
  u = "MMBtu"
167
+ if u.upper() == "LBS":
168
+ u = "lb"
122
169
  return u
123
170
 
124
171
 
125
- __all__ = ["UNIT_MAP", "default_unit_for_commodity", "to_pint_token"]
172
+ __all__ = [
173
+ "UNIT_MAP",
174
+ "canonical_quantity_unit",
175
+ "default_unit_for_commodity",
176
+ "quantity_unit_from_price_unit",
177
+ "to_pint_token",
178
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commodutil
3
- Version: 4.2.0
3
+ Version: 4.3.2
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
@@ -206,6 +206,11 @@ def test_bug2_usc_to_usd_no_fx_required():
206
206
  assert out == pytest.approx(0.005, rel=1e-9)
207
207
 
208
208
 
209
+ def test_usc_per_pound_to_usd_per_pound_no_fx_required():
210
+ out = convfactors.convert_price(71.26, "USc/LBS", "USD/lb")
211
+ assert out == pytest.approx(0.7126, rel=1e-9)
212
+
213
+
209
214
  def test_bug2_gbp_pence_to_gbp_no_fx_required():
210
215
  """Bug 2: GBp -> GBP is a pure /100 scale (no FX)."""
211
216
  out = convfactors.convert_price(50.0, "GBp/therm", "GBP/therm")
@@ -7,6 +7,7 @@ from commodutil.standards.currency import (
7
7
  VALID_CURRENCY_TOKENS,
8
8
  fractional_to_major,
9
9
  is_fractional_currency,
10
+ normalize_currency_token,
10
11
  required_fx_pair,
11
12
  split_currency_unit,
12
13
  to_symbol,
@@ -40,6 +41,21 @@ def test_fractional_to_major():
40
41
  assert fractional_to_major("") == ""
41
42
 
42
43
 
44
+ def test_normalize_currency_token():
45
+ assert normalize_currency_token("USD") == "USD"
46
+ assert normalize_currency_token("usd") == "USD"
47
+ assert normalize_currency_token("$") == "$"
48
+ assert normalize_currency_token("USC") == "USc"
49
+ assert normalize_currency_token("U.S. cents") == "USc"
50
+ assert normalize_currency_token("GBX") == "GBp"
51
+ assert normalize_currency_token("pence") == "GBp"
52
+ assert normalize_currency_token("RMB") == "CNY"
53
+ assert normalize_currency_token("us dollars and cents") == "USD"
54
+ assert normalize_currency_token("bbl") is None
55
+ assert normalize_currency_token("") is None
56
+ assert normalize_currency_token(None) is None
57
+
58
+
43
59
  def test_fractional_currency_cac_auc_correctly_routed():
44
60
  # Regression guard for the pre-2026-05-14 bug where CAc/AUc were valid
45
61
  # currency tokens but had no fractional rule, so required_fx_pair
@@ -155,6 +171,7 @@ def test_legacy_convfactors_currency_shims_are_gone():
155
171
  "FRACTIONAL_TO_MAJOR",
156
172
  "fractional_to_major",
157
173
  "is_fractional_currency",
174
+ "normalize_currency_token",
158
175
  "split_currency_unit",
159
176
  "_split_currency_unit",
160
177
  "_FRACTIONAL_CURRENCY_DIVISORS",
@@ -1,6 +1,10 @@
1
1
  """Tests for commodutil.standards.units."""
2
2
 
3
- from commodutil.standards.units import default_unit_for_commodity
3
+ from commodutil.standards.units import (
4
+ canonical_quantity_unit,
5
+ default_unit_for_commodity,
6
+ quantity_unit_from_price_unit,
7
+ )
4
8
 
5
9
 
6
10
  def test_natgas_defaults_to_mmbtu():
@@ -62,11 +66,39 @@ def test_unit_map_metric_ton_variants():
62
66
  assert UNIT_MAP["tonnes"] == "mt"
63
67
 
64
68
 
65
- def test_unit_map_canonical_set_only_three():
66
- """UNIT_MAP normalises to exactly the canonical units bbl / gal / mt."""
69
+ def test_unit_map_pound_variants():
67
70
  from commodutil.standards.units import UNIT_MAP
68
71
 
69
- assert set(UNIT_MAP.values()) == {"bbl", "gal", "mt"}
72
+ assert UNIT_MAP["pound"] == "lb"
73
+ assert UNIT_MAP["pounds"] == "lb"
74
+ assert UNIT_MAP["lb"] == "lb"
75
+ assert UNIT_MAP["lbs"] == "lb"
76
+
77
+
78
+ def test_unit_map_canonical_set():
79
+ """UNIT_MAP normalises to the canonical physical quote units."""
80
+ from commodutil.standards.units import UNIT_MAP
81
+
82
+ assert set(UNIT_MAP.values()) == {"bbl", "gal", "mt", "lb"}
83
+
84
+
85
+ def test_canonical_quantity_unit_normalizes_common_labels():
86
+ assert canonical_quantity_unit("BBL") == "bbl"
87
+ assert canonical_quantity_unit("barrels") == "bbl"
88
+ assert canonical_quantity_unit("GAL") == "gal"
89
+ assert canonical_quantity_unit("Metric Tonnes") == "mt"
90
+ assert canonical_quantity_unit("mmbtu") is None
91
+ assert canonical_quantity_unit(None) is None
92
+
93
+
94
+ def test_quantity_unit_from_price_unit_uses_quote_denominator():
95
+ assert quantity_unit_from_price_unit("USD/MT") == "mt"
96
+ assert quantity_unit_from_price_unit("usd/mt") == "mt"
97
+ assert quantity_unit_from_price_unit("USc/GAL") == "gal"
98
+ assert quantity_unit_from_price_unit("USC/GAL") == "gal"
99
+ assert quantity_unit_from_price_unit("$/BBL") == "bbl"
100
+ assert quantity_unit_from_price_unit("BBL") == "bbl"
101
+ assert quantity_unit_from_price_unit("bbl/day") is None
70
102
 
71
103
 
72
104
  # ---- to_pint_token tests ----
@@ -97,6 +129,13 @@ def test_to_pint_token_energy_casing():
97
129
  assert to_pint_token("MMBTU") == "MMBtu"
98
130
 
99
131
 
132
+ def test_to_pint_token_pound_casing():
133
+ from commodutil.standards.units import to_pint_token
134
+
135
+ assert to_pint_token("LBS") == "lb"
136
+ assert to_pint_token("lbs") == "lb"
137
+
138
+
100
139
  def test_to_pint_token_whitespace_stripped():
101
140
  from commodutil.standards.units import to_pint_token
102
141
 
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