Skip to content

Commit

Permalink
CLI Text Styles and pre-commit configuration (#609)
Browse files Browse the repository at this point in the history
* Removing language_version from pre-commit config. Now it will take the system installed version. Issue #608

Signed-off-by: victor <victor@seita.nl>

* Text styles for the different events that can occur using the CLI: success, warn, error. The different styles, which are nothing but attributes of the function ,  are stored as dictionaries.

Signed-off-by: victor <victor@seita.nl>

* Changing deprecated command in the documentation to the new one.

Signed-off-by: victor <victor@seita.nl>

* Adding changelog record

Signed-off-by: victor <victor@seita.nl>

* Harmonising click.Abort exceptions calling . Changing changelog.rst.

Signed-off-by: victor <victor@seita.nl>

* Raising the exception properly.

Signed-off-by: victor <victor@seita.nl>

---------

Signed-off-by: victor <victor@seita.nl>
  • Loading branch information
victorgarcia98 committed Mar 22, 2023
1 parent 9494676 commit 9ecc85f
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 131 deletions.
2 changes: 0 additions & 2 deletions .pre-commit-config.yaml
Expand Up @@ -4,13 +4,11 @@ repos:
hooks:
- id: flake8
name: flake8 (code linting)
language_version: python3.9
- repo: https://github.com/psf/black
rev: 22.10.0 # New version tags can be found here: https://github.com/psf/black/tags
hooks:
- id: black
name: black (code formatting)
language_version: python3.9
- repo: local
hooks:
- id: mypy
Expand Down
3 changes: 2 additions & 1 deletion documentation/changelog.rst
Expand Up @@ -3,7 +3,7 @@
FlexMeasures Changelog
**********************

v0.13.0 | February XX, 2023
v0.13.0 | April XX, 2023
============================

.. warning:: The API endpoint (`[POST] /sensors/(id)/schedules/trigger <api/v3_0.html#post--api-v3_0-sensors-(id)-schedules-trigger>`_) to make new schedules sunsets the deprecated (since v0.12) storage flexibility parameters (they move to the ``flex-model`` parameter group), as well as the parameters describing other sensors (they move to ``flex-context``).
Expand All @@ -12,6 +12,7 @@ New features
-------------
* Keyboard control over replay [see `PR #562 <https://www.github.com/FlexMeasures/flexmeasures/pull/562>`_]
* The ``FLEXMEASURES_MAX_PLANNING_HORIZON`` config setting can also be set as an integer number of planning steps rather than just as a fixed duration, which makes it possible to schedule further ahead in coarser time steps [see `PR #583 <https://www.github.com/FlexMeasures/flexmeasures/pull/583>`_]
* Different text styles for CLI output for errors, warnings or success messages. [see `PR #609 <https://www.github.com/FlexMeasures/flexmeasures/pull/609>`_]

Bugfixes
-----------
Expand Down
2 changes: 1 addition & 1 deletion documentation/host/data.rst
Expand Up @@ -199,7 +199,7 @@ First, you can get the database structure with:
.. note:: If you develop code (and might want to make changes to the data model), you should also check out the maintenance section about database migrations.

You can create users with the ``new-user`` command. Check it out:
You can create users with the ``add user`` command. Check it out:

.. code-block:: console
Expand Down
159 changes: 103 additions & 56 deletions flexmeasures/cli/data_add.py
Expand Up @@ -18,7 +18,7 @@
import timely_beliefs.utils as tb_utils
from workalendar.registry import registry as workalendar_registry

from flexmeasures.cli.utils import DeprecatedDefaultGroup
from flexmeasures.cli.utils import DeprecatedDefaultGroup, MsgStyle
from flexmeasures.data import db
from flexmeasures.data.scripts.data_gen import (
add_transmission_zone_asset,
Expand Down Expand Up @@ -77,12 +77,15 @@ def new_account_role(name: str, description: str):
"""
role = AccountRole.query.filter_by(name=name).one_or_none()
if role is not None:
click.echo(f"Account role '{name}' already exists.")
raise click.Abort
click.secho(f"Account role '{name}' already exists.", **MsgStyle.ERROR)
raise click.Abort()
role = AccountRole(name=name, description=description)
db.session.add(role)
db.session.commit()
print(f"Account role '{name}' (ID: {role.id}) successfully created.")
click.secho(
f"Account role '{name}' (ID: {role.id}) successfully created.",
**MsgStyle.SUCCESS,
)


@fm_add_data.command("account")
Expand All @@ -95,21 +98,24 @@ def new_account(name: str, roles: str):
"""
account = db.session.query(Account).filter_by(name=name).one_or_none()
if account is not None:
click.echo(f"Account '{name}' already exists.")
raise click.Abort
click.secho(f"Account '{name}' already exists.", **MsgStyle.ERROR)
raise click.Abort()
account = Account(name=name)
db.session.add(account)
if roles:
for role_name in roles.split(","):
role = AccountRole.query.filter_by(name=role_name).one_or_none()
if role is None:
print(f"Adding account role {role_name} ...")
click.secho(f"Adding account role {role_name} ...", **MsgStyle.ERROR)
role = AccountRole(name=role_name)
db.session.add(role)
db.session.flush()
db.session.add(RolesAccounts(role_id=role.id, account_id=account.id))
db.session.commit()
print(f"Account '{name}' (ID: {account.id}) successfully created.")
click.secho(
f"Account '{name}' (ID: {account.id}) successfully created.",
**MsgStyle.SUCCESS,
)


@fm_add_data.command("user")
Expand Down Expand Up @@ -138,25 +144,26 @@ def new_user(
"""
if timezone_optional is None:
timezone = app.config.get("FLEXMEASURES_TIMEZONE", "UTC")
print(
f"Setting user timezone to {timezone} (taken from FLEXMEASURES_TIMEZONE config setting)..."
click.secho(
f"Setting user timezone to {timezone} (taken from FLEXMEASURES_TIMEZONE config setting)...",
**MsgStyle.WARN,
)
else:
timezone = timezone_optional
try:
pytz.timezone(timezone)
except pytz.UnknownTimeZoneError:
print(f"Timezone {timezone} is unknown!")
raise click.Abort
click.secho(f"Timezone {timezone} is unknown!", **MsgStyle.ERROR)
raise click.Abort()
account = db.session.query(Account).get(account_id)
if account is None:
print(f"No account with ID {account_id} found!")
raise click.Abort
click.secho(f"No account with ID {account_id} found!", **MsgStyle.ERROR)
raise click.Abort()
pwd1 = getpass.getpass(prompt="Please enter the password:")
pwd2 = getpass.getpass(prompt="Please repeat the password:")
if pwd1 != pwd2:
print("Passwords do not match!")
raise click.Abort
click.secho("Passwords do not match!", **MsgStyle.ERROR)
raise click.Abort()
created_user = create_user(
username=username,
email=email,
Expand All @@ -167,7 +174,7 @@ def new_user(
check_email_deliverability=False,
)
db.session.commit()
print(f"Successfully created user {created_user}")
click.secho(f"Successfully created user {created_user}", **MsgStyle.SUCCESS)


@fm_add_data.command("sensor")
Expand Down Expand Up @@ -205,24 +212,33 @@ def add_sensor(**args):
try:
attributes = json.loads(args["attributes"])
except json.decoder.JSONDecodeError as jde:
print(f"Error decoding --attributes. Please check your JSON: {jde}")
click.secho(
f"Error decoding --attributes. Please check your JSON: {jde}",
**MsgStyle.ERROR,
)
raise click.Abort()
del args["attributes"] # not part of schema
check_errors(SensorSchema().validate(args))
args["event_resolution"] = timedelta(minutes=args["event_resolution"])
sensor = Sensor(**args)
if not isinstance(attributes, dict):
print("Attributes should be a dict.")
click.secho("Attributes should be a dict.", **MsgStyle.ERROR)
raise click.Abort()
sensor.attributes = attributes
if sensor.measures_power:
if "capacity_in_mw" not in sensor.attributes:
print("A sensor which measures power needs a capacity (see --attributes).")
raise click.Abort
click.secho(
"A sensor which measures power needs a capacity (see --attributes).",
**MsgStyle.ERROR,
)
raise click.Abort()
db.session.add(sensor)
db.session.commit()
print(f"Successfully created sensor with ID {sensor.id}")
print(f"You can access it at its entity address {sensor.entity_address}")
click.secho(f"Successfully created sensor with ID {sensor.id}", **MsgStyle.SUCCESS)
click.secho(
f"You can access it at its entity address {sensor.entity_address}",
**MsgStyle.SUCCESS,
)


@fm_add_data.command("asset-type")
Expand All @@ -239,8 +255,11 @@ def add_asset_type(**args):
generic_asset_type = GenericAssetType(**args)
db.session.add(generic_asset_type)
db.session.commit()
print(f"Successfully created asset type with ID {generic_asset_type.id}.")
print("You can now assign assets to it.")
click.secho(
f"Successfully created asset type with ID {generic_asset_type.id}.",
**MsgStyle.SUCCESS,
)
click.secho("You can now assign assets to it.", **MsgStyle.SUCCESS)


@fm_add_data.command("asset")
Expand Down Expand Up @@ -270,8 +289,10 @@ def add_asset(**args):
generic_asset = GenericAsset(**args)
db.session.add(generic_asset)
db.session.commit()
print(f"Successfully created asset with ID {generic_asset.id}.")
print("You can now assign sensors to it.")
click.secho(
f"Successfully created asset with ID {generic_asset.id}.", **MsgStyle.SUCCESS
)
click.secho("You can now assign sensors to it.", **MsgStyle.SUCCESS)


@fm_add_data.command("initial-structure")
Expand Down Expand Up @@ -508,8 +529,11 @@ def add_beliefs(
)
duplicate_rows = bdf.index.duplicated(keep="first")
if any(duplicate_rows) > 0:
print("Duplicates found. Dropping duplicates for the following records:")
print(bdf[duplicate_rows])
click.secho(
"Duplicates found. Dropping duplicates for the following records:",
**MsgStyle.WARN,
)
click.secho(bdf[duplicate_rows], **MsgStyle.WARN)
bdf = bdf[~duplicate_rows]
if unit is not None:
bdf["event_value"] = convert_units(
Expand All @@ -526,12 +550,18 @@ def add_beliefs(
bulk_save_objects=True,
commit_transaction=True,
)
print(f"Successfully created beliefs\n{bdf}")
click.secho(f"Successfully created beliefs\n{bdf}", **MsgStyle.SUCCESS)
except IntegrityError as e:
db.session.rollback()
print(f"Failed to create beliefs due to the following error: {e.orig}")
click.secho(
f"Failed to create beliefs due to the following error: {e.orig}",
**MsgStyle.ERROR,
)
if not allow_overwrite:
print("As a possible workaround, use the --allow-overwrite flag.")
click.secho(
"As a possible workaround, use the --allow-overwrite flag.",
**MsgStyle.ERROR,
)


@fm_add_data.command("annotation")
Expand Down Expand Up @@ -635,7 +665,7 @@ def add_annotation(
for sensor in sensors:
sensor.annotations.append(annotation)
db.session.commit()
print("Successfully added annotation.")
click.secho("Successfully added annotation.", **MsgStyle.SUCCESS)


@fm_add_data.command("holidays")
Expand Down Expand Up @@ -715,8 +745,9 @@ def add_holidays(
for asset in assets:
asset.annotations += annotations
db.session.commit()
print(
f"Successfully added holidays to {len(accounts)} {flexmeasures_inflection.pluralize('account', len(accounts))} and {len(assets)} {flexmeasures_inflection.pluralize('asset', len(assets))}:\n{num_holidays}"
click.secho(
f"Successfully added holidays to {len(accounts)} {flexmeasures_inflection.pluralize('account', len(accounts))} and {len(assets)} {flexmeasures_inflection.pluralize('asset', len(assets))}:\n{num_holidays}",
**MsgStyle.SUCCESS,
)


Expand Down Expand Up @@ -808,7 +839,10 @@ def create_forecasts(
end_of_roll=forecast_end - horizon,
)
num_jobs += len(jobs)
print(f"{num_jobs} new forecasting job(s) added to the queue.")
click.secho(
f"{num_jobs} new forecasting job(s) added to the queue.",
**MsgStyle.SUCCESS,
)
else:
from flexmeasures.data.scripts.data_gen import populate_time_series_forecasts

Expand Down Expand Up @@ -967,7 +1001,10 @@ def add_schedule_for_storage(

# Parse input and required sensor attributes
if not power_sensor.measures_power:
click.echo(f"Sensor with ID {power_sensor.id} is not a power sensor.")
click.secho(
f"Sensor with ID {power_sensor.id} is not a power sensor.",
**MsgStyle.ERROR,
)
raise click.Abort()
if production_price_sensor is None:
production_price_sensor = consumption_price_sensor
Expand All @@ -977,7 +1014,9 @@ def add_schedule_for_storage(
try:
check_required_attributes(power_sensor, [("max_soc_in_mwh", float)])
except MissingAttributeException:
click.echo(f"Sensor {power_sensor} has no max_soc_in_mwh attribute.")
click.secho(
f"Sensor {power_sensor} has no max_soc_in_mwh attribute.", **MsgStyle.ERROR
)
raise click.Abort()
capacity_str = f"{power_sensor.get_attribute('max_soc_in_mwh')} MWh"
soc_at_start = convert_units(soc_at_start.magnitude, soc_at_start.units, "MWh", capacity=capacity_str) # type: ignore
Expand Down Expand Up @@ -1020,11 +1059,14 @@ def add_schedule_for_storage(
if as_job:
job = create_scheduling_job(sensor=power_sensor, **scheduling_kwargs)
if job:
print(f"New scheduling job {job.id} has been added to the queue.")
click.secho(
f"New scheduling job {job.id} has been added to the queue.",
**MsgStyle.SUCCESS,
)
else:
success = make_schedule(sensor_id=power_sensor.id, **scheduling_kwargs)
if success:
print("New schedule is stored.")
click.secho("New schedule is stored.", **MsgStyle.SUCCESS)


@fm_add_data.command("toy-account")
Expand All @@ -1047,13 +1089,14 @@ def add_toy_account(kind: str, name: str):
account = Account.query.filter(Account.name == name).one_or_none()
if account:
click.echo(f"Account {name} already exists.")
return
raise click.Abort()
# make an account user (account-admin?)
email = "toy-user@flexmeasures.io"
user = User.query.filter_by(email=email).one_or_none()
if user is not None:
click.echo(
f"User with email {email} already exists in account {user.account.name}."
click.secho(
f"User with email {email} already exists in account {user.account.name}.",
**MsgStyle.ERROR,
)
else:
user = create_user(
Expand Down Expand Up @@ -1117,14 +1160,17 @@ def add_toy_account(kind: str, name: str):
]
db.session.commit()

click.echo(
f"Toy account {name} with user {user.email} created successfully. You might want to run `flexmeasures show account --id {user.account.id}`"
click.secho(
f"Toy account {name} with user {user.email} created successfully. You might want to run `flexmeasures show account --id {user.account.id}`",
**MsgStyle.SUCCESS,
)
click.echo(
f"The sensor for battery discharging is {charging_sensor} (ID: {charging_sensor.id})."
click.secho(
f"The sensor for battery discharging is {charging_sensor} (ID: {charging_sensor.id}).",
**MsgStyle.SUCCESS,
)
click.echo(
f"The sensor for Day ahead prices is {day_ahead_sensor} (ID: {day_ahead_sensor.id})."
click.secho(
f"The sensor for Day ahead prices is {day_ahead_sensor} (ID: {day_ahead_sensor.id}).",
**MsgStyle.SUCCESS,
)


Expand All @@ -1135,24 +1181,25 @@ def check_timezone(timezone):
try:
pytz.timezone(timezone)
except pytz.UnknownTimeZoneError:
print("Timezone %s is unknown!" % timezone)
raise click.Abort
click.secho("Timezone %s is unknown!" % timezone, **MsgStyle.ERROR)
raise click.Abort()


def check_errors(errors: Dict[str, List[str]]):
if errors:
print(
f"Please correct the following errors:\n{errors}.\n Use the --help flag to learn more."
click.secho(
f"Please correct the following errors:\n{errors}.\n Use the --help flag to learn more.",
**MsgStyle.ERROR,
)
raise click.Abort
raise click.Abort()


def parse_source(source):
if source.isdigit():
_source = get_source_or_none(int(source))
if not _source:
print(f"Failed to find source {source}.")
return
click.secho(f"Failed to find source {source}.", **MsgStyle.ERROR)
raise click.Abort()
else:
_source = get_or_create_source(source, source_type="CLI script")
return _source

0 comments on commit 9ecc85f

Please sign in to comment.