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.
- goodmap/__init__.py +1 -0
- goodmap/clustering.py +75 -0
- goodmap/config.py +42 -0
- goodmap/core.py +46 -0
- goodmap/core_api.py +467 -0
- goodmap/data_models/location.py +68 -0
- goodmap/data_validator.py +119 -0
- goodmap/db.py +1466 -0
- goodmap/exceptions.py +100 -0
- goodmap/formatter.py +27 -0
- goodmap/goodmap.py +89 -0
- goodmap/templates/goodmap-admin.html +743 -0
- goodmap/templates/map.html +124 -0
- goodmap-1.1.7.dist-info/METADATA +142 -0
- goodmap-1.1.7.dist-info/RECORD +17 -0
- goodmap-1.1.7.dist-info/WHEEL +4 -0
- goodmap-1.1.7.dist-info/licenses/LICENSE.md +21 -0
|
@@ -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)
|