soft7 0.1.0__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.
s7/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """SOFT7"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ __version__ = "0.1.0"
8
+
9
+ logging.getLogger("s7").setLevel(logging.DEBUG)
s7/exceptions.py ADDED
@@ -0,0 +1,35 @@
1
+ """SOFT7 exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class S7Error(Exception):
7
+ """Base class for all SOFT7 exceptions."""
8
+
9
+
10
+ class S7EntityError(S7Error):
11
+ """Base class for all SOFT7 entity exceptions."""
12
+
13
+
14
+ class EntityNotFound(S7EntityError, FileNotFoundError):
15
+ """Raised when an entity is or can not be found."""
16
+
17
+
18
+ class ConfigsNotFound(S7EntityError, FileNotFoundError):
19
+ """Raised when the configs are or can not be found."""
20
+
21
+
22
+ class S7OTEAPIPluginError(S7Error):
23
+ """Base class for all SOFT7 OTEAPI plugin exceptions."""
24
+
25
+
26
+ class SOFT7FunctionError(S7OTEAPIPluginError):
27
+ """Base class for all JSON to SOFT7 Entity OTEAPI parse strategy exceptions."""
28
+
29
+
30
+ class InsufficientData(SOFT7FunctionError, ValueError):
31
+ """Raised when the data is insufficient to generate a SOFT7 entity."""
32
+
33
+
34
+ class InvalidMapping(SOFT7FunctionError, ValueError):
35
+ """Raised when the mapping is invalid."""
@@ -0,0 +1,8 @@
1
+ """Public factory functions for creating SOFT7-related classes and instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .datasource_factory import create_datasource
6
+ from .entity_factory import create_entity
7
+
8
+ __all__ = ("create_datasource", "create_entity")
@@ -0,0 +1,347 @@
1
+ """Generate SOFT7 entity instances based on basic information:
2
+
3
+ 1. Data source (DB, File, Webpage, ...).
4
+ 2. Generic data source parser.
5
+ 3. Data source parser configuration.
6
+ 4. SOFT7 entity (data model).
7
+
8
+ Parts 2 and 3 are together considered to produce the "specific parser".
9
+ Parts 1 through 3 are provided through a single dictionary based on the
10
+ `ResourceConfig` from `oteapi.models`.
11
+
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Optional
20
+
21
+ from otelib import OTEClient
22
+ from pydantic import AnyUrl, Field, create_model
23
+
24
+ from s7.pydantic_models.datasource import (
25
+ DataSourceDimensions,
26
+ SOFT7DataSource,
27
+ parse_input_configs,
28
+ )
29
+ from s7.pydantic_models.soft7_entity import (
30
+ SOFT7Entity,
31
+ parse_identity,
32
+ parse_input_entity,
33
+ )
34
+ from s7.pydantic_models.soft7_instance import (
35
+ generate_dimensions_docstring,
36
+ generate_model_docstring,
37
+ generate_property_type,
38
+ )
39
+
40
+ if TYPE_CHECKING: # pragma: no cover
41
+ from typing import Any, TypedDict
42
+
43
+ from oteapi.models import GenericConfig
44
+
45
+ from s7.pydantic_models.datasource import (
46
+ GetData,
47
+ GetDataConfigDict,
48
+ )
49
+ from s7.pydantic_models.soft7_entity import PropertyType
50
+
51
+ class SOFT7InstanceDict(TypedDict):
52
+ """A dictionary representation of a SOFT7 instance."""
53
+
54
+ dimensions: Optional[dict[str, int]]
55
+ properties: dict[str, Any]
56
+
57
+
58
+ DEFAULT_OTEAPI_SERVICES_BASE_URL = "http://localhost:8080"
59
+
60
+ LOGGER = logging.getLogger(__name__)
61
+
62
+ CACHE: dict[int, dict[str, Any]] = {}
63
+ """A cache of the OTEAPI pipeline results."""
64
+
65
+
66
+ def create_pipeline_id(configs: GetDataConfigDict, url: str) -> int:
67
+ """Hash given inputs and return it."""
68
+ return hash((*((key, configs[key]) for key in sorted(configs)), url)) # type: ignore[literal-required]
69
+
70
+
71
+ def _get_data(
72
+ config: GetDataConfigDict,
73
+ *,
74
+ url: str | None = None,
75
+ ) -> GetData:
76
+ """Get a datum via an OTEAPI pipeline.
77
+
78
+ The OTEAPI pipeline will look like:
79
+
80
+ DataResource -> Parser -> Mapping -> Function
81
+
82
+ It may be that the Mapping strategy is left out.
83
+
84
+ Everything in the __get_data() function will not be called until the first time
85
+ the attribute `name` is accessed.
86
+
87
+ To do something "lazy", it should be moved to the __get_data() function.
88
+ To instead pre-cache something, it should be moved outside of the __get_data()
89
+ function.
90
+
91
+ Parameters:
92
+ config: Mapping for all necessary OTEAPI strategy configuration.
93
+ url: The base URL of the OTEAPI service instance to use.
94
+
95
+ Returns:
96
+ A function that will return the value of a named dimension or property.
97
+
98
+ """
99
+ # OTEAPI pipeline configuration
100
+ client = OTEClient(url or DEFAULT_OTEAPI_SERVICES_BASE_URL)
101
+
102
+ ote_data_resource = client.create_dataresource(
103
+ **config["dataresource"].model_dump()
104
+ )
105
+ ote_function = client.create_function(**config["function"].model_dump())
106
+ ote_parser = client.create_parser(**config["parser"].model_dump())
107
+
108
+ if "mapping" in config:
109
+ ote_mapping = client.create_mapping(**config["mapping"].model_dump()) # type: ignore[typeddict-item]
110
+
111
+ ote_pipeline = ote_data_resource >> ote_parser >> ote_mapping >> ote_function
112
+ else:
113
+ raise NotImplementedError(
114
+ "Only OTEAPI pipelines with a mapping are supported for now, i.e., "
115
+ "implicit 1:1 mapping is currently not supported."
116
+ )
117
+ # ote_pipeline = ote_data_resource >> ote_parser >> ote_function
118
+
119
+ # Remove unused variables from memory
120
+ del client
121
+
122
+ pipeline_id = create_pipeline_id(config, url or DEFAULT_OTEAPI_SERVICES_BASE_URL)
123
+
124
+ def __get_data(soft7_property: str) -> Any:
125
+ """Get a named datum (property or dimension) from the data resource.
126
+
127
+ Properties:
128
+ soft7_property: The name of a datum to get, i.e., the SOFT7 data resource
129
+ (property or dimension).
130
+
131
+ Returns:
132
+ The value of the SOFT7 data resource (property or dimension).
133
+
134
+ """
135
+ LOGGER.debug("soft7_property: %r", soft7_property)
136
+
137
+ if pipeline_id not in CACHE:
138
+ LOGGER.debug(
139
+ "Running OTEAPI pipeline: %r (id: %r)", ote_pipeline, pipeline_id
140
+ )
141
+ # Should only run once per pipeline - after that we retrieve from the cache
142
+ pipeline_result: dict[str, Any] = json.loads(ote_pipeline.get())
143
+ CACHE[pipeline_id] = pipeline_result
144
+ else:
145
+ pipeline_result = CACHE[pipeline_id]
146
+
147
+ LOGGER.debug("Pipeline result: %r", pipeline_result)
148
+
149
+ # TODO: Use variable from SOFT7 OTEAPI Function configuration instead of
150
+ # 'soft7_entity_data'. Maybe even consider parsing `pipeline_result` into the
151
+ # eventual Session model to thereby validate the desired keys are present.
152
+ if "soft7_entity_data" not in pipeline_result:
153
+ error_message = (
154
+ "The OTEAPI pipeline did not return the expected data structure."
155
+ )
156
+ LOGGER.error(
157
+ "%s\nsoft7_property: %r\npipeline_result: %r",
158
+ error_message,
159
+ soft7_property,
160
+ pipeline_result,
161
+ )
162
+ raise ValueError(error_message)
163
+
164
+ data: SOFT7InstanceDict = pipeline_result["soft7_entity_data"]
165
+
166
+ if soft7_property in data["properties"]:
167
+ LOGGER.debug(
168
+ "Returning property: %r = %r",
169
+ soft7_property,
170
+ data["properties"][soft7_property],
171
+ )
172
+ return data["properties"][soft7_property]
173
+
174
+ if (
175
+ "dimensions" in data
176
+ and data["dimensions"]
177
+ and soft7_property in data["dimensions"]
178
+ ):
179
+ LOGGER.debug(
180
+ "Returning dimension: %r = %r",
181
+ soft7_property,
182
+ data["dimensions"][soft7_property],
183
+ )
184
+ return data["dimensions"][soft7_property]
185
+
186
+ error_message = f"{soft7_property!r} could not be determined for the resource."
187
+ LOGGER.error(
188
+ "%s\nsoft7_property: %r\ndata: %r",
189
+ error_message,
190
+ soft7_property,
191
+ data,
192
+ )
193
+ raise AttributeError(error_message)
194
+
195
+ return __get_data
196
+
197
+
198
+ def create_datasource(
199
+ entity: SOFT7Entity | dict[str, Any] | Path | AnyUrl | str,
200
+ configs: (
201
+ GetDataConfigDict
202
+ | dict[str, GenericConfig | dict[str, Any] | Path | AnyUrl | str]
203
+ | Path
204
+ | AnyUrl
205
+ | str
206
+ ),
207
+ oteapi_url: str | None = None,
208
+ ) -> SOFT7DataSource:
209
+ """Create and return an instance of a SOFT7 Data Source wrapped as a pydantic model.
210
+
211
+ TODO: Utilize the `generated_classes` module and check whether we can return an
212
+ already created model based on the inputs given here.
213
+
214
+ TODO: Determine what to do with regards to differing inputs, but similar names.
215
+
216
+ Parameters:
217
+ entity: A SOFT7 entity (data model). It can be supplied as a URL reference,
218
+ path or as a raw JSON/YAML string or Python `dict`.
219
+ configs: A dictionary of the various required OTEAPI strategy configurations
220
+ needed for the underlying OTEAPI pipeline. It can be supplied as a URL
221
+ reference, path or as a raw JSON/YAML string or Python `dict`.
222
+ oteapi_url: The base URL of the OTEAPI service to use.
223
+
224
+ Returns:
225
+ An instance of a SOFT7 Data Source wrapped as a pydantic data model.
226
+
227
+ """
228
+ import s7.factories.generated_classes as module_namespace
229
+
230
+ entity = parse_input_entity(entity)
231
+ configs = parse_input_configs(configs, entity_instance=entity.identity)
232
+
233
+ # Split the identity into its parts
234
+ namespace, version, name = parse_identity(entity.identity)
235
+
236
+ # Setup the OTEAPI pipeline configuration
237
+ get_piped_data = lambda: _get_data(configs, url=oteapi_url) # noqa: E731
238
+
239
+ # Create the dimensions model
240
+ dimensions: dict[str, tuple[type[int], GetData]] = (
241
+ # Value must be a (<type>, <default>) or (<type>, <FieldInfo>) tuple
242
+ # Note, Field() returns a FieldInfo instance (but is set to return an Any type).
243
+ {
244
+ dimension_name: (
245
+ int,
246
+ Field(
247
+ default_factory=get_piped_data,
248
+ description=dimension_description,
249
+ ),
250
+ )
251
+ for dimension_name, dimension_description in entity.dimensions.items()
252
+ }
253
+ if entity.dimensions
254
+ else {}
255
+ )
256
+
257
+ DataSourceDimensionsModel = create_model(
258
+ f"{name.replace(' ', '')}DataSourceDimensions",
259
+ __config__=None,
260
+ __doc__=generate_dimensions_docstring(entity),
261
+ __base__=DataSourceDimensions,
262
+ __module__=module_namespace.__name__,
263
+ __validators__=None,
264
+ __cls_kwargs__=None,
265
+ **dimensions,
266
+ )
267
+
268
+ data_source_dimensions = DataSourceDimensionsModel()
269
+
270
+ # Create the SOFT7 metadata fields for the data source model
271
+ # All of these fields will be excluded from the data source model representation as
272
+ # well as the serialized JSON schema or Python dictionary.
273
+ soft7_metadata: dict[str, tuple[type | object, Any]] = {
274
+ # Value must be a (<type>, <default>) or (<type>, <FieldInfo>) tuple
275
+ # Note, Field() returns a FieldInfo instance (but is set to return an Any type).
276
+ "dimensions": (
277
+ DataSourceDimensionsModel,
278
+ Field(data_source_dimensions, repr=False, exclude=True),
279
+ ),
280
+ "identity": (
281
+ entity.model_fields["identity"].rebuild_annotation(),
282
+ Field(entity.identity, repr=False, exclude=True),
283
+ ),
284
+ "namespace": (AnyUrl, Field(namespace, repr=False, exclude=True)),
285
+ "version": (Optional[str], Field(version, repr=False, exclude=True)),
286
+ "name": (str, Field(name, repr=False, exclude=True)),
287
+ }
288
+
289
+ # Pre-calculate property types
290
+ property_types: dict[str, type[PropertyType]] = {
291
+ property_name: generate_property_type(property_value, data_source_dimensions)
292
+ for property_name, property_value in entity.properties.items()
293
+ }
294
+
295
+ # Create the data source model's properties
296
+ properties: dict[str, tuple[type[PropertyType], GetData]] = {
297
+ # Value must be a (<type>, <default>) or (<type>, <FieldInfo>) tuple
298
+ # Note, Field() returns a FieldInfo instance (but is set to return an Any type).
299
+ property_name: (
300
+ property_types[property_name],
301
+ Field(
302
+ default_factory=get_piped_data,
303
+ description=property_value.description or "",
304
+ title=property_name.replace(" ", "_"),
305
+ json_schema_extra={
306
+ f"x-soft7-{field}": getattr(property_value, field)
307
+ for field in property_value.model_fields
308
+ if (
309
+ field not in ("description", "type")
310
+ and getattr(property_value, field)
311
+ )
312
+ },
313
+ ),
314
+ )
315
+ for property_name, property_value in entity.properties.items()
316
+ }
317
+
318
+ # Ensure there is no overlap between the SOFT7 metadata fields and the data source
319
+ # model properties and the data source model properties are not trying to hack the
320
+ # attribute retrieval mechanism.
321
+ if any(field.startswith("soft7___") for field in properties):
322
+ raise ValueError(
323
+ "The data model properties are not allowed to overwrite or mock SOFT7 "
324
+ "metadata fields."
325
+ )
326
+
327
+ DataSourceModel = create_model(
328
+ f"{name.replace(' ', '')}DataSource",
329
+ __config__=None,
330
+ __doc__=generate_model_docstring(entity, property_types),
331
+ __base__=SOFT7DataSource,
332
+ __module__=module_namespace.__name__,
333
+ __validators__=None,
334
+ __cls_kwargs__=None,
335
+ **{
336
+ # SOFT7 metadata fields
337
+ **{f"soft7___{name}": value for name, value in soft7_metadata.items()},
338
+ # Data source properties
339
+ **properties,
340
+ },
341
+ )
342
+
343
+ # Register the classes with the generated_classes globals
344
+ module_namespace.register_class(DataSourceDimensionsModel)
345
+ module_namespace.register_class(DataSourceModel)
346
+
347
+ return DataSourceModel() # type: ignore[call-arg]
@@ -0,0 +1,165 @@
1
+ """Generate SOFT7 entities, wrapped as pydantic data models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Optional
8
+
9
+ from pydantic import AnyUrl, ConfigDict, Field, create_model
10
+
11
+ from s7.pydantic_models.soft7_entity import (
12
+ SOFT7Entity,
13
+ parse_identity,
14
+ parse_input_entity,
15
+ )
16
+ from s7.pydantic_models.soft7_instance import (
17
+ SOFT7EntityInstance,
18
+ generate_dimensions_docstring,
19
+ generate_list_property_type,
20
+ generate_model_docstring,
21
+ generate_properties_docstring,
22
+ )
23
+
24
+ if TYPE_CHECKING: # pragma: no cover
25
+ from typing import Any, Union
26
+
27
+ from s7.pydantic_models.soft7_entity import (
28
+ ListPropertyType,
29
+ )
30
+
31
+
32
+ LOGGER = logging.getLogger(__name__)
33
+
34
+
35
+ def create_entity(
36
+ entity: Union[SOFT7Entity, dict[str, Any], Path, AnyUrl, str],
37
+ ) -> type[SOFT7EntityInstance]:
38
+ """Create and return a SOFT7 entity as a pydantic model.
39
+
40
+ TODO: Utilize the `generated_classes` module and check whether we can return an
41
+ already created model based on the inputs given here.
42
+
43
+ TODO: Determine what to do with regards to differing inputs, but similar names.
44
+
45
+ Parameters:
46
+ entity: A SOFT7 entity (data model). It can be supplied as a URL reference,
47
+ path or as a raw JSON/YAML string or Python `dict`.
48
+
49
+ Returns:
50
+ A SOFT7 entity as a pydantic model.
51
+
52
+ """
53
+ import s7.factories.generated_classes as module_namespace
54
+
55
+ # Parse the input entity
56
+ entity = parse_input_entity(entity)
57
+
58
+ # Split the identity into its parts
59
+ _, _, name = parse_identity(entity.identity)
60
+
61
+ # Create the entity model's dimensions
62
+ dimensions: dict[str, tuple[Union[type[Optional[int]], object], Any]] = (
63
+ # Value must be a (<type>, <default>) or (<type>, <FieldInfo>) tuple
64
+ # Note, Field() returns a FieldInfo instance (but is set to return an Any type).
65
+ {
66
+ dimension_name: (
67
+ Optional[int],
68
+ Field(None, description=dimension_description),
69
+ )
70
+ for dimension_name, dimension_description in entity.dimensions.items()
71
+ }
72
+ if entity.dimensions
73
+ else {}
74
+ )
75
+
76
+ if dimensions:
77
+ Dimensions = create_model(
78
+ f"{name.replace(' ', '')}EntityDimensions",
79
+ __config__=ConfigDict(extra="forbid", frozen=True, validate_default=False),
80
+ __doc__=generate_dimensions_docstring(entity),
81
+ __base__=None,
82
+ __module__=module_namespace.__name__,
83
+ __validators__=None,
84
+ __cls_kwargs__=None,
85
+ **dimensions,
86
+ )
87
+
88
+ # Pre-calculate property types
89
+ property_types: dict[str, type[ListPropertyType]] = {
90
+ property_name: generate_list_property_type(property_value)
91
+ for property_name, property_value in entity.properties.items()
92
+ }
93
+
94
+ # Create the entity model's properties
95
+ properties: dict[
96
+ str, tuple[Union[type[Optional[ListPropertyType]], object], Any]
97
+ ] = {
98
+ # Value must be a (<type>, <default>) or (<type>, <FieldInfo>) tuple
99
+ # Note, Field() returns a FieldInfo instance (but is set to return an Any type).
100
+ property_name: (
101
+ Optional[property_types[property_name]],
102
+ Field(
103
+ None,
104
+ description=property_value.description or "",
105
+ title=property_name.replace(" ", "_"),
106
+ json_schema_extra={
107
+ f"x-soft7-{field}": getattr(property_value, field)
108
+ for field in property_value.model_fields
109
+ if (
110
+ field not in ("description", "type")
111
+ and getattr(property_value, field)
112
+ )
113
+ },
114
+ ),
115
+ )
116
+ for property_name, property_value in entity.properties.items()
117
+ }
118
+
119
+ Properties = create_model(
120
+ f"{name.replace(' ', '')}EntityProperties",
121
+ __config__=ConfigDict(extra="forbid", frozen=True, validate_default=False),
122
+ __doc__=generate_properties_docstring(entity, property_types),
123
+ __base__=None,
124
+ __module__=module_namespace.__name__,
125
+ __validators__=None,
126
+ __cls_kwargs__=None,
127
+ **properties,
128
+ )
129
+
130
+ # Generate the fields_definitions for the final model
131
+ fields_definitions: dict[str, Any] = {
132
+ # Value must be a (<type>, <default>) or (<type>, <FieldInfo>) tuple
133
+ # Note, Field() returns a FieldInfo instance (but is set to return an Any type).
134
+ "properties": (
135
+ Properties,
136
+ Field(description=f"The {name} SOFT7 entity properties."),
137
+ )
138
+ }
139
+ if dimensions:
140
+ fields_definitions["dimensions"] = (
141
+ Dimensions,
142
+ Field(description=f"The {name} SOFT7 entity dimensions."),
143
+ )
144
+
145
+ EntityInstance = create_model(
146
+ f"{name.replace(' ', '')}Entity",
147
+ __config__=None,
148
+ __doc__=generate_model_docstring(entity, property_types),
149
+ __base__=SOFT7EntityInstance,
150
+ __module__=module_namespace.__name__,
151
+ __validators__=None,
152
+ __cls_kwargs__=None,
153
+ **fields_definitions,
154
+ )
155
+
156
+ # Set the entity class variable
157
+ EntityInstance.entity = entity
158
+
159
+ # Register the classes with the generated_classes globals
160
+ if dimensions:
161
+ module_namespace.register_class(Dimensions)
162
+ module_namespace.register_class(Properties)
163
+ module_namespace.register_class(EntityInstance)
164
+
165
+ return EntityInstance
@@ -0,0 +1,13 @@
1
+ """A module to import certain factory-generated classes from."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def register_class(cls: type) -> None:
7
+ """Register a class with the module's globals.
8
+
9
+ Parameters:
10
+ cls: The class to register.
11
+
12
+ """
13
+ globals()[cls.__name__] = cls
File without changes
@@ -0,0 +1,101 @@
1
+ """Pydantic data models for the SOFT7 OTEAPI plugin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import Annotated, Any, Optional
7
+
8
+ if sys.version_info >= (3, 10):
9
+ from typing import Literal
10
+ else:
11
+ from typing_extensions import Literal
12
+
13
+ from oteapi.models import AttrDict
14
+ from oteapi.strategies.mapping.mapping import MappingStrategyConfig
15
+ from pydantic import Field, field_validator
16
+
17
+ from s7.exceptions import EntityNotFound
18
+ from s7.factories import create_entity
19
+ from s7.pydantic_models.oteapi import HashableFunctionConfig
20
+ from s7.pydantic_models.soft7_entity import parse_identity, parse_input_entity
21
+ from s7.pydantic_models.soft7_instance import SOFT7EntityInstance
22
+
23
+ PrefixesType: Any = MappingStrategyConfig.model_fields["prefixes"].rebuild_annotation()
24
+ TriplesType: Any = MappingStrategyConfig.model_fields["triples"].rebuild_annotation()
25
+
26
+
27
+ class SOFT7GeneratorConfig(AttrDict):
28
+ """SOFT7 Generator strategy-specific configuration.
29
+
30
+ Inherit from the MappingStrategyConfig to include the prefixes and triples fields,
31
+ as well as any connected validation or serialization functionality that may be part
32
+ of the MappingStrategyConfig.
33
+ """
34
+
35
+ entity: Annotated[
36
+ type[SOFT7EntityInstance],
37
+ Field(description="The SOFT7 entity to be used for the generator."),
38
+ ]
39
+
40
+ # Data mapping information
41
+ # Field added from a mapping strategy.
42
+ prefixes: Annotated[
43
+ Optional[PrefixesType],
44
+ Field(description=MappingStrategyConfig.model_fields["prefixes"].description),
45
+ ] = None
46
+ triples: Annotated[
47
+ Optional[TriplesType],
48
+ Field(description=MappingStrategyConfig.model_fields["triples"].description),
49
+ ] = None
50
+
51
+ # Parsed data content
52
+ # Field added from a parser strategy.
53
+ content: Annotated[
54
+ Optional[dict],
55
+ Field(description="The parsed data content to be used for the generator."),
56
+ ] = None
57
+
58
+ @field_validator("entity", mode="before")
59
+ @classmethod
60
+ def ensure_entity_is_cls(cls, value: Any) -> type[SOFT7EntityInstance]:
61
+ """Ensure the given entity is a SOFT7EntityInstance class."""
62
+ if isinstance(value, type) and issubclass(value, SOFT7EntityInstance):
63
+ # The entity is already a SOFT7EntityInstance (or subclass) type.
64
+ return value
65
+
66
+ # The entity is not a SOFT7EntityInstance (or subclass) type.
67
+ # Try to parse it as a SOFT7 Entity.
68
+ try:
69
+ entity = parse_input_entity(value)
70
+ except (TypeError, EntityNotFound) as exc:
71
+ raise ValueError(
72
+ f"Invalid value {value!r} for the 'entity' field in "
73
+ f"SOFT7GeneratorConfig. Internal error: {exc}"
74
+ ) from exc
75
+
76
+ # Try to retrieve the SOFT7EntityInstance class from the generated_classes
77
+ # module. If unsuccessful, we create it.
78
+ _, _, name = parse_identity(entity.identity)
79
+ name = name.replace(" ", "")
80
+
81
+ try:
82
+ import s7.factories.generated_classes as lookup_module
83
+
84
+ return getattr(lookup_module, name)
85
+ except AttributeError:
86
+ return create_entity(entity)
87
+
88
+
89
+ class SOFT7FunctionConfig(HashableFunctionConfig):
90
+ """SOFT7 OTEAPI Function strategy configuration."""
91
+
92
+ functionType: Annotated[
93
+ Literal["soft7", "SOFT7"],
94
+ Field(
95
+ description=HashableFunctionConfig.model_fields["functionType"].description,
96
+ ),
97
+ ]
98
+
99
+ configuration: Annotated[
100
+ SOFT7GeneratorConfig, Field(description=SOFT7GeneratorConfig.__doc__)
101
+ ]