diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 265bc6f1..a2c43dd5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,10 @@ Changelog ========= +0.3.0 (2023-09-14) +------------------ +- Add common notation see https://github.com/hpsim/OBR/pull/146 + 0.2.0 (2023-09-14) ------------------ - Add --json=file.json option to obr query, which writes the result of the query to a json file. diff --git a/pyproject.toml b/pyproject.toml index 0dcf4e02..7aa4a8dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "obr" -version = "0.2.0" +version = "0.3.0" dependencies = [ "click", diff --git a/src/obr/create_tree.py b/src/obr/create_tree.py index 4d149cec..1d7322be 100644 --- a/src/obr/create_tree.py +++ b/src/obr/create_tree.py @@ -38,7 +38,11 @@ def flatten(d, parent_key="", sep="/"): return dict(items) -def get_path_from(operation: dict, value) -> str: +def get_path_from(operation: dict, value: dict) -> str: + """Derive a view path from schema and the value dict + + Returns: a view path as string + """ if not operation.get("schema"): logging.error("Error Schema missing for Set schema to allow creating views") raise KeyError @@ -46,9 +50,28 @@ def get_path_from(operation: dict, value) -> str: return operation["schema"].format(**flatten(value)) + "/" -def extract_from_operation(operation, value): - """takes an operation""" +def extract_from_operation(operation: dict, value) -> dict: + """takes an operation dictionary and do some processing + It extracts keys path and args from the operation dictionary + based on given value. The passed value is used as a selector + to create keys path and args. + + - args are later used to pass it to the selected operation, + either the operation contains: + 1. {key: key, values: [v1, v2, ...]} or + 2. {values: [{k:v1},{k:v2}] } + 2. {common: {c1:v1, c2:v2}, values: [{k:v1},{k:v2}] } + + - the path is derived from the schema key value pair + a entry {schema: path/{foo}, values: [{foo: 1}]} will be formatted + to path/1 + + Returns: a dictionary with keys, path and args + """ key = operation.get("key", "").replace(".", "_dot_") + common = operation.get("common", {}) + if isinstance(value, dict) and common: + value.update(common) if not key: path = get_path_from(operation, value) args = value @@ -63,7 +86,7 @@ def extract_from_operation(operation, value): path = "{}/{}/".format(key_, value) keys = [key] - return keys, path, args + return {"keys": keys, "path": path, "args": args} def generate_view( @@ -141,8 +164,12 @@ def add_variations( variation: list, parent_job: Job, id_path_mapping: dict, -) -> list: - """Recursively adds variations to the project""" +) -> list[str]: + """Recursively adds variations to the project and initialises the jobs. This + creates the workspace/uid folder and signac files as sideeffect. + + Returns: A list of all operation names + """ for operation in variation: sub_variation = operation.get("variation", {}) @@ -155,27 +182,29 @@ def add_variations( continue # derive path name from schema or key value - keys, path, args = extract_from_operation(operation, value) - path = clean_path(path) + parse_res = extract_from_operation(operation, value) + path = clean_path(parse_res["path"]) base_dict = deepcopy(to_dict(parent_job.sp)) statepoint = { - "keys": keys, + "keys": parse_res["keys"], "parent_id": parent_job.id, "operation": operation["operation"], "has_child": True if sub_variation else False, "pre_build": operation.get("pre_build", []), "post_build": operation.get("post_build", []), - **args, + **parse_res["args"], } statepoint["parent"] = base_dict job = project.open_job(statepoint) - setup_job_doc(job, value) + setup_job_doc(job) job.init() job.doc["state"]["global"] = "" - id_path_mapping[job.id] = id_path_mapping.get(parent_job.id, "") + path + id_path_mapping[job.id] = ( + id_path_mapping.get(parent_job.id, "") + parse_res["path"] + ) if sub_variation: operations = add_variations( @@ -186,7 +215,7 @@ def add_variations( return operations -def setup_job_doc(job, value, reset=False): +def setup_job_doc(job: Job, reset: bool =False) -> None: """Sets basic information in the job document""" # dont overwrite old job state on init so that we can update a tree without @@ -221,7 +250,7 @@ def create_tree( base_case_state.update({k: v for k, v in config["case"].items()}) of_case = project.open_job(base_case_state) - setup_job_doc(of_case, value=[]) + setup_job_doc(of_case) of_case.init() operations: list = [] diff --git a/tests/test_create_tree.py b/tests/test_create_tree.py index 89d5c5d3..610510ee 100644 --- a/tests/test_create_tree.py +++ b/tests/test_create_tree.py @@ -1,4 +1,4 @@ -from obr.create_tree import create_tree +from obr.create_tree import create_tree, add_variations, extract_from_operation from obr.signac_wrapper.operations import OpenFOAMProject import pytest @@ -33,6 +33,62 @@ def emit_test_config(): } +def test_extract_from_operation(): + operation = {"operation": "op1", "key": "foo", "values": [1, 2, 3]} + res = extract_from_operation(operation, 1) + assert res == {"keys": ["foo"], "path": "foo/1/", "args": {"foo": 1}} + + operation = { + "operation": "op1", + "schema": "path/{foo}", + "values": [{"foo": 1}, {"foo": 2}, {"foo": 3}], + } + res = extract_from_operation(operation, {"foo": 1}) + assert res == {"keys": ["foo"], "path": "path/1/", "args": {"foo": 1}} + + operation = { + "operation": "op1", + "schema": "path/{foo}", + "common": {"c1": "v1"}, + "values": [{"foo": 1}, {"foo": 2}, {"foo": 3}], + } + res = extract_from_operation(operation, {"foo": 1}) + assert res == { + "keys": ["foo", "c1"], + "path": "path/1/", + "args": {"foo": 1, "c1": "v1"}, + } + + +def test_add_variations(): + class MockJob: + id = "0" + sp = {} + doc = {} + + def init(self): + pass + + class MockProject: + def open_job(self, statepoint): + return MockJob() + + operations = [] + test_variation = [ + { + "operation": "n/a", + "schema": "n/a", + "values": [{"foo": 1}, {"foo": 2}, {"foo": 3}], + } + ] + id_path_mapping = {} + operations = add_variations( + operations, MockProject(), test_variation, MockJob(), id_path_mapping + ) + + assert operations == ["n/a"] + + def test_create_tree(tmpdir, emit_test_config): project = OpenFOAMProject.init_project(root=tmpdir)