Skip to content

Commit 34a33cc

Browse files
author
Bryan Sieber
committed
Using pydantic-yaml and validations through pydantic.validators.
1 parent 3ff2b79 commit 34a33cc

File tree

9 files changed

+244
-167
lines changed

9 files changed

+244
-167
lines changed

poetry.lock

Lines changed: 159 additions & 96 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ prometheus-client = "^0.13.1"
1515
python-bugzilla = "^3.2.0"
1616
atlassian-python-api = "^3.20.1"
1717
dockerflow = "2022.1.0"
18-
PyYAML = "^6.0"
1918
types-PyYAML = "^6.0.4"
2019
Jinja2 = "^3.0.3"
20+
pydantic-yaml = {extras = ["pyyaml"], version = "^0.6.1"}
2121

2222

2323
[tool.poetry.dev-dependencies]

src/app/monitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from fastapi.responses import JSONResponse
88

99
from src.app import environment
10-
from src.jbi.services import jbi_service_health_map
10+
from src.jbi.service import jbi_service_health_map
1111

1212
api_router = APIRouter(tags=["Monitor"])
1313

src/jbi/configuration.py

Lines changed: 14 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,33 @@
1-
"""Parsing and validating the YAML configuration occurs within this module"""
2-
import importlib
1+
"""
2+
Parsing and validating the YAML configuration occurs within this module
3+
"""
34
import logging
4-
from inspect import signature
5-
from types import ModuleType
6-
from typing import Any, Dict, Optional
5+
from functools import lru_cache
76

8-
import yaml
9-
from yaml import Loader
7+
from pydantic import ValidationError
108

119
from src.app import environment
10+
from src.jbi.model import Actions
1211

1312
settings = environment.get_settings()
1413
jbi_logger = logging.getLogger("src.jbi")
1514

1615

1716
class ConfigError(Exception):
18-
"""Errors used to determine when the Configuration is invalid"""
19-
20-
21-
class ProcessError(Exception):
22-
"""Error when an exception occurs during processing"""
17+
"""Error when an exception occurs during processing config"""
2318

2419

20+
@lru_cache
2521
def get_yaml_configurations(
2622
jbi_config_file: str = f"config/config.{settings.env}.yaml",
27-
) -> Dict[str, Dict]:
23+
) -> Actions:
2824
"""Convert and validate YAML configuration to python dict"""
2925

3026
with open(jbi_config_file, encoding="utf-8") as file:
3127
try:
32-
file_data = file.read()
33-
data = yaml.load(file_data, Loader)
34-
validated_action_dict = process_actions(
35-
action_configuration=data.get("actions")
36-
)
37-
return validated_action_dict
38-
except (ValueError, ConfigError, yaml.YAMLError) as exception:
28+
yaml_data = file.read()
29+
actions: Actions = Actions.parse_raw(yaml_data)
30+
return actions
31+
except ValidationError as exception:
3932
jbi_logger.exception(exception)
40-
raise ProcessError("Errors exist.") from exception
41-
42-
43-
def process_actions(action_configuration) -> Dict[str, Dict]:
44-
"""Validate `actions` section of the YAML config"""
45-
requested_actions = {}
46-
for yaml_action_key, inner_action_dict in action_configuration.items():
47-
inner_action_dict.setdefault("action", "src.jbi.whiteboard_actions.default")
48-
inner_action_dict.setdefault("enabled", False)
49-
inner_action_dict.setdefault("parameters", {})
50-
validate_action_yaml_jbi_naming(
51-
yaml_action_key=yaml_action_key, action_dict=inner_action_dict
52-
)
53-
validate_action_yaml_module(action_dict=inner_action_dict)
54-
requested_actions[yaml_action_key] = inner_action_dict
55-
return requested_actions
56-
57-
58-
def validate_action_yaml_jbi_naming(yaml_action_key, action_dict):
59-
"""Validate yaml_action_key == parameters.whiteboard_tag"""
60-
wb_tag = action_dict["parameters"].get("whiteboard_tag")
61-
if yaml_action_key != wb_tag:
62-
raise ConfigError(
63-
f"Expected action key '{wb_tag}', found `{yaml_action_key}."
64-
"(from the `parameters.whiteboard_tag` field)."
65-
)
66-
67-
68-
def validate_action_yaml_module(action_dict: Dict[str, Any]):
69-
"""Validate action: exists, has init function, and has expected params"""
70-
try:
71-
action: str = action_dict.get("action") # type: ignore
72-
action_parameters: Optional[Dict[str, Any]] = action_dict.get("parameters")
73-
action_module: ModuleType = importlib.import_module(action)
74-
if not action_module:
75-
raise TypeError("Module is not found.")
76-
if not hasattr(action_module, "init"):
77-
raise TypeError("Module is missing `init` method.")
78-
79-
signature(action_module.init).bind(**action_parameters) # type: ignore
80-
except ImportError as exception:
81-
raise ConfigError(f"Unknown action `{action}`.") from exception
82-
except (TypeError, AttributeError) as exception:
83-
raise ConfigError("Action is not properly setup.") from exception
33+
raise ConfigError("Errors exist.") from exception

src/jbi/model.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
Python Module for Pydantic Models and validation
3+
"""
4+
import importlib
5+
from inspect import signature
6+
from types import ModuleType
7+
from typing import Any, Dict, Optional
8+
9+
from pydantic import Extra, ValidationError, root_validator, validator
10+
from pydantic_yaml import YamlModel
11+
12+
13+
class ActionConfig(YamlModel, extra=Extra.allow):
14+
"""
15+
ActionConfig is the inner model of `actions` in yaml
16+
"""
17+
18+
action: str = "src.jbi.whiteboard_actions.default"
19+
enabled: bool = False
20+
parameters: dict = {}
21+
22+
@root_validator
23+
def validate_action_config(
24+
cls, values
25+
): # pylint: disable=no-self-argument, no-self-use
26+
"""Validate action: exists, has init function, and has expected params"""
27+
try:
28+
action: str = values["action"] # type: ignore
29+
action_parameters: Optional[Dict[str, Any]] = values["parameters"]
30+
action_module: ModuleType = importlib.import_module(action)
31+
if not action_module:
32+
raise TypeError("Module is not found.")
33+
if not hasattr(action_module, "init"):
34+
raise TypeError("Module is missing `init` method.")
35+
36+
signature(action_module.init).bind(**action_parameters) # type: ignore
37+
except ImportError as exception:
38+
raise ValidationError(f"unknown action `{action}`.") from exception
39+
except (TypeError, AttributeError) as exception:
40+
raise ValidationError("action is not properly setup.") from exception
41+
return values
42+
43+
44+
class Actions(YamlModel):
45+
"""
46+
Actions is the overall model from parsing the yaml config `actions` sections
47+
"""
48+
49+
actions: Dict[str, ActionConfig]
50+
51+
@validator("actions")
52+
def validate_action_yaml_jbi_naming(
53+
cls, actions
54+
): # pylint: disable=no-self-argument, no-self-use
55+
"""
56+
Validate that the inner actions are named as expected
57+
"""
58+
if not actions:
59+
raise ValidationError("no actions configured")
60+
for name, action in actions.items():
61+
if name != action.parameters["whiteboard_tag"]:
62+
raise ValidationError("action name must match whiteboard tag")
63+
64+
return actions

src/jbi/router.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def get_whiteboard_tag(
3636
whiteboard_tag: Optional[str] = None,
3737
):
3838
"""API for viewing whiteboard_tags"""
39-
data = configuration.get_yaml_configurations()
39+
data = configuration.get_yaml_configurations().dict()["actions"]
4040
if whiteboard_tag:
4141
wb_val = data.get(whiteboard_tag)
4242
if wb_val:
@@ -47,7 +47,7 @@ def get_whiteboard_tag(
4747
@api_router.get("/actions/")
4848
def get_actions_by_type(action_type: Optional[str] = None):
4949
"""API for viewing actions"""
50-
configured_actions = configuration.get_yaml_configurations()
50+
configured_actions = configuration.get_yaml_configurations().dict()["actions"]
5151
if action_type:
5252
data = [
5353
a["action"]
@@ -62,7 +62,7 @@ def get_actions_by_type(action_type: Optional[str] = None):
6262
@api_router.get("/powered_by_jbi", response_class=HTMLResponse)
6363
def powered_by_jbi(request: Request, enabled: Optional[bool] = None):
6464
"""API for `Powered By` endpoint"""
65-
data = configuration.get_yaml_configurations()
65+
data = configuration.get_yaml_configurations().dict()["actions"]
6666
context = {
6767
"request": request,
6868
"title": "Powered by JBI",
File renamed without changes.

src/templates/powered_by_template.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ <h1>{{title}}</h1>
2020
</thead>
2121
<tbody class="list">
2222
{% for key, value in data.items() %}
23-
{% if enable_query is none or value.enabled is enable_query %}
23+
{% if enable_query == none or value.enabled == enable_query %}
2424
<tr>
2525
<td class="identifier">{{key}}</td>
2626
<td class="action">{{value.action}}</td>

tests/unit/jbi/test_configuration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
def test_mock_jbi_files():
11-
with pytest.raises(configuration.ProcessError):
11+
with pytest.raises(configuration.ConfigError):
1212
configuration.get_yaml_configurations(
1313
jbi_config_file="tests/unit/jbi/test-config.yaml"
1414
)

0 commit comments

Comments
 (0)