Source code for task_script_utils.extract_to_decorate

"""
Derived from extract-to-decorate function from ts-task-script-file-util
"""

from __future__ import annotations

import re
from typing import Callable, Dict, List

import pydash


[docs] def process_metadata(function: Callable, metadata: Dict[str, any]) -> Dict[str, any]: """ Applies a function to all values in a metadata object. :param function: a function that can be applied to a string :param metadata: an object of the following structure:: { "key": "value 1", "key2": "value 2", } """ # Return nothing given if metadata: return {key: function(value) for key, value in metadata.items()} return metadata
[docs] def process_tags(function: Callable, tags: List) -> List: """ Applies a function to all values in a tags list. :param function: a function that can be applied to a string :param tags: a list of stings """ if tags: return [function(tag) for tag in tags] return tags
[docs] def process_labels(function: Callable, labels: List) -> List: """ Applies a function to all values in a labels object. :param function: a function that can be applied to a string :param labels: a list of objects with the following structure:: [ { "name": "key1", "value": "value 1" }, { "name": "key2", "value": "value 2" } ] """ if labels: return [ { key: (function(value) if key == "value" else value) for key, value in label.items() } for label in labels ] return labels
[docs] def sanitize_output(text: str) -> str: """ Removes characters that will cause pipeline errors if in metadata. Currently, the Regex for metadata values is ^[0-9a-zA-Z-_+.,/ ]+$ Replaces with whitespace, then reduces multiple adjoining whitespace to single """ illegal_char_regex = re.compile(r"[^0-9a-zA-Z-_+.,/ ]") replaced_text = illegal_char_regex.sub(" ", text) # Remove multiple adjoining spaces # (e.g. "A & B" becomes "A B" instead of "A B") return " ".join([t for t in replaced_text.split(" ") if t != ""])
[docs] def do_mapping_extraction( mappings: List[Dict[str, any]], source_value: str, logger: Callable, flags=0 ): """ Iterates through the mapping list and conducts the regex search over the source string. :param mappings: (list, required) a list of dictionaries. Example: .. code-block:: python [ { "example": "/data/raw/Attune/EXP22000214/2022-08-12 vb serp test_experiment_6b5 hep 10k_a9.fcs", "source": "fileKey", "pattern": ".*(exp[0-9]{8})", "targets": [{"type": "metadata", "name": "benchling_entry_id"}], } ] :param source_value: (str, required) string to search :param logger: (function, required) Logger function :param flags: (RegexFlag, optional) flag options to include in re.search, e.g. re.IGNORECASE """ metadata = {} tags = [] labels = [] for mapping in mappings: pattern = mapping["pattern"] logger({"message": f"Using pattern: {pattern}", "level": "info"}) match = re.search(pattern, source_value, flags) if match: targets = mapping["targets"] match_groups = match.groups() assert len(targets) <= len(match_groups), ( f"Number of targets {len(targets)} should not exceed " f"number of matched groups {len(match_groups)}" ) for target, value in zip(targets, match_groups): assert target["type"] in [ "metadata", "tag", "label", ], "Allowed target types are 'metadata', 'tag', 'label'" target_name = target.get("name") if target["type"] in ["metadata", "label"]: assert target_name is not None, ( f"The target type {target['type']} requires a non-null " f"'name' field" ) if target["type"] == "metadata": if "value" in target: metadata[target_name] = target["value"] continue metadata[target_name] = value elif target["type"] == "label": if "value" in target: labels.append({"name": target_name, "value": target["value"]}) continue labels.append({"name": target_name, "value": value}) elif target["type"] == "tag": if "value" in target: tags.append(target["value"]) continue tags.append(value) logger( { "message": f"Matched {pattern} and stopped iterating through patterns.", "level": "info", } ) break else: logger( {"message": f"Failed the extraction with {pattern}", "level": "info"} ) return ( metadata, pydash.uniq(tags), pydash.uniq_by(labels, lambda l: f"{l['name']}@{l['value']}"), )
# pylint: disable=too-many-arguments
[docs] def extract_to_decorate( mappings: List[Dict[str, any]], source_value: str, re_flags=0, sanitize=False, title_case=False, logger: Callable = print, ): """ This function uses a user-defined regex pattern search to extract metadata, tags and labels from a filename :param mappings: (list, required) a list of dictionaries. Example: .. code-block:: python [ { "example": "/data/raw/Attune/EXP22000214/2022-08-12 vb serp test_experiment_6b5 hep 10k_a9.fcs", "source": "fileKey", "pattern": ".*(exp[0-9]{8})", "targets": [{"type": "metadata", "name": "benchling_entry_id"}], } ] :param source_value: (str, required) string to search :param re_flags: (RegexFlag, optional) flag options to include in re.search, e.g. re.IGNORECASE :param sanitize: (bool, optional) Removes characters that will cause pipeline errors if in metadata :param title_case: (bool, optional) Transforms MTL values to title case :param logger: (function, optional) Logger function """ # extract mappings metadata, tags, labels = do_mapping_extraction( mappings, source_value, logger, re_flags ) if sanitize: metadata = process_metadata(sanitize_output, metadata) tags = process_tags(sanitize_output, tags) labels = process_labels(sanitize_output, labels) if title_case: norm_func = str.title metadata = process_metadata(norm_func, metadata) tags = process_tags(norm_func, tags) labels = process_labels(norm_func, labels) logger({"message": f"Metadata: {metadata}", "level": "info"}) logger({"message": f"Tags: {tags}", "level": "info"}) logger({"message": f"Labels: {labels}", "level": "info"}) if not (metadata or tags or labels): logger({"message": "Metadata, tags and labels are empty!", "level": "error"}) return {"metadata": metadata, "tags": tags, "labels": labels}