utils.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import click
  2. import marshmallow as ma
  3. from click import get_current_context
  4. from flask.cli import with_appcontext as with_cli_appcontext
  5. from pint import DefinitionSyntaxError, DimensionalityError, UndefinedUnitError
  6. from flexmeasures.utils.unit_utils import to_preferred, ur
  7. class MarshmallowClickMixin(click.ParamType):
  8. def __init__(self, *args, **kwargs):
  9. super().__init__(*args, **kwargs)
  10. self.name = self.__class__.__name__
  11. def get_metavar(self, param, **kwargs):
  12. return self.__class__.__name__
  13. def convert(self, value, param, ctx, **kwargs):
  14. try:
  15. return self.deserialize(value, **kwargs)
  16. except ma.exceptions.ValidationError as e:
  17. raise click.exceptions.BadParameter(e, ctx=ctx, param=param)
  18. class FMValidationError(ma.exceptions.ValidationError):
  19. """
  20. Custom validation error class.
  21. It differs from the classic validation error by having two
  22. attributes, according to the USEF 2015 reference implementation.
  23. Subclasses of this error might adjust the `status` attribute accordingly.
  24. """
  25. result = "Rejected"
  26. status = "UNPROCESSABLE_ENTITY"
  27. def with_appcontext_if_needed():
  28. """Execute within the script's application context, in case there is one.
  29. An exception is `flexmeasures run`, which has a click context at the time the decorator is called,
  30. but no longer has a click context at the time the decorated function is called,
  31. which, typically, is a request to the running FlexMeasures server.
  32. """
  33. def decorator(f):
  34. ctx = get_current_context(silent=True)
  35. if ctx and not ctx.invoked_subcommand == "run":
  36. return with_cli_appcontext(f)
  37. return f
  38. return decorator
  39. def convert_to_quantity(value: str, to_unit: str) -> ur.Quantity:
  40. """Convert value to quantity in the given unit.
  41. :param value: Value to convert.
  42. :param to_unit: Unit to convert to. If the unit starts with a '/',
  43. the value can have any unit, and the unit is used as the denominator.
  44. :returns: Quantity in the desired unit.
  45. """
  46. if to_unit.startswith("/") and len(to_unit) < 2:
  47. raise ValueError(f"Variable `to_unit='{to_unit}'` must define a denominator.")
  48. try:
  49. if to_unit.startswith("/"):
  50. return to_preferred(
  51. ur.Quantity(value) * ur.Quantity(to_unit[1:])
  52. ) / ur.Quantity(to_unit[1:])
  53. return ur.Quantity(value).to(ur.Quantity(to_unit))
  54. except DimensionalityError as e:
  55. raise FMValidationError(f"Cannot convert value `{value}` to '{to_unit}'") from e
  56. except (AssertionError, DefinitionSyntaxError, UndefinedUnitError) as e:
  57. raise FMValidationError(
  58. f"Cannot convert value '{value}' to a valid quantity. {e}"
  59. )