goodmap 1.1.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ from typing import Any, Type
2
+
3
+ from pydantic import (
4
+ BaseModel,
5
+ Field,
6
+ ValidationError,
7
+ create_model,
8
+ field_validator,
9
+ model_validator,
10
+ )
11
+
12
+ from goodmap.exceptions import LocationValidationError
13
+
14
+
15
+ class LocationBase(BaseModel, extra="allow"):
16
+ position: tuple[float, float]
17
+ uuid: str
18
+
19
+ @field_validator("position")
20
+ @classmethod
21
+ def position_must_be_valid(cls, v):
22
+ if v[0] < -90 or v[0] > 90:
23
+ raise ValueError("latitude must be in range -90 to 90")
24
+ if v[1] < -180 or v[1] > 180:
25
+ raise ValueError("longitude must be in range -180 to 180")
26
+ return v
27
+
28
+ @model_validator(mode="before")
29
+ @classmethod
30
+ def validate_uuid_exists(cls, data: Any) -> Any:
31
+ """Ensure UUID is present before validation for better error messages."""
32
+ if isinstance(data, dict) and "uuid" not in data:
33
+ raise ValueError("Location data must include 'uuid' field")
34
+ return data
35
+
36
+ @model_validator(mode="wrap")
37
+ @classmethod
38
+ def enrich_validation_errors(cls, data, handler):
39
+ """Wrap validation errors with UUID context for better debugging."""
40
+ try:
41
+ return handler(data)
42
+ except ValidationError as e:
43
+ uuid = data.get("uuid") if isinstance(data, dict) else None
44
+ raise LocationValidationError(e, uuid=uuid) from e
45
+
46
+ def basic_info(self):
47
+ return {
48
+ "uuid": self.uuid,
49
+ "position": self.position,
50
+ "remark": bool(getattr(self, "remark", False)),
51
+ }
52
+
53
+
54
+ def create_location_model(obligatory_fields: list[tuple[str, Type[Any]]]) -> Type[BaseModel]:
55
+ fields = {
56
+ field_name: (field_type, Field(...)) for (field_name, field_type) in obligatory_fields
57
+ }
58
+
59
+ return create_model(
60
+ "Location",
61
+ __config__=None,
62
+ __doc__=None,
63
+ __module__="Location",
64
+ __validators__=None,
65
+ __base__=LocationBase,
66
+ __cls_kwargs__=None,
67
+ **fields,
68
+ )
@@ -0,0 +1,119 @@
1
+ import json
2
+ from enum import Enum
3
+ from sys import argv, stderr
4
+
5
+
6
+ class ViolationType(Enum):
7
+ INVALID_JSON_FORMAT = 0
8
+ MISSING_OBLIGATORY_FIELD = 1
9
+ INVALID_VALUE_IN_CATEGORY = 2
10
+ NULL_VALUE = 3
11
+
12
+ def get_error_message(self):
13
+ error_message_dict = {
14
+ 0: "invalid json format",
15
+ 1: "missing obligatory field",
16
+ 2: "invalid value in category",
17
+ 3: "attribute has null value",
18
+ }
19
+ return error_message_dict[self.value]
20
+
21
+
22
+ class DataViolation:
23
+ def __init__(self, violation_type: ViolationType):
24
+ self.violation_type = violation_type
25
+
26
+
27
+ class FormatViolation(DataViolation):
28
+ def __init__(self, decoding_error):
29
+ super().__init__(ViolationType.INVALID_JSON_FORMAT)
30
+ self.decoding_error = decoding_error
31
+
32
+
33
+ class FieldViolation(DataViolation):
34
+ def __init__(self, violation_type: ViolationType, datapoint, violating_field):
35
+ super().__init__(violation_type)
36
+ self.datapoint = datapoint
37
+ self.violating_field = violating_field
38
+
39
+
40
+ def get_missing_obligatory_fields_violations(p, obligatory_fields):
41
+ violations = []
42
+ for field in obligatory_fields:
43
+ if field not in p.keys():
44
+ violations.append(FieldViolation(ViolationType.MISSING_OBLIGATORY_FIELD, p, field))
45
+ return violations
46
+
47
+
48
+ def get_invalid_value_in_category_violations(p, categories):
49
+ violations = []
50
+ for category in categories & p.keys():
51
+ category_value_in_point = p[category]
52
+ valid_values_set = categories[category]
53
+ if isinstance(category_value_in_point, list):
54
+ for attribute_value in category_value_in_point:
55
+ if attribute_value not in valid_values_set:
56
+ violations.append(
57
+ FieldViolation(ViolationType.INVALID_VALUE_IN_CATEGORY, p, category)
58
+ )
59
+ else:
60
+ if category_value_in_point not in valid_values_set:
61
+ violations.append(
62
+ FieldViolation(ViolationType.INVALID_VALUE_IN_CATEGORY, p, category)
63
+ )
64
+ return violations
65
+
66
+
67
+ def get_null_values_violations(p):
68
+ violations = []
69
+ for attribute, value in p.items():
70
+ if value is None:
71
+ violations.append(FieldViolation(ViolationType.NULL_VALUE, p, attribute))
72
+ return violations
73
+
74
+
75
+ def report_data_violations_from_json(json_database):
76
+ map_data = json_database["map"]
77
+ datapoints = map_data["data"]
78
+ categories = map_data["categories"]
79
+ obligatory_fields = map_data["location_obligatory_fields"]
80
+
81
+ data_violations = []
82
+
83
+ for p in datapoints:
84
+ data_violations += get_missing_obligatory_fields_violations(p, obligatory_fields)
85
+ data_violations += get_invalid_value_in_category_violations(p, categories)
86
+ data_violations += get_null_values_violations(p)
87
+
88
+ return data_violations
89
+
90
+
91
+ def report_data_violations_from_json_file(path_to_json_file):
92
+ with open(path_to_json_file) as json_file:
93
+ try:
94
+ return report_data_violations_from_json(json.load(json_file))
95
+ except json.JSONDecodeError as e:
96
+ return [FormatViolation(e)]
97
+
98
+
99
+ def print_reported_violations(data_violations): # pragma: no cover
100
+ for violation in data_violations:
101
+ violation_type = violation.violation_type
102
+ if violation_type == ViolationType.INVALID_JSON_FORMAT:
103
+ print("DATA ERROR: invalid json format", file=stderr)
104
+ print(violation.decoding_error, file=stderr)
105
+ else:
106
+ violating_field = violation.violating_field
107
+ violation_type_error = violation_type.get_error_message()
108
+ print(
109
+ f"DATA ERROR: {violation_type_error} {violating_field} in datapoint:", file=stderr
110
+ )
111
+ print(violation.datapoint, file=stderr)
112
+
113
+
114
+ if __name__ == "__main__": # pragma: no cover
115
+ data_violations = report_data_violations_from_json_file(argv[1])
116
+ if data_violations == []:
117
+ print("All data is valid", file=stderr)
118
+ else:
119
+ print_reported_violations(data_violations)