Skip to content

Commit 1fd2372

Browse files
Merge branch 'main' into #17-validate-json
2 parents e64697a + 0f38bf0 commit 1fd2372

File tree

11 files changed

+503
-49
lines changed

11 files changed

+503
-49
lines changed

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"python.testing.pytestArgs": [
3-
"tests"
3+
"tests",
4+
"dpytools"
45
],
56
"python.testing.unittestEnabled": false,
67
"python.testing.pytestEnabled": true,

dpytools/config/config.py

+74-20
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,89 @@
1-
from typing import Dict
1+
from __future__ import annotations
2+
3+
import os
4+
from typing import Any, Dict, List
25

36
from .properties.base import BaseProperty
7+
from .properties.intproperty import IntegerProperty
8+
from .properties.string import StringProperty
49

510

611
class Config:
12+
13+
def __init__(self):
14+
self._properties_to_validate: List[BaseProperty] = []
15+
716
@staticmethod
8-
def from_env(config_dict: Dict[str, BaseProperty]):
9-
# TODO = read in and populate property classes as
10-
# per the example in the main readme.
11-
# You need to populate with dot notation in mind so:
12-
#
13-
# StringProperty("fieldname", "fieldvalue")
14-
#
15-
# should be accessed on Config/self, so:
16-
#
17-
# value = config.fieldvalue.value
18-
# i.e
19-
# config.fieldvalue = StringProperty("fieldname", "fieldvalue")
20-
#
21-
# Worth looking at the __setattr_ dunder method and a loop
22-
# for how to do this.
23-
#
24-
# Do track the BaseProperty's that you add ready for
25-
# assert_valid_config call.
26-
...
17+
def from_env(config_dict: Dict[str, Dict[str, Any]]) -> Config:
18+
19+
config = Config()
20+
21+
for env_var_name, value in config_dict.items():
22+
23+
value_for_property = os.environ.get(env_var_name, None)
24+
assert (
25+
value_for_property is not None
26+
), f'Required envionrment value "{env_var_name}" could not be found.'
27+
28+
if value["class"] == StringProperty:
29+
if value["kwargs"]:
30+
regex = value["kwargs"].get("regex")
31+
min_len = value["kwargs"].get("min_len")
32+
max_len = value["kwargs"].get("max_len")
33+
else:
34+
regex = None
35+
min_len = None
36+
max_len = None
37+
38+
stringprop = StringProperty(
39+
_name=value["property"],
40+
_value=value_for_property,
41+
regex=regex,
42+
min_len=min_len,
43+
max_len=max_len,
44+
)
45+
46+
prop_name = value["property"]
47+
setattr(config, prop_name, stringprop)
48+
config._properties_to_validate.append(stringprop)
49+
50+
elif value["class"] == IntegerProperty:
51+
if value["kwargs"]:
52+
min_val = value["kwargs"].get("min_val")
53+
max_val = value["kwargs"].get("max_val")
54+
else:
55+
min_val = None
56+
max_val = None
57+
58+
intprop = IntegerProperty(
59+
_name=value["property"],
60+
_value=value_for_property,
61+
min_val=min_val,
62+
max_val=max_val,
63+
)
64+
65+
prop_name = value["property"]
66+
setattr(config, prop_name, intprop)
67+
config._properties_to_validate.append(intprop)
68+
69+
else:
70+
prop_type = value["class"]
71+
raise TypeError(
72+
f"Unsupported property type specified via 'property' field, got {prop_type}. Should be of type StringProperty or IntegerProperty"
73+
)
74+
75+
return config
2776

2877
def assert_valid_config(self):
2978
"""
3079
Assert that then Config class has the properties that
3180
provided properties.
3281
"""
82+
for property in self._properties_to_validate:
83+
property.type_is_valid()
84+
property.secondary_validation()
85+
86+
self._properties_to_validate = []
3387

3488
# For each of the properties you imbided above, run
3589
# self.type_is_valid()
+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .string import StringProperty
2+
from .intproperty import IntegerProperty

dpytools/config/properties/base.py

+16-9
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,24 @@
55

66
@dataclass
77
class BaseProperty(metaclass=ABCMeta):
8-
name: str
9-
value: Any
8+
_name: str
9+
_value: Any
1010

11-
# TODO: getter
12-
# allow someone to get the property
11+
@property
12+
def name(self):
13+
return self._name
1314

14-
# TODO: setter
15-
# categorically disallow anyone from
16-
# changing a property after the class
17-
# has been instantiated.
18-
# Refuse to do it, and log an error.
15+
@name.setter
16+
def name(self, value):
17+
raise ValueError(f"Trying to change name property to value {value} but you cannot change a property name after instantiation.")
18+
19+
@property
20+
def value(self):
21+
return self._value
22+
23+
@value.setter
24+
def value(self, value):
25+
raise ValueError(f"Trying to change value to {value} but you cannot change a property value after instantiation.")
1926

2027
@abstractmethod
2128
def type_is_valid(self):
+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import Optional
2+
from dataclasses import dataclass
3+
from .base import BaseProperty
4+
5+
@dataclass
6+
class IntegerProperty(BaseProperty):
7+
min_val: Optional[int]
8+
max_val: Optional[int]
9+
10+
def type_is_valid(self):
11+
"""
12+
Validate that the property looks like
13+
its of the correct type
14+
"""
15+
try:
16+
int(self._value)
17+
except Exception as err:
18+
raise Exception(f"Cannot cast {self._name} value {self._value} to integer.") from err
19+
20+
def secondary_validation(self):
21+
"""
22+
Non type based validation you might want to
23+
run against a configuration value of this kind.
24+
"""
25+
if not self._value:
26+
raise ValueError(f"Integer value for {self._name} does not exist.")
27+
28+
if self.min_val and self._value < self.min_val:
29+
raise ValueError(f"Integer value for {self._name} is lower than allowed minimum.")
30+
31+
if self.max_val and self._value > self.max_val:
32+
raise ValueError(f"Integer value for {self._name} is higher than allowed maximum.")

dpytools/config/properties/string.py

+22-10
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from typing import Optional
2-
2+
from dataclasses import dataclass
33
from .base import BaseProperty
44

5+
import re
6+
57

8+
@dataclass
69
class StringProperty(BaseProperty):
710
regex: Optional[str]
811
min_len: Optional[int]
@@ -14,28 +17,37 @@ def type_is_valid(self):
1417
its of the correct type
1518
"""
1619
try:
17-
str(self.value)
20+
str(self._value)
1821
except Exception as err:
1922
raise Exception(
20-
f"Cannot cast {self.name} value {self.value} to string."
23+
f"Cannot cast {self.name} value {self._value} to string."
2124
) from err
2225

23-
def secondary_validation_passed(self):
26+
def secondary_validation(self):
2427
"""
2528
Non type based validation you might want to
2629
run against a configuration value of this kind.
2730
"""
28-
if len(self.value) == 0:
31+
32+
if len(self._value) == 0:
2933
raise ValueError(f"Str value for {self.name} is an empty string")
3034

3135
if self.regex:
3236
# TODO - confirm the value matches the regex
33-
...
37+
regex_search = re.search(self.regex, self._value)
38+
if not regex_search:
39+
raise ValueError(
40+
f"Str value for {self.name} does not match the given regex."
41+
)
3442

3543
if self.min_len:
36-
# TODO - confirm the string matches of exceeds the minimum length
37-
...
44+
if len(self._value) < self.min_len:
45+
raise ValueError(
46+
f"Str value for {self.name} is shorter than minimum length {self.min_len}"
47+
)
3848

3949
if self.max_len:
40-
# TODO - confirm the value matches or is less than the max length
41-
...
50+
if len(self._value) > self.max_len:
51+
raise ValueError(
52+
f"Str value for {self.name} is longer than maximum length {self.max_len}"
53+
)

dpytools/validation/json/validation.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def validate_json_schema(
1919
2020
Either `data_dict` or `data_path` must be provided.
2121
22-
`msg` and `indent` are used to format the error message if validation fails.
22+
`error_msg` and `indent` are used to format the error message if validation fails.
2323
"""
2424
# Confirm that *either* `data_dict` *or* `data_path` has been provided, otherwise raise ValueError
2525
if data_dict and data_path:
@@ -37,23 +37,27 @@ def validate_json_schema(
3737
if parsed_schema_path.scheme == "http":
3838
# TODO Load schema from URL
3939
raise NotImplementedError("Validation from remote schema not yet supported")
40+
# Convert `schema_path` to pathlib.Path
4041
schema_path = Path(schema_path).absolute()
42+
# Check `schema_path` exists
4143
if not schema_path.exists():
4244
raise ValueError(f"Schema path '{schema_path}' does not exist")
4345
with open(schema_path, "r") as f:
4446
schema_from_path = json.load(f)
4547

46-
# Load data to be validated as dict
48+
# Load data to be validated
4749
if data_dict:
4850
if not isinstance(data_dict, Dict):
4951
raise ValueError("Invalid data format")
5052
data_to_validate = data_dict
5153

5254
if data_path:
55+
# Convert `data_path` to pathlib.Path
5356
if isinstance(data_path, str):
5457
data_path = Path(data_path).absolute()
5558
if not isinstance(data_path, Path):
5659
raise ValueError("Invalid data format")
60+
# Check `data_path` exists
5761
if not data_path.exists():
5862
raise ValueError(f"Data path '{data_path}' does not exist")
5963
with open(data_path, "r") as f:
@@ -62,8 +66,8 @@ def validate_json_schema(
6266
# Validate data against schema
6367
try:
6468
jsonschema.validate(data_to_validate, schema_from_path)
69+
# TODO Handle jsonschema.SchemaError?
6570
except jsonschema.ValidationError as err:
66-
# TODO Handle jsonschema.SchemaError?
6771
# If error is in a specific field, get the JSON path of the error location
6872
if err.json_path != "$":
6973
error_location = err.json_path
@@ -73,8 +77,8 @@ def validate_json_schema(
7377
if error_msg or indent:
7478
formatted_msg = f"""
7579
Exception: {error_msg}
76-
Error details: {err.message}
77-
Error location: {error_location}
80+
Exception details: {err.message}
81+
Exception location: {error_location}
7882
JSON data:
7983
{json.dumps(data_to_validate, indent=indent)}
8084
"""

0 commit comments

Comments
 (0)