diff --git a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/generator_set.py b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/generator_set.py new file mode 100644 index 0000000000..b13f9c2d3e --- /dev/null +++ b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/generator_set.py @@ -0,0 +1,50 @@ +from typing import Any, Dict, List, Type + +from libecalc.dto.base import ConsumerUserDefinedCategoryType +from libecalc.input.yaml_types import YamlBase +from libecalc.input.yaml_types.components.category import CategoryField +from libecalc.input.yaml_types.components.legacy.yaml_electricity_consumer import ( + YamlElectricityConsumer, +) +from libecalc.input.yaml_types.placeholder_type import PlaceholderType +from libecalc.input.yaml_types.schema_helpers import ( + replace_temporal_placeholder_property_with_legacy_ref, +) +from libecalc.input.yaml_types.temporal_model import TemporalModel +from pydantic import Field + + +class YamlGeneratorSet(YamlBase): + class Config: + title = "GENERATORSET" + + @staticmethod + def schema_extra(schema: Dict[str, Any], model: Type["YamlGeneratorSet"]) -> None: + replace_temporal_placeholder_property_with_legacy_ref( + schema=schema, + property_key="ELECTRICITY2FUEL", + property_ref="$SERVER_NAME/api/v1/schema-validation/energy-usage-model-common.json#definitions/number_or_string", + ) + + name: str = Field( + ..., + title="NAME", + description="Name of the generator set.\n\n$ECALC_DOCS_KEYWORDS_URL/NAME", + ) + category: ConsumerUserDefinedCategoryType = CategoryField(...) + fuel: TemporalModel[str] = Field( + None, + title="FUEL", + description="The fuel used by the generator set." "\n\n$ECALC_DOCS_KEYWORDS_URL/FUEL", + ) + electricity2fuel: TemporalModel[PlaceholderType] = Field( + ..., + title="ELECTRICITY2FUEL", + description="Specifies the correlation between the electric power delivered and the fuel burned by a " + "generator set.\n\n$ECALC_DOCS_KEYWORDS_URL/ELECTRICITY2FUEL", + ) + consumers: List[YamlElectricityConsumer] = Field( + ..., + title="CONSUMERS", + description="Consumers getting electrical power from the generator set.\n\n$ECALC_DOCS_KEYWORDS_URL/CONSUMERS", + ) diff --git a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/installation.py b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/installation.py index 69b7980390..aa1537fa72 100644 --- a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/installation.py +++ b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/installation.py @@ -5,6 +5,7 @@ from libecalc.input.yaml_types import YamlBase from libecalc.input.yaml_types.components.category import CategoryField from libecalc.input.yaml_types.components.compressor_system import CompressorSystem +from libecalc.input.yaml_types.components.generator_set import YamlGeneratorSet from libecalc.input.yaml_types.components.legacy.yaml_fuel_consumer import ( YamlFuelConsumer, ) @@ -23,11 +24,6 @@ class Config: @staticmethod def schema_extra(schema: Dict[str, Any], model: Type["YamlInstallation"]) -> None: - replace_placeholder_property_with_legacy_ref( - schema=schema, - property_key="GENERATORSETS", - property_ref="$SERVER_NAME/api/v1/schema-validation/generator-sets.json#properties/GENERATORSETS", - ) replace_placeholder_property_with_legacy_ref( schema=schema, property_key="DIRECTEMITTERS", @@ -55,7 +51,7 @@ def schema_extra(schema: Dict[str, Any], model: Type["YamlInstallation"]) -> Non title="REGULARITY", description="Regularity of the installation can be specified by a single number or as an expression. USE WITH CARE.\n\n$ECALC_DOCS_KEYWORDS_URL/REGULARITY", ) - generatorsets: PlaceholderType = Field( + generatorsets: List[YamlGeneratorSet] = Field( None, title="GENERATORSETS", description="Defines one or more generator sets.\n\n$ECALC_DOCS_KEYWORDS_URL/GENERATORSETS", diff --git a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/legacy/yaml_electricity_consumer.py b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/legacy/yaml_electricity_consumer.py new file mode 100644 index 0000000000..913e1e514a --- /dev/null +++ b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/legacy/yaml_electricity_consumer.py @@ -0,0 +1,37 @@ +from typing import Any, Dict, Type + +from libecalc.dto.base import ConsumerUserDefinedCategoryType +from libecalc.input.yaml_types import YamlBase +from libecalc.input.yaml_types.components.category import CategoryField +from libecalc.input.yaml_types.placeholder_type import PlaceholderType +from libecalc.input.yaml_types.schema_helpers import ( + replace_temporal_placeholder_property_with_legacy_ref, +) +from libecalc.input.yaml_types.temporal_model import TemporalModel +from pydantic import Field + + +class YamlElectricityConsumer(YamlBase): + class Config: + title = "ELECTRICITY_CONSUMER" + + @staticmethod + def schema_extra(schema: Dict[str, Any], model: Type["YamlElectricityConsumer"]) -> None: + replace_temporal_placeholder_property_with_legacy_ref( + schema=schema, + property_key="ENERGY_USAGE_MODEL", + property_ref="$SERVER_NAME/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL", + ) + + name: str = Field( + ..., + title="NAME", + description="Name of the consumer.\n\n$ECALC_DOCS_KEYWORDS_URL/NAME", + ) + category: ConsumerUserDefinedCategoryType = CategoryField(...) + energy_usage_model: TemporalModel[PlaceholderType] = Field( + ..., + title="ENERGY_USAGE_MODEL", + description="Definition of the energy usage model for the consumer." + "\n\n$ECALC_DOCS_KEYWORDS_URL/ENERGY_USAGE_MODEL", + ) diff --git a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/legacy/yaml_fuel_consumer.py b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/legacy/yaml_fuel_consumer.py index 2f3b578611..830d506ecb 100644 --- a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/legacy/yaml_fuel_consumer.py +++ b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/components/legacy/yaml_fuel_consumer.py @@ -1,37 +1,14 @@ -from typing import Any, Dict, Type - -from libecalc.dto.base import ConsumerUserDefinedCategoryType -from libecalc.input.yaml_types import YamlBase -from libecalc.input.yaml_types.components.category import CategoryField -from libecalc.input.yaml_types.placeholder_type import PlaceholderType +from libecalc.input.yaml_types.components.legacy.yaml_electricity_consumer import ( + YamlElectricityConsumer, +) from libecalc.input.yaml_types.temporal_model import TemporalModel from pydantic import Field -class YamlFuelConsumer(YamlBase): +class YamlFuelConsumer(YamlElectricityConsumer): class Config: title = "FUEL_CONSUMER" - @staticmethod - def schema_extra(schema: Dict[str, Any], model: Type["YamlFuelConsumer"]) -> None: - for energy_usage_model in schema["properties"]["ENERGY_USAGE_MODEL"]["anyOf"]: - del energy_usage_model["type"] - energy_usage_model[ - "$ref" - ] = "$SERVER_NAME/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" - - name: str = Field( - ..., - title="NAME", - description="Name of the fuel consumer.\n\n$ECALC_DOCS_KEYWORDS_URL/NAME", - ) - category: ConsumerUserDefinedCategoryType = CategoryField(...) - energy_usage_model: TemporalModel[PlaceholderType] = Field( - ..., - title="ENERGY_USAGE_MODEL", - description="Definition of the energy usage model for the consumer." - "\n\n$ECALC_DOCS_KEYWORDS_URL/ENERGY_USAGE_MODEL", - ) fuel: TemporalModel[str] = Field( None, title="FUEL", diff --git a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/schema_helpers.py b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/schema_helpers.py index d38cebd402..208de43ff0 100644 --- a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/schema_helpers.py +++ b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/schema_helpers.py @@ -2,7 +2,7 @@ def replace_placeholder_property_with_legacy_ref(schema: dict, property_key: str """ Replace a property with a $ref to an existing json-schema file. This method exists so that we can gradually replace all json-schema files with pydantic classes that can generate the schema. Instead of converting all schemas at once, - we can use this method to inject the old schemas for some of the properties in the class we are converting. + we can use this method to inject the old schemas for some properties in the class we are converting. This method will delete the type property, and inject the given $ref. @@ -13,3 +13,17 @@ def replace_placeholder_property_with_legacy_ref(schema: dict, property_key: str """ del schema["properties"][property_key]["type"] schema["properties"][property_key]["$ref"] = property_ref + + +def replace_temporal_placeholder_property_with_legacy_ref(schema: dict, property_key: str, property_ref: str) -> None: + for value in schema["properties"][property_key]["anyOf"]: + if "additionalProperties" in value: + # Replace type in additional properties (temporal) + # This will also replace for patternProperties since it is the same object used for both. + additional_properties = value["additionalProperties"] + del additional_properties["type"] + additional_properties["$ref"] = property_ref + else: + # Replace type when not temporal + del value["type"] + value["$ref"] = property_ref diff --git a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/temporal_model.py b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/temporal_model.py index 429819ca19..519ef02bdc 100644 --- a/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/temporal_model.py +++ b/src/ecalc/libraries/libecalc/common/libecalc/input/yaml_types/temporal_model.py @@ -1,5 +1,11 @@ -from datetime import datetime -from typing import MutableMapping, TypeVar, Union +from typing import Dict, TypeVar, Union + +from pydantic import ConstrainedStr + + +class DatetimeString(ConstrainedStr): + regex = "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$" + TModel = TypeVar("TModel") -TemporalModel = Union[TModel, MutableMapping[datetime, TModel]] +TemporalModel = Union[TModel, Dict[DatetimeString, TModel]] diff --git a/src/ecalc/libraries/libecalc/common/tests/input/validation/snapshots/test_validation_json_schemas/test_json_schema_changed/schemas.json b/src/ecalc/libraries/libecalc/common/tests/input/validation/snapshots/test_validation_json_schemas/test_json_schema_changed/schemas.json index cf207f4f12..73c46ededd 100644 --- a/src/ecalc/libraries/libecalc/common/tests/input/validation/snapshots/test_validation_json_schemas/test_json_schema_changed/schemas.json +++ b/src/ecalc/libraries/libecalc/common/tests/input/validation/snapshots/test_validation_json_schemas/test_json_schema_changed/schemas.json @@ -23,6 +23,11 @@ "additionalProperties": { "type": "string" }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "type": "string" + } + }, "type": "object" } ], @@ -76,6 +81,14 @@ }, "type": "array" }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "items": { + "$ref": "#/definitions/libecalc__input__yaml_types__components__compressor_system__OperationalSettings" + }, + "type": "array" + } + }, "type": "object" } ], @@ -149,6 +162,11 @@ "additionalProperties": { "type": "string" }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "type": "string" + } + }, "type": "object" } ], @@ -202,6 +220,14 @@ }, "type": "array" }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "items": { + "$ref": "#/definitions/libecalc__input__yaml_types__components__pump_system__OperationalSettings" + }, + "type": "array" + } + }, "type": "object" } ], @@ -255,6 +281,52 @@ "title": "SingleVariable", "type": "object" }, + "YamlElectricityConsumer": { + "additionalProperties": false, + "properties": { + "CATEGORY": { + "allOf": [ + { + "$ref": "#/definitions/ConsumerUserDefinedCategoryType" + } + ], + "description": "Output category/tag.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/CATEGORY", + "title": "CATEGORY" + }, + "ENERGY_USAGE_MODEL": { + "anyOf": [ + { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" + }, + { + "additionalProperties": { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" + }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" + } + }, + "type": "object" + } + ], + "description": "Definition of the energy usage model for the consumer.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/ENERGY_USAGE_MODEL", + "title": "ENERGY_USAGE_MODEL" + }, + "NAME": { + "description": "Name of the consumer.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/NAME", + "title": "NAME", + "type": "string" + } + }, + "required": [ + "NAME", + "CATEGORY", + "ENERGY_USAGE_MODEL" + ], + "title": "ELECTRICITY_CONSUMER", + "type": "object" + }, "YamlFuelConsumer": { "additionalProperties": false, "properties": { @@ -273,10 +345,15 @@ "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" }, { - "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL", "additionalProperties": { - "type": "object" - } + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" + }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" + } + }, + "type": "object" } ], "description": "Definition of the energy usage model for the consumer.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/ENERGY_USAGE_MODEL", @@ -291,6 +368,11 @@ "additionalProperties": { "type": "string" }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "type": "string" + } + }, "type": "object" } ], @@ -298,7 +380,7 @@ "title": "FUEL" }, "NAME": { - "description": "Name of the fuel consumer.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/NAME", + "description": "Name of the consumer.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/NAME", "title": "NAME", "type": "string" } @@ -311,6 +393,81 @@ "title": "FUEL_CONSUMER", "type": "object" }, + "YamlGeneratorSet": { + "additionalProperties": false, + "properties": { + "CATEGORY": { + "allOf": [ + { + "$ref": "#/definitions/ConsumerUserDefinedCategoryType" + } + ], + "description": "Output category/tag.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/CATEGORY", + "title": "CATEGORY" + }, + "CONSUMERS": { + "description": "Consumers getting electrical power from the generator set.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/CONSUMERS", + "items": { + "$ref": "#/definitions/YamlElectricityConsumer" + }, + "title": "CONSUMERS", + "type": "array" + }, + "ELECTRICITY2FUEL": { + "anyOf": [ + { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model-common.json#definitions/number_or_string" + }, + { + "additionalProperties": { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model-common.json#definitions/number_or_string" + }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model-common.json#definitions/number_or_string" + } + }, + "type": "object" + } + ], + "description": "Specifies the correlation between the electric power delivered and the fuel burned by a generator set.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/ELECTRICITY2FUEL", + "title": "ELECTRICITY2FUEL" + }, + "FUEL": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": { + "type": "string" + }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "type": "string" + } + }, + "type": "object" + } + ], + "description": "The fuel used by the generator set.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/FUEL", + "title": "FUEL" + }, + "NAME": { + "description": "Name of the generator set.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/NAME", + "title": "NAME", + "type": "string" + } + }, + "required": [ + "NAME", + "CATEGORY", + "ELECTRICITY2FUEL", + "CONSUMERS" + ], + "title": "GENERATORSET", + "type": "object" + }, "YamlInstallation": { "additionalProperties": false, "properties": { @@ -337,6 +494,11 @@ "additionalProperties": { "type": "string" }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "type": "string" + } + }, "type": "object" } ], @@ -362,9 +524,12 @@ "type": "array" }, "GENERATORSETS": { - "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/generator-sets.json#properties/GENERATORSETS", "description": "Defines one or more generator sets.\n\nhttps://test.ecalc.equinor.com/docs/docs/modelling/keywords/GENERATORSETS", - "title": "GENERATORSETS" + "items": { + "$ref": "#/definitions/YamlGeneratorSet" + }, + "title": "GENERATORSETS", + "type": "array" }, "HCEXPORT": { "anyOf": [ @@ -407,6 +572,27 @@ ], "title": "Expression" }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + }, + { + "pattern": "^[\\w * ^ . : ; () {} = > < + \\- /]+$", + "type": "string" + } + ], + "examples": [ + "SIM1;OIL_PROD {+} 1000", + 5 + ], + "title": "Expression" + } + }, "type": "object" } ], @@ -459,6 +645,27 @@ ], "title": "Expression" }, + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + }, + { + "pattern": "^[\\w * ^ . : ; () {} = > < + \\- /]+$", + "type": "string" + } + ], + "examples": [ + "SIM1;OIL_PROD {+} 1000", + 5 + ], + "title": "Expression" + } + }, "type": "object" } ], diff --git a/src/ecalc/libraries/libecalc/common/tests/input/validation/test_validation_json_schemas.py b/src/ecalc/libraries/libecalc/common/tests/input/validation/test_validation_json_schemas.py index efbcdb32e1..1bb87019b8 100644 --- a/src/ecalc/libraries/libecalc/common/tests/input/validation/test_validation_json_schemas.py +++ b/src/ecalc/libraries/libecalc/common/tests/input/validation/test_validation_json_schemas.py @@ -26,3 +26,34 @@ def test_json_schema_changed(self, snapshot): docs_keywords_url="https://test.ecalc.equinor.com/docs/docs/modelling/keywords", ) snapshot.assert_match(json.dumps(schemas, sort_keys=True, indent=4), snapshot_name="schemas.json") + + def test_temporal_property_placeholder(self): + """ + Make sure we replace the type in temporal models correctly with ref. This can be removed when all json-schemas + have been replaced with pydantic yaml models. + + See schema_helpers.replace_temporal_placeholder_property_with_legacy_ref + """ + schemas = generate_json_schemas( + server_url="https://test.ecalc.equinor.com", + docs_keywords_url="https://test.ecalc.equinor.com/docs/docs/modelling/keywords", + ) + energy_usage_model = schemas[0]["schema"]["definitions"]["YamlElectricityConsumer"]["properties"][ + "ENERGY_USAGE_MODEL" + ] + assert energy_usage_model["anyOf"] == [ + { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" + }, + { + "type": "object", + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" + } + }, + "additionalProperties": { + "$ref": "https://test.ecalc.equinor.com/api/v1/schema-validation/energy-usage-model.json#properties/ENERGY_USAGE_MODEL" + }, + }, + ] diff --git a/src/ecalc/libraries/libecalc/common/tests/input/yaml_types/test_temporal_model.py b/src/ecalc/libraries/libecalc/common/tests/input/yaml_types/test_temporal_model.py new file mode 100644 index 0000000000..c87d58e00c --- /dev/null +++ b/src/ecalc/libraries/libecalc/common/tests/input/yaml_types/test_temporal_model.py @@ -0,0 +1,25 @@ +from libecalc.input.yaml_types.temporal_model import TemporalModel +from pydantic import schema_of + + +class TestSchema: + def test_temporal_model_schema(self): + """ + Test to make sure temporal model creates the correct schema. We could improve TemporalModel to generate + patternProperties, we would also need to change schema_helpers.replace_temporal_placeholder_property_with_legacy_ref + """ + assert schema_of(TemporalModel[str], title="TemporalModel") == { + "title": "TemporalModel", + "anyOf": [ + { + "type": "string", + }, + { + "type": "object", + "patternProperties": { + "^\\d{4}\\-(0?[1-9]|1[012])\\-(0?[1-9]|[12][0-9]|3[01])$": {"type": "string"} + }, + "additionalProperties": {"type": "string"}, + }, + ], + }