Creating new vendors/clock probe types based on config files.
This module provides functionality to create new vendor types for the openSAMPL
package based on YAML configuration files. It handles the generation of probe
classes, metadata classes, and updates to the constants file.
Note
This module is in beta and may change in future versions.
Default metadata fields for vendor configurations.
Source code in opensampl/create/create_vendor.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 | class DEFAULT_METADATA: # noqa N801
"""Default metadata fields for vendor configurations."""
ADDITIONAL_FIELDS = MetadataField(name="additional_metadata", sqlalchemy_type="JSONB")
@classmethod
def get_default_fields(cls) -> list[MetadataField]:
"""
Get list of default metadata fields.
Returns:
List of MetadataField instances for default fields.
"""
return [v for k, v in cls.__dict__.items() if isinstance(v, MetadataField)]
|
Get list of default metadata fields.
Returns:
Type |
Description |
list[MetadataField]
|
List of MetadataField instances for default fields.
|
Source code in opensampl/create/create_vendor.py
46
47
48
49
50
51
52
53
54
55 | @classmethod
def get_default_fields(cls) -> list[MetadataField]:
"""
Get list of default metadata fields.
Returns:
List of MetadataField instances for default fields.
"""
return [v for k, v in cls.__dict__.items() if isinstance(v, MetadataField)]
|
Bases: BaseModel
Definition for a metadata field in the vendor config.
Attributes:
Name |
Type |
Description |
name |
str
|
The name of the metadata field.
|
sqlalchemy_type |
Optional[str]
|
The SQLAlchemy type for the field (default: "Text").
|
primary_key |
Optional[bool]
|
Whether this field is a primary key (default: False).
|
Source code in opensampl/create/create_vendor.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38 | class MetadataField(BaseModel):
"""
Definition for a metadata field in the vendor config.
Attributes:
name: The name of the metadata field.
sqlalchemy_type: The SQLAlchemy type for the field (default: "Text").
primary_key: Whether this field is a primary key (default: False).
"""
name: str
sqlalchemy_type: Optional[str] = Field(default="Text")
primary_key: Optional[bool] = False
|
VendorConfig
Bases: VendorType
Configuration definition for a new vendor type.
Attributes:
Name |
Type |
Description |
base_path |
Path
|
Base path for the openSAMPL package.
|
Source code in opensampl/create/create_vendor.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239 | class VendorConfig(VendorType):
"""
Configuration definition for a new vendor type.
Attributes:
base_path: Base path for the openSAMPL package.
"""
base_path: Path = Path(__file__).parent.parent
metadata_fields: list[MetadataField]
@classmethod
def from_config_file(cls, config_path: Union[str, Path]) -> "VendorConfig":
"""
Convert file config into Config object.
Args:
config_path: Path to the YAML configuration file.
Returns:
VendorConfig instance created from the file.
"""
if isinstance(config_path, str):
config_path = Path(config_path)
config = yaml.safe_load(config_path.read_text())
return cls(**config)
@model_validator(mode="before")
@classmethod
def generate_default_fields(cls, data: Any) -> Any:
"""
Generate default values for fields if they are not provided in the config.
Args:
data: Raw configuration data.
Returns:
Updated configuration data with default values.
"""
if not isinstance(data, dict):
return data
name = data.get("name")
if not name:
return data
# Generate default values if not provided
if not data.get("metadata_table"):
data["metadata_table"] = f"{name.lower()}_metadata"
if not data.get("metadata_orm"):
data["metadata_orm"] = f"{name.capitalize()}Metadata"
if not data.get("parser_class"):
data["parser_class"] = f"{name.capitalize()}Probe"
if not data.get("parser_module"):
data["parser_module"] = f"{name.lower()}"
fields = []
metadata_fields = data.get("metadata_fields", None)
if isinstance(metadata_fields, list) and (len(metadata_fields) > 0 and isinstance(metadata_fields[0], dict)):
for field in metadata_fields:
if field.get("type", None) is not None:
fields.append(MetadataField(name=field.get("name"), sqlalchemy_type=field.get("type")))
else:
fields.append(MetadataField(name=field.get("name")))
else:
logger.warning(
"Metadata fields defaulting to probe uuid and freeform json: additional metadata. Either metadata "
"fields not provided or were malformed. "
)
fields.extend(list(DEFAULT_METADATA.get_default_fields()))
data["metadata_fields"] = fields
return data
def create_probe_file(self) -> Path:
"""
Create a new probe class file.
Returns:
Path to the created probe file.
"""
# Create the probe file
probe_file = self.base_path / "vendors" / f"{self.parser_module}.py"
# TODO in write time data, optionally add value_str to df ensure maximum precision when sending through backend.
template_file = Path(__file__).parent / "templates" / "parser_template.txt"
content = Template(template_file.read_text()).safe_substitute(
name=self.name, upper_name=self.parser_class.upper(), parser_class=self.parser_class
)
probe_file.write_text(content)
logger.warning(
f"Wrote {self.parser_class} to {probe_file}. Open the file, and follow TODO instructions to implement."
)
return probe_file
def generate_metadata_columns(self) -> str:
"""
Generate the metadata column definitions for the ORM template.
Returns:
String containing formatted column definitions.
"""
columns = [f" {field.name} = Column({field.sqlalchemy_type})" for field in self.metadata_fields]
return "\n".join(columns)
def create_metadata_class(self) -> str:
"""
Create the metadata class using template-based approach.
Returns:
String containing the ORM class definition.
"""
template_file = Path(__file__).parent / "templates" / "orm_metadata.txt"
metadata_columns = self.generate_metadata_columns()
return Template(template_file.read_text()).safe_substitute(
metadata_orm=self.metadata_orm,
metadata_table=self.metadata_table,
metadata_columns=metadata_columns,
name_lower=self.name.lower(),
)
@staticmethod
def insert_content_at_marker(marker: InsertMarker, content: str) -> None:
"""
Insert content at a specified marker in a file.
Args:
marker: InsertMarker defining where to insert content.
content: Content to insert at the marker location.
"""
target_file = marker.filepath
output_lines = []
inserted = False
for line in target_file.read_text().splitlines():
output_lines.append(line)
if not inserted and marker.comment_marker in line:
output_lines.append(content)
inserted = True
if not inserted:
logger.warning(f"Marker '{marker.comment_marker}' not found in {target_file}")
return
target_file.write_text("\n".join(output_lines))
logger.info(f"Content inserted at marker in {target_file}")
def create_orm_class(self):
"""Create the ORM metadata class in the database ORM file."""
orm_content = self.create_metadata_class()
self.insert_content_at_marker(INSERT_MARKERS.ORM_CLASS, orm_content)
logger.info(f"Created ORM class {self.metadata_orm} in database")
def update_constants(self):
"""Update the constants.py file with the new vendor type."""
template_file = INSERT_MARKERS.VENDOR.template_path
content = Template(template_file.read_text()).safe_substitute(
upper_name=self.parser_class.upper(),
name=self.name,
parser_class=self.parser_class,
parser_module=self.parser_module,
metadata_table=self.metadata_table,
metadata_orm=self.metadata_orm,
)
self.insert_content_at_marker(INSERT_MARKERS.VENDOR, content)
def create(self):
"""Create the new vendor by generating probe file, ORM class, and updating constants."""
self.create_probe_file()
self.create_orm_class()
self.update_constants()
|
create()
Create the new vendor by generating probe file, ORM class, and updating constants.
Source code in opensampl/create/create_vendor.py
| def create(self):
"""Create the new vendor by generating probe file, ORM class, and updating constants."""
self.create_probe_file()
self.create_orm_class()
self.update_constants()
|
Create the metadata class using template-based approach.
Returns:
Type |
Description |
str
|
String containing the ORM class definition.
|
Source code in opensampl/create/create_vendor.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187 | def create_metadata_class(self) -> str:
"""
Create the metadata class using template-based approach.
Returns:
String containing the ORM class definition.
"""
template_file = Path(__file__).parent / "templates" / "orm_metadata.txt"
metadata_columns = self.generate_metadata_columns()
return Template(template_file.read_text()).safe_substitute(
metadata_orm=self.metadata_orm,
metadata_table=self.metadata_table,
metadata_columns=metadata_columns,
name_lower=self.name.lower(),
)
|
create_orm_class()
Create the ORM metadata class in the database ORM file.
Source code in opensampl/create/create_vendor.py
| def create_orm_class(self):
"""Create the ORM metadata class in the database ORM file."""
orm_content = self.create_metadata_class()
self.insert_content_at_marker(INSERT_MARKERS.ORM_CLASS, orm_content)
logger.info(f"Created ORM class {self.metadata_orm} in database")
|
create_probe_file()
Create a new probe class file.
Returns:
Type |
Description |
Path
|
Path to the created probe file.
|
Source code in opensampl/create/create_vendor.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158 | def create_probe_file(self) -> Path:
"""
Create a new probe class file.
Returns:
Path to the created probe file.
"""
# Create the probe file
probe_file = self.base_path / "vendors" / f"{self.parser_module}.py"
# TODO in write time data, optionally add value_str to df ensure maximum precision when sending through backend.
template_file = Path(__file__).parent / "templates" / "parser_template.txt"
content = Template(template_file.read_text()).safe_substitute(
name=self.name, upper_name=self.parser_class.upper(), parser_class=self.parser_class
)
probe_file.write_text(content)
logger.warning(
f"Wrote {self.parser_class} to {probe_file}. Open the file, and follow TODO instructions to implement."
)
return probe_file
|
from_config_file(config_path)
classmethod
Convert file config into Config object.
Parameters:
Name |
Type |
Description |
Default |
config_path
|
Union[str, Path]
|
Path to the YAML configuration file.
|
required
|
Returns:
Type |
Description |
VendorConfig
|
VendorConfig instance created from the file.
|
Source code in opensampl/create/create_vendor.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85 | @classmethod
def from_config_file(cls, config_path: Union[str, Path]) -> "VendorConfig":
"""
Convert file config into Config object.
Args:
config_path: Path to the YAML configuration file.
Returns:
VendorConfig instance created from the file.
"""
if isinstance(config_path, str):
config_path = Path(config_path)
config = yaml.safe_load(config_path.read_text())
return cls(**config)
|
generate_default_fields(data)
classmethod
Generate default values for fields if they are not provided in the config.
Parameters:
Name |
Type |
Description |
Default |
data
|
Any
|
|
required
|
Returns:
Type |
Description |
Any
|
Updated configuration data with default values.
|
Source code in opensampl/create/create_vendor.py
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135 | @model_validator(mode="before")
@classmethod
def generate_default_fields(cls, data: Any) -> Any:
"""
Generate default values for fields if they are not provided in the config.
Args:
data: Raw configuration data.
Returns:
Updated configuration data with default values.
"""
if not isinstance(data, dict):
return data
name = data.get("name")
if not name:
return data
# Generate default values if not provided
if not data.get("metadata_table"):
data["metadata_table"] = f"{name.lower()}_metadata"
if not data.get("metadata_orm"):
data["metadata_orm"] = f"{name.capitalize()}Metadata"
if not data.get("parser_class"):
data["parser_class"] = f"{name.capitalize()}Probe"
if not data.get("parser_module"):
data["parser_module"] = f"{name.lower()}"
fields = []
metadata_fields = data.get("metadata_fields", None)
if isinstance(metadata_fields, list) and (len(metadata_fields) > 0 and isinstance(metadata_fields[0], dict)):
for field in metadata_fields:
if field.get("type", None) is not None:
fields.append(MetadataField(name=field.get("name"), sqlalchemy_type=field.get("type")))
else:
fields.append(MetadataField(name=field.get("name")))
else:
logger.warning(
"Metadata fields defaulting to probe uuid and freeform json: additional metadata. Either metadata "
"fields not provided or were malformed. "
)
fields.extend(list(DEFAULT_METADATA.get_default_fields()))
data["metadata_fields"] = fields
return data
|
Generate the metadata column definitions for the ORM template.
Returns:
Type |
Description |
str
|
String containing formatted column definitions.
|
Source code in opensampl/create/create_vendor.py
160
161
162
163
164
165
166
167
168
169 | def generate_metadata_columns(self) -> str:
"""
Generate the metadata column definitions for the ORM template.
Returns:
String containing formatted column definitions.
"""
columns = [f" {field.name} = Column({field.sqlalchemy_type})" for field in self.metadata_fields]
return "\n".join(columns)
|
insert_content_at_marker(marker, content)
staticmethod
Insert content at a specified marker in a file.
Parameters:
Name |
Type |
Description |
Default |
marker
|
InsertMarker
|
InsertMarker defining where to insert content.
|
required
|
content
|
str
|
Content to insert at the marker location.
|
required
|
Source code in opensampl/create/create_vendor.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214 | @staticmethod
def insert_content_at_marker(marker: InsertMarker, content: str) -> None:
"""
Insert content at a specified marker in a file.
Args:
marker: InsertMarker defining where to insert content.
content: Content to insert at the marker location.
"""
target_file = marker.filepath
output_lines = []
inserted = False
for line in target_file.read_text().splitlines():
output_lines.append(line)
if not inserted and marker.comment_marker in line:
output_lines.append(content)
inserted = True
if not inserted:
logger.warning(f"Marker '{marker.comment_marker}' not found in {target_file}")
return
target_file.write_text("\n".join(output_lines))
logger.info(f"Content inserted at marker in {target_file}")
|
update_constants()
Update the constants.py file with the new vendor type.
Source code in opensampl/create/create_vendor.py
222
223
224
225
226
227
228
229
230
231
232
233 | def update_constants(self):
"""Update the constants.py file with the new vendor type."""
template_file = INSERT_MARKERS.VENDOR.template_path
content = Template(template_file.read_text()).safe_substitute(
upper_name=self.parser_class.upper(),
name=self.name,
parser_class=self.parser_class,
parser_module=self.parser_module,
metadata_table=self.metadata_table,
metadata_orm=self.metadata_orm,
)
self.insert_content_at_marker(INSERT_MARKERS.VENDOR, content)
|