Skip to content

Commit

Permalink
feat: adds model_to_object() function (#34)
Browse files Browse the repository at this point in the history
This library currently contains a lot of functionality for going from a
dictionary to a model, but doesn't have an interface for going the other
way. All of the models are currently Pydantic models and Pydantic makes
a `dict()` method available on all models, but that method isn't
sufficient for OpenJD models. OpenJD models may contain values of type
Decimal, and Decimal types don't serialize in to json/yaml using the
default encoders.

This adds a method that can be used to convert a model to a dictionary
in a way that is compatible with json/yaml encoders -- we convert all
Decimal values in to strings.

Signed-off-by: Daniel Neilson <53624638+ddneilson@users.noreply.github.com>
  • Loading branch information
ddneilson committed Jan 17, 2024
1 parent 0a7b0d8 commit c6d7752
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 3 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,38 @@ job_template = JobTemplate(
)
```

### Converting a Template Model to a Dictionary

```python
import json
from openjd.model import (
decode_job_template,
model_to_object,
)
from openjd.model.v2023_09 import *

job_template = JobTemplate(
specificationVersion="jobtemplate-2023-09",
name="DemoJob",
steps=[
StepTemplate(
name="DemoStep",
script=StepScript(
actions=StepActions(
onRun=Action(
command="echo",
args=["Hello world"]
)
)
)
)
]
)

obj = model_to_object(model=job_template)
print(json.dumps(obj))
```

### Creating a Job from a Job Template

```python
Expand Down
8 changes: 6 additions & 2 deletions src/openjd/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
decode_environment_template,
decode_job_template,
document_string_to_object,
model_to_object,
parse_model,
)
from ._step_dependency_graph import (
Expand All @@ -33,6 +34,7 @@
JobParameterInputValues,
JobParameterValues,
JobTemplate,
OpenJDModel,
ParameterValue,
ParameterValueType,
SchemaVersion,
Expand All @@ -48,10 +50,11 @@
"decode_environment_template",
"decode_job_template",
"document_string_to_object",
"validate_amount_capability_name",
"validate_attribute_capability_name",
"model_to_object",
"parse_model",
"preprocess_job_parameters",
"validate_amount_capability_name",
"validate_attribute_capability_name",
"CompatibilityError",
"DecodeValidationError",
"DocumentType",
Expand All @@ -64,6 +67,7 @@
"JobParameterValues",
"JobTemplate",
"ModelValidationError",
"OpenJDModel",
"ParameterValue",
"ParameterValueType",
"SchemaVersion",
Expand Down
35 changes: 34 additions & 1 deletion src/openjd/model/_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import json
from dataclasses import is_dataclass
from decimal import Decimal
from enum import Enum
from typing import Any, ClassVar, Type, TypeVar, cast
from typing import Any, ClassVar, Type, TypeVar, Union, cast

import yaml
from pydantic import BaseModel
Expand Down Expand Up @@ -85,6 +86,38 @@ def document_string_to_object(*, document: str, document_type: DocumentType) ->
)


def model_to_object(*, model: OpenJDModel) -> dict[str, Any]:
"""Given a model from this package, encode it as a dictionary such that it could
be written to a JSON/YAML document."""

as_dict = model.dict()

# Some of the values in the model can be type 'Decimal', which doesn't
# encode into json/yaml without special handling. So, we convert those in to
# strings.
def decimal_to_str(data: Union[dict[str, Any], list[Any]]) -> None:
if isinstance(data, list):
for i, item in enumerate(data):
if isinstance(item, Decimal):
data[i] = str(item)
elif isinstance(item, (dict, list)):
decimal_to_str(item)
else:
delete_keys: list[str] = []
for k, v in data.items():
if isinstance(v, Decimal):
data[k] = str(v)
elif isinstance(v, (dict, list)):
decimal_to_str(v)
elif v is None:
delete_keys.append(k)
for k in delete_keys:
del data[k]

decimal_to_str(as_dict)
return as_dict


def decode_job_template(*, template: dict[str, Any]) -> JobTemplate:
"""Given a dictionary containing a Job Template, this will decode the template, run validation checks on it,
and then return the decoded template.
Expand Down
41 changes: 41 additions & 0 deletions test/openjd/model/test_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
decode_environment_template,
decode_job_template,
document_string_to_object,
model_to_object,
)
from openjd.model._types import OpenJDModel
from openjd.model.v2023_09 import JobTemplate as JobTemplate_2023_09
Expand Down Expand Up @@ -62,6 +63,46 @@ def test_bad_parse(self, document: str, doctype: DocumentType) -> None:
document_string_to_object(document=document, document_type=doctype)


class TestModelToObject:
@pytest.mark.parametrize(
"template",
[
pytest.param(
{
"name": "DemoJob",
"specificationVersion": "jobtemplate-2023-09",
"parameterDefinitions": [{"name": "Foo", "type": "FLOAT", "default": "12"}],
"steps": [
{
"name": "DemoStep",
"parameterSpace": {
"taskParameterDefinitions": [
{"name": "Foo", "type": "FLOAT", "range": ["1.1", "1.2"]}
]
},
"script": {
"actions": {
"onRun": {"command": "echo", "args": ["Foo={{Param.Foo}}"]}
}
},
}
],
},
id="translates Decimal to string",
)
],
)
def test(self, template: dict[str, Any]) -> None:
# GIVEN
model = decode_job_template(template=template)

# WHEN
result = model_to_object(model=model)

# THEN
assert result == template


class TestDecodeJobTemplate:
@pytest.mark.parametrize(
"template",
Expand Down

0 comments on commit c6d7752

Please sign in to comment.