@Dennis89: Ich bin halt nicht gewohnt so etwas wie `isinstance()` in solchen Fällen selbst zu schreiben. In dem `glom`-Beispiel ist das ja auch drin, aber halt ”versteckt” weil das die Aktion ist, die durch `Match` gemacht wird, wenn ein Datentyp in der Struktur verwendet wird. Wobei speziell in Deinem Code ist das ja nicht nur ein Test, sondern Teil einer automagischen Umwandlung von Zeichenkette in Gleitkommazahl.
Wegen Gleitkommawerte vergleichen könntest Du Dir mal `math.isclose()` anschauen.
Ich weiss nicht ob ich `glom` für Validierung empfehlen würde. Ich habe das schon dafür verwendet weil ich es sowieso schon für das deklarative erstellen oder umbauen von JSON-ähnlichen ”Dokumenten” verwendet habe. Validierung war bei der Bibliothek auch noch nicht dabei als ich angefangen hatte die zu verwenden.
Was ich sonst noch so kannte war `cerberus`. Das ist wirklich nur zum Validieren von solchen Daten. Beispiel:
Code: Alles auswählen
#!/usr/bin/env python3
from cerberus import Validator
DATA = {
"Gas": [
{"gas_name": "CO2", "percent": 50},
{"gas_name": "CO", "percent": 50},
],
"General": {
"suction_pressure": 5,
},
"Kühler": [
{
"cooler_name": "DN 150",
"cooling_countercurrent": True,
"temperature_cooling_fluid_in": 5,
}
],
}
def check_percentage_sum(field, value, error):
total = sum(gas["percent"] for gas in value)
if total != 100:
error(
field,
f"the 'percent' values must add up to 100%, not {total}%",
)
SCHEMA = {
"Gas": {
"type": "list",
"schema": {
"type": "dict",
"schema": {
"gas_name": {"type": "string", "allowed": ["CO2", "CO"]},
"percent": {"type": "number", "min": 0, "max": 100},
},
},
"check_with": check_percentage_sum,
},
"General": {
"type": "dict",
"schema": {"suction_pressure": {"type": "number", "min": 0}},
},
"Kühler": {
"type": "list",
"minlength": 1,
"schema": {
"type": "dict",
"schema": {
"cooler_name": {"type": "string"},
"cooling_countercurrent": {"type": "boolean"},
"temperature_cooling_fluid_in": {"type": "number"},
},
},
},
}
def main():
validator = Validator(require_all=True)
if validator.validate(DATA, SCHEMA):
print(validator.document)
else:
print(validator.errors)
if __name__ == "__main__":
main()
Das hat den Vorteil, dass das Schema selbst wieder JSON sein kann/könnte. Im Beispiel jetzt nicht ganz weil da eine Funktion als Wert drin steckt, das kann man aber umgehen in dem man das als eigene Regel angibt die man auf eine von `Validator` abgeleiteten Klasse als Methode implementiert.
Dann gibt es noch `marshmallow` das auch (de)serialisieren kann, wo das Schema als Klassen angegeben wird:
Code: Alles auswählen
#!/usr/bin/env python3
from pprint import pprint
from marshmallow import Schema, ValidationError, fields, validate, validates
DATA = {
"Gas": [
{"gas_name": "CO2", "percent": 50},
{"gas_name": "CO", "percent": 50},
],
"General": {
"suction_pressure": 5,
},
"Kühler": [
{
"cooler_name": "DN 150",
"cooling_countercurrent": True,
"temperature_cooling_fluid_in": 5,
}
],
}
class GeneralSchema(Schema):
suction_pressure = fields.Number(
validate=validate.Range(min=0), required=True
)
class GasSchema(Schema):
name = fields.String(
data_key="gas_name",
validate=validate.OneOf(["CO2", "CO"]),
required=True,
)
percent = fields.Number(
validate=validate.Range(0, 100, min_inclusive=False), required=True
)
class CoolerSchema(Schema):
name = fields.String(data_key="cooler_name", required=True)
cooling_countercurrent = fields.Boolean(required=True)
temperature_cooling_fluid_in = fields.Number(required=True)
class DataSchema(Schema):
gases = fields.List(
fields.Nested(GasSchema), data_key="Gas", required=True
)
general = fields.Nested(GeneralSchema, data_key="General", required=True)
coolers = fields.List(
fields.Nested(CoolerSchema), data_key="Kühler", required=True
)
@validates("gases")
def validate_gases(self, data, **_kwargs):
total = sum(gas["percent"] for gas in data)
if total != 100:
raise ValidationError(
f"total percent must be 100, not {total}", "gases"
)
def main():
pprint(DataSchema().load(DATA))
if __name__ == "__main__":
main()
Die Ausgabe, wo man sehen kann, dass da auch die Namen der Schlüssel ”korrigiert” werden können, also jetzt alles in englisch, in kleiner Schreibweise und die "name"-Schlüssel ohne die etwas redundanten Prefixe:
Code: Alles auswählen
{'coolers': [{'cooling_countercurrent': True,
'name': 'DN 150',
'temperature_cooling_fluid_in': 5.0}],
'gases': [{'name': 'CO2', 'percent': 50.0}, {'name': 'CO', 'percent': 50.0}],
'general': {'suction_pressure': 5.0}}
Das dann mal als Basis für das nächste Beispiel `attrs` + `cattrs` (wobei die Kombination auch konfiguriert werden kann das Namen zwischen Objekten und Schlüsseln ”übersetzt” werden können:
Code: Alles auswählen
#!/usr/bin/env python3
from attrs import define, field
from attrs.validators import (
and_,
ge as is_greater_or_equal,
gt as is_greater,
in_,
le as is_less_or_equal,
min_len,
)
from cattrs import structure
from prettyprinter import install_extras, pprint
install_extras(["attrs"])
DATA = {
"gases": [
{"name": "CO2", "percent": 50},
{"name": "CO", "percent": 50},
],
"general": {
"suction_pressure": 5,
},
"coolers": [
{
"name": "DN 150",
"cooling_countercurrent": True,
"temperature_cooling_fluid_in": 5,
}
],
}
@define
class General:
suction_pressure: float = field(validator=is_greater_or_equal(0))
@define
class Gas:
name: str = field(validator=in_(["CO2", "CO"]))
percent: float = field(
validator=and_(is_greater(0), is_less_or_equal(100))
)
@define
class Cooler:
name: str
cooling_countercurrent: bool
temperature_cooling_fluid_in: float
@define
class Data:
gases: list[Gas] = field()
general: General
coolers: list[Cooler] = field(validator=min_len(1))
@gases.validator
def _validate_gases(self, _attribute, value):
total = sum(gas.percent for gas in value)
if total != 100:
raise ValueError(f"total percent must be 100, not {total}")
def main():
pprint(structure(DATA, Data))
if __name__ == "__main__":
main()
Ausgabe:
Code: Alles auswählen
Data(
gases=[Gas(name='CO2', percent=50.0), Gas(name='CO', percent=50.0)],
general=General(suction_pressure=5.0),
coolers=[
Cooler(
name='DN 150',
cooling_countercurrent=True,
temperature_cooling_fluid_in=5.0
)
]
)
Pydantic mag ich nicht so weil ich ja schon auf `attrs` für Klassen und Boilerplate schreiben vermeiden setze.