idmtools 0.0.0.dev0__py3-none-any.whl → 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. idmtools/__init__.py +27 -8
  2. idmtools/analysis/__init__.py +5 -0
  3. idmtools/analysis/add_analyzer.py +89 -0
  4. idmtools/analysis/analyze_manager.py +490 -0
  5. idmtools/analysis/csv_analyzer.py +103 -0
  6. idmtools/analysis/download_analyzer.py +96 -0
  7. idmtools/analysis/map_worker_entry.py +100 -0
  8. idmtools/analysis/platform_analysis_bootstrap.py +94 -0
  9. idmtools/analysis/platform_anaylsis.py +291 -0
  10. idmtools/analysis/tags_analyzer.py +93 -0
  11. idmtools/assets/__init__.py +9 -0
  12. idmtools/assets/asset.py +453 -0
  13. idmtools/assets/asset_collection.py +514 -0
  14. idmtools/assets/content_handlers.py +19 -0
  15. idmtools/assets/errors.py +23 -0
  16. idmtools/assets/file_list.py +191 -0
  17. idmtools/builders/__init__.py +11 -0
  18. idmtools/builders/arm_simulation_builder.py +152 -0
  19. idmtools/builders/csv_simulation_builder.py +76 -0
  20. idmtools/builders/simulation_builder.py +348 -0
  21. idmtools/builders/sweep_arm.py +109 -0
  22. idmtools/builders/yaml_simulation_builder.py +82 -0
  23. idmtools/config/__init__.py +7 -0
  24. idmtools/config/idm_config_parser.py +486 -0
  25. idmtools/core/__init__.py +10 -0
  26. idmtools/core/cache_enabled.py +114 -0
  27. idmtools/core/context.py +68 -0
  28. idmtools/core/docker_task.py +207 -0
  29. idmtools/core/enums.py +51 -0
  30. idmtools/core/exceptions.py +91 -0
  31. idmtools/core/experiment_factory.py +71 -0
  32. idmtools/core/id_file.py +70 -0
  33. idmtools/core/interfaces/__init__.py +5 -0
  34. idmtools/core/interfaces/entity_container.py +64 -0
  35. idmtools/core/interfaces/iassets_enabled.py +58 -0
  36. idmtools/core/interfaces/ientity.py +331 -0
  37. idmtools/core/interfaces/iitem.py +206 -0
  38. idmtools/core/interfaces/imetadata_operations.py +89 -0
  39. idmtools/core/interfaces/inamed_entity.py +17 -0
  40. idmtools/core/interfaces/irunnable_entity.py +159 -0
  41. idmtools/core/logging.py +387 -0
  42. idmtools/core/platform_factory.py +316 -0
  43. idmtools/core/system_information.py +104 -0
  44. idmtools/core/task_factory.py +145 -0
  45. idmtools/entities/__init__.py +10 -0
  46. idmtools/entities/command_line.py +229 -0
  47. idmtools/entities/command_task.py +155 -0
  48. idmtools/entities/experiment.py +787 -0
  49. idmtools/entities/generic_workitem.py +43 -0
  50. idmtools/entities/ianalyzer.py +163 -0
  51. idmtools/entities/iplatform.py +1106 -0
  52. idmtools/entities/iplatform_default.py +39 -0
  53. idmtools/entities/iplatform_ops/__init__.py +5 -0
  54. idmtools/entities/iplatform_ops/iplatform_asset_collection_operations.py +148 -0
  55. idmtools/entities/iplatform_ops/iplatform_experiment_operations.py +415 -0
  56. idmtools/entities/iplatform_ops/iplatform_simulation_operations.py +315 -0
  57. idmtools/entities/iplatform_ops/iplatform_suite_operations.py +322 -0
  58. idmtools/entities/iplatform_ops/iplatform_workflowitem_operations.py +301 -0
  59. idmtools/entities/iplatform_ops/utils.py +185 -0
  60. idmtools/entities/itask.py +316 -0
  61. idmtools/entities/iworkflow_item.py +167 -0
  62. idmtools/entities/platform_requirements.py +20 -0
  63. idmtools/entities/relation_type.py +14 -0
  64. idmtools/entities/simulation.py +255 -0
  65. idmtools/entities/suite.py +188 -0
  66. idmtools/entities/task_proxy.py +37 -0
  67. idmtools/entities/templated_simulation.py +325 -0
  68. idmtools/frozen/frozen_dict.py +71 -0
  69. idmtools/frozen/frozen_list.py +66 -0
  70. idmtools/frozen/frozen_set.py +86 -0
  71. idmtools/frozen/frozen_tuple.py +18 -0
  72. idmtools/frozen/frozen_utils.py +179 -0
  73. idmtools/frozen/ifrozen.py +66 -0
  74. idmtools/plugins/__init__.py +5 -0
  75. idmtools/plugins/git_commit.py +117 -0
  76. idmtools/registry/__init__.py +4 -0
  77. idmtools/registry/experiment_specification.py +105 -0
  78. idmtools/registry/functions.py +28 -0
  79. idmtools/registry/hook_specs.py +132 -0
  80. idmtools/registry/master_plugin_registry.py +51 -0
  81. idmtools/registry/platform_specification.py +138 -0
  82. idmtools/registry/plugin_specification.py +129 -0
  83. idmtools/registry/task_specification.py +104 -0
  84. idmtools/registry/utils.py +119 -0
  85. idmtools/services/__init__.py +5 -0
  86. idmtools/services/ipersistance_service.py +135 -0
  87. idmtools/services/platforms.py +13 -0
  88. idmtools/utils/__init__.py +5 -0
  89. idmtools/utils/caller.py +24 -0
  90. idmtools/utils/collections.py +246 -0
  91. idmtools/utils/command_line.py +45 -0
  92. idmtools/utils/decorators.py +300 -0
  93. idmtools/utils/display/__init__.py +22 -0
  94. idmtools/utils/display/displays.py +181 -0
  95. idmtools/utils/display/settings.py +25 -0
  96. idmtools/utils/dropbox_location.py +30 -0
  97. idmtools/utils/entities.py +127 -0
  98. idmtools/utils/file.py +72 -0
  99. idmtools/utils/file_parser.py +151 -0
  100. idmtools/utils/filter_simulations.py +182 -0
  101. idmtools/utils/filters/__init__.py +5 -0
  102. idmtools/utils/filters/asset_filters.py +88 -0
  103. idmtools/utils/general.py +286 -0
  104. idmtools/utils/gitrepo.py +336 -0
  105. idmtools/utils/hashing.py +239 -0
  106. idmtools/utils/info.py +124 -0
  107. idmtools/utils/json.py +82 -0
  108. idmtools/utils/language.py +107 -0
  109. idmtools/utils/local_os.py +40 -0
  110. idmtools/utils/time.py +22 -0
  111. idmtools-0.0.2.dist-info/METADATA +120 -0
  112. idmtools-0.0.2.dist-info/RECORD +116 -0
  113. idmtools-0.0.2.dist-info/entry_points.txt +9 -0
  114. idmtools-0.0.2.dist-info/licenses/LICENSE.TXT +3 -0
  115. idmtools-0.0.0.dev0.dist-info/METADATA +0 -41
  116. idmtools-0.0.0.dev0.dist-info/RECORD +0 -5
  117. {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.2.dist-info}/WHEEL +0 -0
  118. {idmtools-0.0.0.dev0.dist-info → idmtools-0.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,182 @@
1
+ """
2
+ Filtering utility.
3
+
4
+ Copyright 2025, Gates Foundation. All rights reserved.
5
+ """
6
+ from idmtools.core import ItemType, EntityStatus
7
+ from idmtools.core.interfaces.ientity import IEntity
8
+ from idmtools.entities.experiment import Experiment
9
+ from idmtools.entities.iplatform import IPlatform
10
+ from idmtools.utils.general import parse_value_tags
11
+
12
+
13
+ class FilterItem:
14
+ """
15
+ FilterItem provides a utility to filter items on a platform.
16
+ """
17
+
18
+ @staticmethod
19
+ def filter_item(platform: IPlatform, item: IEntity, tags=None, status: EntityStatus = None,
20
+ entity_type: bool = False, max_simulations: int = None, skip_sims=None, **kwargs):
21
+ """
22
+ Filter simulations from an Experiment or Suite using tag and status criteria.
23
+
24
+ By default, this filters simulations that have a status of `EntityStatus.SUCCEEDED`.
25
+ Additional filtering can be applied by specifying tag values or tag-based conditions.
26
+
27
+ This method supports:
28
+ - Skipping specific simulations by ID
29
+ - Filtering based on simulation status (e.g., FAILED, SUCCEEDED)
30
+ - Tag-based filtering (both exact match and conditional/lambda-based)
31
+
32
+ Examples:
33
+ >>> filter_item(platform, experiment, status=EntityStatus.FAILED)
34
+ >>> filter_item(platform, experiment, tags={"Run_Number": "2"})
35
+ >>> filter_item(platform, experiment, tags={"Run_Number": lambda v: 2 <= v <= 10})
36
+ >>> filter_item(platform, experiment, tags={"Coverage": 0.8}, status=EntityStatus.SUCCEEDED)
37
+ >>> filter_item(platform, experiment, tags={"Coverage": 0.8}, max_simulations=10)
38
+
39
+ Args:
40
+ platform (IPlatform): The platform instance to query simulations from.
41
+ item (IEntity): An Experiment or Suite to filter simulations from.
42
+ tags (dict): A dictionary of tag key-value pairs to filter by for a simulation. Values may be:
43
+ * A fixed value (e.g., {"Run_Number": 2})
44
+ * A lambda or callable function for conditional logic
45
+ (e.g., {"Run_Number": lambda v: 2 <= v <= 10})
46
+ status (EntityStatus, Optional): The experiment's status.
47
+ entity_type (bool, optional): If True, return simulation entities instead of just their IDs.
48
+ skip_sims (list, optional): A list of simulation IDs (as strings) to exclude from the results.
49
+ max_simulations (int, optional): Maximum number of simulations to return. Returns all if not set.
50
+ **kwargs: Extra args.
51
+
52
+ Returns:
53
+ Union[List[Union[str, Simulation]], Dict[str, List[Union[str, Simulation]]]]:
54
+ If the item is an `Experiment`, returns a list of simulation IDs or simulation entities
55
+ (if `entity_type=True`).
56
+
57
+ If the item is a `Suite`, returns list ofdictionary where each key is an experiment ID and
58
+ each value is a list of simulation IDs or simulation entities (depending on the `entity_type` flag).
59
+
60
+ """
61
+ def match_tags(sim: IEntity, tags=None):
62
+ """
63
+ Check if simulation match tags.
64
+
65
+ Args:
66
+ sim: simulation
67
+ tags: search tags
68
+
69
+ Returns: bool True/False
70
+ """
71
+ # If no tags are provided, treat it as an empty filter (match all)
72
+ if tags is None:
73
+ return True
74
+ # Normalize simulation tag values and wrap them with TagValue for safe comparisons
75
+ # (e.g., allows "5" == 5 and supports operators like >, <, == in tag filters)
76
+ sim_tags = parse_value_tags(sim.tags, wrap_with_tagvalue=True)
77
+
78
+ # Iterate over each tag filter condition
79
+ for k, v in tags.items():
80
+ sim_val = sim_tags.get(k)
81
+ # If the simulation does not have the tag, skip it
82
+ if sim_val is None:
83
+ return False
84
+ # If the filter value is a callable (e.g., lambda), evaluate the condition
85
+ if callable(v):
86
+ if not v(sim_val):
87
+ return False
88
+ # Otherwise, do a direct comparison between the simulation tag and expected value
89
+ elif sim_val != v:
90
+ return False
91
+
92
+ return True
93
+
94
+ if item.item_type not in [ItemType.EXPERIMENT, ItemType.SUITE]:
95
+ raise ValueError("This method only supports Experiment and Suite types!")
96
+
97
+ if skip_sims is None:
98
+ skip_sims = []
99
+ # ------------------------------------------------------------------ #
100
+ # Base case ─ Experiment
101
+ # ------------------------------------------------------------------ #
102
+ if isinstance(item, Experiment):
103
+ potential_sims = item.get_simulations()
104
+
105
+ # filter by status
106
+ sims_status_filtered = [sim for sim in potential_sims if sim.status == status] if status else potential_sims
107
+
108
+ # filter tags
109
+ sims_tags_filtered = [sim for sim in sims_status_filtered if match_tags(sim, tags)]
110
+
111
+ # filter sims
112
+ sims_id_filtered = [sim for sim in sims_tags_filtered if str(sim.uid) not in skip_sims]
113
+
114
+ # consider max_simulations for return
115
+ sims_final = sims_id_filtered[0:max_simulations if max_simulations else len(sims_id_filtered)]
116
+
117
+ if entity_type:
118
+ return sims_final
119
+ else:
120
+ return [s.id for s in sims_final]
121
+
122
+ # Suite case:
123
+ experiments = item.get_experiments()
124
+ result = {}
125
+
126
+ for exp in experiments:
127
+ filtered = FilterItem.filter_item(
128
+ platform,
129
+ item=exp,
130
+ tags=tags,
131
+ status=status,
132
+ entity_type=entity_type,
133
+ max_simulations=max_simulations,
134
+ skip_sims=skip_sims,
135
+ **kwargs,
136
+ )
137
+ result[exp.id] = filtered
138
+ return result
139
+
140
+ @classmethod
141
+ def filter_item_by_id(cls, platform: IPlatform, item_id: str, item_type: ItemType = ItemType.EXPERIMENT,
142
+ tags=None, status=None, entity_type=False, skip_sims=None, max_simulations: int = None,
143
+ **kwargs):
144
+ """
145
+ Retrieve and filter simulations from an Experiment or Suite by item ID.
146
+
147
+ This method looks up the specified item (Experiment or Suite) by ID on the given platform,
148
+ then filters its simulations using the class's `filter_item()` method.
149
+
150
+ Args:
151
+ platform (IPlatform): The platform instance used to fetch the item.
152
+ item_id (str): The unique identifier of the Experiment or Suite.
153
+ item_type (ItemType, optional): The type of item (Experiment or Suite). Defaults to Experiment.
154
+ tags (dict, optional): A simulation's tags to filter by.
155
+ status (EntityStatus, Optional): The experiment's status.
156
+ entity_type (bool, optional): If True, return simulation entities instead of just their IDs.
157
+ skip_sims (List[str], optional): List of simulation IDs to skip during filtering. Defaults to an empty list.
158
+ max_simulations (int, optional): Maximum number of simulations to return. Defaults to None (no limit).
159
+ **kwargs: Additional keyword arguments passed to `filter_item()`.
160
+
161
+ Returns:
162
+ Union[List[Union[str, Simulation]], Dict[str, List[Union[str, Simulation]]]]:
163
+ If the item is an `Experiment`, returns a list of simulation IDs or simulation entities
164
+ (if `entity_type=True`).
165
+
166
+ If the item is a `Suite`, returns a dictionary where each key is an experiment ID and
167
+ each value is a list of simulation IDs or simulation entities (depending on the `entity_type` flag).
168
+
169
+ Raises:
170
+ ValueError: If the provided `item_type` is not Experiment or Suite.
171
+ """
172
+ if skip_sims is None:
173
+ skip_sims = []
174
+ if item_type not in [ItemType.EXPERIMENT, ItemType.SUITE]:
175
+ raise ValueError("This method only supports Experiment and Suite types!")
176
+
177
+ # retrieve item by id and type
178
+ item = platform.get_item(item_id, item_type, raw=False, force=True)
179
+
180
+ # filter simulations
181
+ return cls.filter_item(platform, item=item, tags=tags, status=status, entity_type=entity_type,
182
+ skip_sims=skip_sims, max_simulations=max_simulations, **kwargs)
@@ -0,0 +1,5 @@
1
+ """
2
+ defines filter utilities.
3
+
4
+ Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved.
5
+ """
@@ -0,0 +1,88 @@
1
+ """
2
+ This module contains all the default filters for the assets.
3
+
4
+ A filter function needs to take only one argument: an asset. It returns True/False indicating whether to add or filter
5
+ out the asset.
6
+
7
+ You can notice functions taking more than only an asset.
8
+ To use those functions, use must create a partial before adding it to a filters list.
9
+ For example::
10
+
11
+ python
12
+ fname = partial(file_name_is, filenames=["a.txt", "b.txt"])
13
+ AssetCollection.from_directory(... filters=[fname], ...)
14
+ """
15
+ import os
16
+ import typing
17
+
18
+ if typing.TYPE_CHECKING:
19
+ from typing import List
20
+ from idmtools.core import TAsset
21
+
22
+
23
+ def default_asset_file_filter(asset: 'TAsset') -> 'bool':
24
+ """
25
+ Default filter to leave out Python caching.
26
+
27
+ This filter is used in the creation of
28
+ :class:`~idmtools.assets.asset_collection.AssetCollection`, regardless of user filters.
29
+
30
+ Returns:
31
+ True if no files match default patterns of "__py_cache__" and ".pyc"
32
+ """
33
+ patterns = [
34
+ "__pycache__",
35
+ ".pyc"
36
+ ]
37
+ return not any([p in asset.absolute_path for p in patterns])
38
+
39
+
40
+ def file_name_is(asset: 'TAsset', filenames: 'List[str]') -> 'bool':
41
+ """
42
+ Restrict filtering to assets with the indicated filenames.
43
+
44
+ Args:
45
+ asset: The asset to filter.
46
+ filenames: List of filenames to filter on.
47
+
48
+ Returns:
49
+ True if asset.filename in filenames
50
+ """
51
+ return asset.filename in filenames
52
+
53
+
54
+ def file_extension_is(asset: 'TAsset', extensions: 'List[str]') -> 'bool':
55
+ """
56
+ Restrict filtering to assets with the indicated filetypes.
57
+
58
+ Args:
59
+ asset: The asset to filter.
60
+ extensions: List of extensions to filter on.
61
+
62
+ Returns:
63
+ True if extension in extensions
64
+ """
65
+ return asset.extension in extensions
66
+
67
+
68
+ def asset_in_directory(asset: 'TAsset', directories: 'List[str]', base_path: str = None) -> 'bool':
69
+ """
70
+ Restrict filtering to assets within a given directory.
71
+
72
+ This filter is not strict and simply checks if the directory portion is present in the assets absolute path.
73
+
74
+ Args:
75
+ asset: The asset to filter.
76
+ directories: List of directory portions to include.
77
+ base_path: base_path
78
+ """
79
+ if base_path is None:
80
+ base_path = os.getcwd()
81
+ norm_base_path = os.path.abspath(base_path)
82
+ norm_dirs = [f"{os.sep}{d}{os.sep}" for d in directories]
83
+ norm_asset_absolute_path = os.path.abspath(asset.absolute_path)
84
+ norm_root = norm_asset_absolute_path.replace(norm_base_path, "")
85
+
86
+ if not norm_asset_absolute_path.startswith(norm_base_path):
87
+ return False
88
+ return any([d in norm_root for d in norm_dirs])
@@ -0,0 +1,286 @@
1
+ """
2
+ Tag Parsing and Normalization Utilities.
3
+
4
+ This module provides helper functions and classes for converting string-based metadata tags
5
+ into native Python types (e.g., bool, int, float) to enable accurate filtering and comparison.
6
+
7
+ It includes:
8
+ - JSON serialization/deserialization of tags
9
+ - Conversion of sets to lists for JSON compatibility
10
+ - Coercion logic via TagValue for safe and flexible comparisons
11
+
12
+ Typical use cases include tag-based filtering of simulations or experiments during analysis.
13
+
14
+ Typical Use Case:
15
+ -----------------
16
+ Used during filtering of simulations or experiments in analysis pipelines where user-defined
17
+ tags are compared with numeric thresholds or exact matches.
18
+
19
+ Example:
20
+ --------
21
+ sim.tags={"Run_Number": lambda v: 4 <= v <= 10, "Coverage": "0.8"}
22
+ parsed = parse_value_tags(sim.tags, wrap_with_tagvalue=True)
23
+ assert parsed["Run_Number"] >=4 and <=10
24
+ assert parsed["Coverage"] == 0.8
25
+
26
+ Copyright 2025, Gates Foundation. All rights reserved.
27
+ """
28
+ import json
29
+ from typing import Dict
30
+
31
+ from idmtools.core.interfaces.ientity import IEntity
32
+
33
+
34
+ def parse_item_tags(item: IEntity):
35
+ """
36
+ Normalize and update an entity's tags in place.
37
+
38
+ This function parses the given item's tags using `parse_value_tags` and updates
39
+ the `item.tags` dictionary with the normalized values.
40
+
41
+ Args:
42
+ item: An entity object that contains a `.tags` dictionary.
43
+
44
+ Returns:
45
+ The same item, with its `.tags` dictionary updated in-place.
46
+ """
47
+ if item.tags is None:
48
+ return item
49
+ else:
50
+ item.tags = parse_value_tags(item.tags)
51
+ return item
52
+
53
+
54
+ def parse_value_tags(tags: Dict, wrap_with_tagvalue: bool = False) -> Dict[str, any]:
55
+ """
56
+ Parse and normalize a tag dictionary into native Python types.
57
+
58
+ Converts string tag values such as:
59
+ - "true"/"false" → bool
60
+ - "5" → int
61
+ - "0.8" → float
62
+ - sets → lists
63
+
64
+ Optionally wraps values using `TagValue` for comparison safety.
65
+
66
+ Args:
67
+ tags (dict): The dictionary of raw tag values to parse.
68
+ wrap_with_tagvalue (bool): If True, wraps each value in a TagValue object
69
+ for smart comparison support.
70
+
71
+ Returns:
72
+ dict: A dictionary with normalized or wrapped tag values.
73
+ """
74
+ # convert tags value as set to list first
75
+ tags = {k: list(v) if isinstance(v, set) else v for k, v in tags.items()}
76
+ tags_json = json.dumps(tags, cls=SetEncoder)
77
+ # Then: parse back into a dict with properly typed values
78
+ converted_tags = json.loads(tags_json, cls=CustomDecoder)
79
+ # Update result.tags in-place
80
+ for k, v in converted_tags.items():
81
+ converted = v
82
+ if wrap_with_tagvalue:
83
+ converted = TagValue(v)
84
+ tags[k] = converted
85
+ return tags
86
+
87
+
88
+ class SetEncoder(json.JSONEncoder):
89
+ """
90
+ Custom JSON encoder that converts Python sets into lists to ensure compatibility with JSON serialization.
91
+ """
92
+
93
+ def default(self, obj):
94
+ """
95
+ Override default encoding behavior.
96
+
97
+ Args:
98
+ obj: The object being encoded.
99
+
100
+ Returns:
101
+ JSON-compatible representation of the object.
102
+ """
103
+ if isinstance(obj, set):
104
+ return list(obj)
105
+ return super().default(obj)
106
+
107
+
108
+ class CustomDecoder(json.JSONDecoder):
109
+ """
110
+ Custom JSON decoder that converts string values into appropriate Python types (bool, int, float, None).
111
+ """
112
+
113
+ def __init__(self, *args, **kwargs): # noqa D415
114
+ # Optionally override object_hook here
115
+ super().__init__(object_hook=self.custom_object_hook, *args, **kwargs)
116
+
117
+ @staticmethod
118
+ def denormalize_tag_value(val):
119
+ """
120
+ Convert a raw string tag value into a Python-native type.
121
+
122
+ Supported conversions:
123
+ - "true"/"false" → bool
124
+ - "null"/"none" → None
125
+ - "5", "0.8" → int, float
126
+
127
+ Args:
128
+ val (Any): The value to convert.
129
+
130
+ Returns:
131
+ The normalized Python value.
132
+ """
133
+ if not isinstance(val, str):
134
+ return val # Already a native type
135
+
136
+ val_lower = val.strip().lower()
137
+ if val_lower == "true":
138
+ return True
139
+ elif val_lower == "false":
140
+ return False
141
+ elif val_lower in ("null", "none"):
142
+ return None
143
+
144
+ # Try integer/float
145
+ try:
146
+ if "." in val:
147
+ return float(val)
148
+ return int(val)
149
+ except ValueError:
150
+ pass
151
+
152
+ # Return as-is if it's not a simple number/bool/null
153
+ return val
154
+
155
+ def custom_object_hook(self, obj: Dict):
156
+ """
157
+ Apply normalization to each value in a decoded JSON object.
158
+
159
+ Args:
160
+ obj (dict): Dictionary of tag values.
161
+
162
+ Returns:
163
+ dict: Normalized tag dictionary.
164
+ """
165
+ # Modify dicts as they're loaded
166
+ new_obj = {}
167
+ for k, v in obj.items():
168
+ new_obj[k] = self.denormalize_tag_value(v)
169
+ return new_obj
170
+
171
+
172
+ class TagValue:
173
+ """
174
+ Wrapper for a tag value that supports smart comparisons.
175
+
176
+ Automatically converts strings like "5" or "0.8" to int/float
177
+ and enables comparison against numbers or other tag values.
178
+
179
+ Useful when users perform tag-based filtering with operators
180
+ like >, <, ==, etc., and tag values may be strings.
181
+
182
+ Attributes:
183
+ raw (Any): The original tag value before coercion.
184
+ """
185
+
186
+ def __init__(self, raw): # noqa D107
187
+ self.raw = raw
188
+
189
+ def _coerce(self, other):
190
+ """
191
+ Convert both self.raw and other to comparable types using the same logic as CustomDecoder.
192
+
193
+ Args:
194
+ other (Any): Value to compare to.
195
+
196
+ Returns:
197
+ Tuple[Any, Any]: Coerced values ready for comparison.
198
+ """
199
+ convert = CustomDecoder.denormalize_tag_value
200
+ return convert(self.raw), convert(other)
201
+
202
+ def __eq__(self, other): return self._coerce(other)[0] == self._coerce(other)[1] # noqa E704
203
+
204
+ def __lt__(self, other): return self._coerce(other)[0] < self._coerce(other)[1] # noqa E704
205
+
206
+ def __le__(self, other): return self._coerce(other)[0] <= self._coerce(other)[1] # noqa E704
207
+
208
+ def __gt__(self, other): return self._coerce(other)[0] > self._coerce(other)[1] # noqa E704
209
+
210
+ def __ge__(self, other): return self._coerce(other)[0] >= self._coerce(other)[1] # noqa E704
211
+
212
+ def __repr__(self): return repr(self.raw) # noqa E704
213
+
214
+ def __str__(self): return str(self.raw) # noqa E704
215
+
216
+
217
+ class FilterSafeItem:
218
+ """
219
+ A lightweight wrapper around an entity (e.g., Simulation or Experiment) to enable safe tag-based filtering during multiprocessing operations (e.g., within analyzers).
220
+
221
+ This wrapper:
222
+ - Normalizes and wraps tag values via `TagValue` to support flexible comparisons (e.g., >, ==).
223
+ - Supports safe pickling/unpickling by stripping unpickleable fields like `_platform`.
224
+ - Delegates attribute access to the original wrapped item via `__getattr__` (if implemented).
225
+
226
+ Typical usage:
227
+ safe_item = FilterSafeItem(simulation)
228
+ tags = safe_item.tags
229
+ # Now tags["Run_Number"] supports TagValue comparison logic
230
+
231
+ Attributes:
232
+ _item (IEntity): The original entity being wrapped.
233
+ """
234
+
235
+ def __init__(self, item):
236
+ """
237
+ Initialize the filter-safe wrapper with a given entity.
238
+
239
+ Args:
240
+ item (IEntity): The original simulation or experiment to wrap.
241
+ """
242
+ self._item = item
243
+
244
+ def __getstate__(self):
245
+ """
246
+ Prepare the object for pickling by removing unpickleable platform references.
247
+
248
+ Returns:
249
+ dict: The serializable state dictionary.
250
+ """
251
+ state = self.__dict__.copy()
252
+ for key in ['_platform', '_platform_object', '_parent']:
253
+ state.pop(key, None)
254
+ return state
255
+
256
+ def __getattr__(self, attr):
257
+ """
258
+ Delegate access to attributes not defined on the wrapper to the wrapped item.
259
+
260
+ This makes FilterSafeItem behave like the original simulation object for all
261
+ standard attributes (e.g., `id`, `status`, `experiment_id`, etc.).
262
+ This function makes simulation._item.id = simulation.id the same in filter function.
263
+
264
+ Returns:
265
+ The attribute value from the wrapped entity.
266
+ """
267
+ return getattr(self._item, attr)
268
+
269
+ def __setstate__(self, state):
270
+ """
271
+ Restore object state after unpickling.
272
+
273
+ Args:
274
+ state (dict): The state dictionary.
275
+ """
276
+ self.__dict__.update(state)
277
+
278
+ @property
279
+ def tags(self):
280
+ """
281
+ Get normalized tags from the wrapped item, with each value wrapped in TagValue for type-safe comparison.
282
+
283
+ Returns:
284
+ Dict[str, any]: Dictionary of tags.
285
+ """
286
+ return parse_value_tags(self._item.tags)