Coverage for utilities/conversion.py: 36%

24 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1from decimal import Decimal, InvalidOperation 

2 

3from django.utils.translation import gettext as _ 

4 

5from dcim.choices import CableLengthUnitChoices 

6from netbox.choices import ( 

7 DiameterUnitChoices, 

8 FlowRateUnitChoices, 

9 WeightUnitChoices, 

10) 

11 

12__all__ = ( 

13 'to_grams', 

14 'to_liters_per_minute', 

15 'to_meters', 

16 'to_millimeters', 

17) 

18 

19 

20def _normalized_measurement(value, unit, converters, quantity, precision=4) -> Decimal: 

21 """ 

22 Shared implementation for the unit-normalization helpers below. Coerce `value` to a non-negative 

23 Decimal, apply the matching per-unit converter, and round the result to `precision` decimal places. 

24 `converters` maps each valid unit to a callable receiving the Decimal value; `quantity` labels the 

25 value in error messages. Pass `precision=None` to return the unrounded result. 

26 """ 

27 try: 

28 value = Decimal(value) 

29 except (InvalidOperation, TypeError, ValueError): 

30 raise TypeError( 

31 _("Invalid value '{value}' for {quantity} (must be a number)").format(value=value, quantity=quantity) 

32 ) 

33 if value < 0: 

34 raise ValueError(_("Invalid value for {quantity}: must be a positive number").format(quantity=quantity)) 

35 if unit not in converters: 

36 raise ValueError( 

37 _("Unknown unit {unit}. Must be one of the following: {valid_units}").format( 

38 unit=unit, 

39 valid_units=', '.join(converters) 

40 ) 

41 ) 

42 result = converters[unit](value) 

43 return result if precision is None else round(result, precision) 

44 

45 

46def to_grams(weight, unit) -> int: 

47 """ 

48 Convert the given weight to integer grams. 

49 """ 

50 # Rounding is suppressed so that the result is truncated exactly as the caller's int() expects; rounding 

51 # first could nudge a value across an integer boundary. 

52 return int(_normalized_measurement(weight, unit, { 

53 WeightUnitChoices.UNIT_KILOGRAM: lambda v: v * 1000, 

54 WeightUnitChoices.UNIT_GRAM: lambda v: v, 

55 WeightUnitChoices.UNIT_POUND: lambda v: v * Decimal(453.592), 

56 WeightUnitChoices.UNIT_OUNCE: lambda v: v * Decimal(28.3495), 

57 }, _('weight'), precision=None)) 

58 

59 

60def to_meters(length, unit) -> Decimal: 

61 """ 

62 Convert the given length to meters, returning a Decimal value. 

63 """ 

64 return _normalized_measurement(length, unit, { 

65 CableLengthUnitChoices.UNIT_KILOMETER: lambda v: v * 1000, 

66 CableLengthUnitChoices.UNIT_METER: lambda v: v, 

67 CableLengthUnitChoices.UNIT_CENTIMETER: lambda v: v / 100, 

68 CableLengthUnitChoices.UNIT_MILE: lambda v: v * Decimal(1609.344), 

69 CableLengthUnitChoices.UNIT_FOOT: lambda v: v * Decimal(0.3048), 

70 CableLengthUnitChoices.UNIT_INCH: lambda v: v * Decimal(0.0254), 

71 }, _('length')) 

72 

73 

74def to_millimeters(diameter, unit) -> Decimal: 

75 """ 

76 Convert the given diameter to millimeters, returning a Decimal value. 

77 """ 

78 return _normalized_measurement(diameter, unit, { 

79 DiameterUnitChoices.UNIT_MILLIMETER: lambda v: v, 

80 DiameterUnitChoices.UNIT_CENTIMETER: lambda v: v * 10, 

81 DiameterUnitChoices.UNIT_INCH: lambda v: v * Decimal('25.4'), 

82 }, _('diameter')) 

83 

84 

85def to_liters_per_minute(flow_rate, unit) -> Decimal: 

86 """ 

87 Convert the given flow rate to liters per minute, returning a Decimal value. 

88 """ 

89 return _normalized_measurement(flow_rate, unit, { 

90 FlowRateUnitChoices.UNIT_LITERS_PER_MINUTE: lambda v: v, 

91 FlowRateUnitChoices.UNIT_CUBIC_METERS_PER_HOUR: lambda v: v * Decimal(1000) / Decimal(60), 

92 FlowRateUnitChoices.UNIT_GALLONS_PER_MINUTE: lambda v: v * Decimal('3.785411784'), 

93 }, _('flow rate'))