openai-sdk-helpers 0.1.4__py3-none-any.whl → 0.3.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.
- openai_sdk_helpers/__init__.py +4 -4
- openai_sdk_helpers/agent/__init__.py +6 -4
- openai_sdk_helpers/agent/base.py +211 -47
- openai_sdk_helpers/agent/config.py +354 -47
- openai_sdk_helpers/agent/coordination.py +15 -6
- openai_sdk_helpers/agent/runner.py +13 -4
- openai_sdk_helpers/agent/search/base.py +17 -17
- openai_sdk_helpers/agent/search/vector.py +13 -10
- openai_sdk_helpers/agent/search/web.py +13 -10
- openai_sdk_helpers/agent/summarizer.py +5 -4
- openai_sdk_helpers/agent/translator.py +5 -4
- openai_sdk_helpers/agent/validation.py +5 -4
- openai_sdk_helpers/response/base.py +84 -8
- openai_sdk_helpers/response/config.py +28 -110
- openai_sdk_helpers/response/messages.py +16 -0
- openai_sdk_helpers/utils/instructions.py +35 -0
- openai_sdk_helpers/utils/json_utils.py +136 -4
- openai_sdk_helpers/utils/registry.py +183 -0
- {openai_sdk_helpers-0.1.4.dist-info → openai_sdk_helpers-0.3.0.dist-info}/METADATA +1 -1
- {openai_sdk_helpers-0.1.4.dist-info → openai_sdk_helpers-0.3.0.dist-info}/RECORD +23 -21
- {openai_sdk_helpers-0.1.4.dist-info → openai_sdk_helpers-0.3.0.dist-info}/WHEEL +0 -0
- {openai_sdk_helpers-0.1.4.dist-info → openai_sdk_helpers-0.3.0.dist-info}/entry_points.txt +0 -0
- {openai_sdk_helpers-0.1.4.dist-info → openai_sdk_helpers-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,28 +10,19 @@ from openai.types.responses.response_text_config_param import ResponseTextConfig
|
|
|
10
10
|
from ..config import OpenAISettings
|
|
11
11
|
from ..structure.base import BaseStructure
|
|
12
12
|
from ..response.base import BaseResponse, ToolHandler
|
|
13
|
+
from ..utils import JSONSerializable
|
|
14
|
+
from ..utils.registry import BaseRegistry
|
|
15
|
+
from ..utils.instructions import resolve_instructions_from_path
|
|
13
16
|
|
|
14
17
|
TIn = TypeVar("TIn", bound="BaseStructure")
|
|
15
18
|
TOut = TypeVar("TOut", bound="BaseStructure")
|
|
16
19
|
|
|
17
20
|
|
|
18
|
-
class ResponseRegistry:
|
|
21
|
+
class ResponseRegistry(BaseRegistry["ResponseConfiguration"]):
|
|
19
22
|
"""Registry for managing ResponseConfiguration instances.
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
enabling reusable response specs across the application.
|
|
23
|
-
are stored by name and can be retrieved or listed as needed.
|
|
24
|
-
|
|
25
|
-
Methods
|
|
26
|
-
-------
|
|
27
|
-
register(config)
|
|
28
|
-
Add a ResponseConfiguration to the registry.
|
|
29
|
-
get(name)
|
|
30
|
-
Retrieve a configuration by name.
|
|
31
|
-
list_names()
|
|
32
|
-
Return all registered configuration names.
|
|
33
|
-
clear()
|
|
34
|
-
Remove all registered configurations.
|
|
24
|
+
Inherits from BaseRegistry to provide centralized storage and retrieval
|
|
25
|
+
of response configurations, enabling reusable response specs across the application.
|
|
35
26
|
|
|
36
27
|
Examples
|
|
37
28
|
--------
|
|
@@ -49,91 +40,7 @@ class ResponseRegistry:
|
|
|
49
40
|
'test'
|
|
50
41
|
"""
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
"""Initialize an empty registry."""
|
|
54
|
-
self._configs: dict[str, ResponseConfiguration] = {}
|
|
55
|
-
|
|
56
|
-
def register(self, config: ResponseConfiguration) -> None:
|
|
57
|
-
"""Add a ResponseConfiguration to the registry.
|
|
58
|
-
|
|
59
|
-
Parameters
|
|
60
|
-
----------
|
|
61
|
-
config : ResponseConfiguration
|
|
62
|
-
Configuration to register.
|
|
63
|
-
|
|
64
|
-
Raises
|
|
65
|
-
------
|
|
66
|
-
ValueError
|
|
67
|
-
If a configuration with the same name is already registered.
|
|
68
|
-
|
|
69
|
-
Examples
|
|
70
|
-
--------
|
|
71
|
-
>>> registry = ResponseRegistry()
|
|
72
|
-
>>> config = ResponseConfiguration(...)
|
|
73
|
-
>>> registry.register(config)
|
|
74
|
-
"""
|
|
75
|
-
if config.name in self._configs:
|
|
76
|
-
raise ValueError(
|
|
77
|
-
f"Configuration '{config.name}' is already registered. "
|
|
78
|
-
"Use a unique name or clear the registry first."
|
|
79
|
-
)
|
|
80
|
-
self._configs[config.name] = config
|
|
81
|
-
|
|
82
|
-
def get(self, name: str) -> ResponseConfiguration:
|
|
83
|
-
"""Retrieve a configuration by name.
|
|
84
|
-
|
|
85
|
-
Parameters
|
|
86
|
-
----------
|
|
87
|
-
name : str
|
|
88
|
-
Configuration name to look up.
|
|
89
|
-
|
|
90
|
-
Returns
|
|
91
|
-
-------
|
|
92
|
-
ResponseConfiguration
|
|
93
|
-
The registered configuration.
|
|
94
|
-
|
|
95
|
-
Raises
|
|
96
|
-
------
|
|
97
|
-
KeyError
|
|
98
|
-
If no configuration with the given name exists.
|
|
99
|
-
|
|
100
|
-
Examples
|
|
101
|
-
--------
|
|
102
|
-
>>> registry = ResponseRegistry()
|
|
103
|
-
>>> config = registry.get("test")
|
|
104
|
-
"""
|
|
105
|
-
if name not in self._configs:
|
|
106
|
-
raise KeyError(
|
|
107
|
-
f"No configuration named '{name}' found. "
|
|
108
|
-
f"Available: {list(self._configs.keys())}"
|
|
109
|
-
)
|
|
110
|
-
return self._configs[name]
|
|
111
|
-
|
|
112
|
-
def list_names(self) -> list[str]:
|
|
113
|
-
"""Return all registered configuration names.
|
|
114
|
-
|
|
115
|
-
Returns
|
|
116
|
-
-------
|
|
117
|
-
list[str]
|
|
118
|
-
Sorted list of configuration names.
|
|
119
|
-
|
|
120
|
-
Examples
|
|
121
|
-
--------
|
|
122
|
-
>>> registry = ResponseRegistry()
|
|
123
|
-
>>> registry.list_names()
|
|
124
|
-
[]
|
|
125
|
-
"""
|
|
126
|
-
return sorted(self._configs.keys())
|
|
127
|
-
|
|
128
|
-
def clear(self) -> None:
|
|
129
|
-
"""Remove all registered configurations.
|
|
130
|
-
|
|
131
|
-
Examples
|
|
132
|
-
--------
|
|
133
|
-
>>> registry = ResponseRegistry()
|
|
134
|
-
>>> registry.clear()
|
|
135
|
-
"""
|
|
136
|
-
self._configs.clear()
|
|
43
|
+
pass
|
|
137
44
|
|
|
138
45
|
|
|
139
46
|
# Global default registry instance
|
|
@@ -158,12 +65,13 @@ def get_default_registry() -> ResponseRegistry:
|
|
|
158
65
|
|
|
159
66
|
|
|
160
67
|
@dataclass(frozen=True, slots=True)
|
|
161
|
-
class ResponseConfiguration(Generic[TIn, TOut]):
|
|
68
|
+
class ResponseConfiguration(JSONSerializable, Generic[TIn, TOut]):
|
|
162
69
|
"""
|
|
163
70
|
Represent an immutable configuration describing input and output structures.
|
|
164
71
|
|
|
165
72
|
Encapsulate all metadata required to define how a request is interpreted and
|
|
166
73
|
how a response is structured, while enforcing strict type and runtime safety.
|
|
74
|
+
Inherits from JSONSerializable to support serialization to JSON format.
|
|
167
75
|
|
|
168
76
|
Parameters
|
|
169
77
|
----------
|
|
@@ -181,6 +89,12 @@ class ResponseConfiguration(Generic[TIn, TOut]):
|
|
|
181
89
|
Structure class used to format or validate output. Schema is
|
|
182
90
|
automatically generated from this structure. Must subclass
|
|
183
91
|
BaseStructure. Default is None.
|
|
92
|
+
system_vector_store : list[str], optional
|
|
93
|
+
Optional list of vector store names to attach as system context.
|
|
94
|
+
Default is None.
|
|
95
|
+
data_path : Path, str, or None, optional
|
|
96
|
+
Optional absolute directory path for storing artifacts. If not provided,
|
|
97
|
+
defaults to get_data_path(class_name). Default is None.
|
|
184
98
|
|
|
185
99
|
Raises
|
|
186
100
|
------
|
|
@@ -201,6 +115,14 @@ class ResponseConfiguration(Generic[TIn, TOut]):
|
|
|
201
115
|
Validate configuration invariants and enforce BaseStructure subclassing.
|
|
202
116
|
instructions_text
|
|
203
117
|
Return the resolved instruction content as a string.
|
|
118
|
+
to_json()
|
|
119
|
+
Return a JSON-compatible dict representation (inherited from JSONSerializable).
|
|
120
|
+
to_json_file(filepath)
|
|
121
|
+
Write serialized JSON data to a file path (inherited from JSONSerializable).
|
|
122
|
+
from_json(data)
|
|
123
|
+
Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
|
|
124
|
+
from_json_file(filepath)
|
|
125
|
+
Load an instance from a JSON file (class method, inherited from JSONSerializable).
|
|
204
126
|
|
|
205
127
|
Examples
|
|
206
128
|
--------
|
|
@@ -219,6 +141,8 @@ class ResponseConfiguration(Generic[TIn, TOut]):
|
|
|
219
141
|
tools: Optional[list]
|
|
220
142
|
input_structure: Optional[Type[TIn]]
|
|
221
143
|
output_structure: Optional[Type[TOut]]
|
|
144
|
+
system_vector_store: Optional[list[str]] = None
|
|
145
|
+
data_path: Optional[Path | str] = None
|
|
222
146
|
|
|
223
147
|
def __post_init__(self) -> None:
|
|
224
148
|
"""
|
|
@@ -277,15 +201,7 @@ class ResponseConfiguration(Generic[TIn, TOut]):
|
|
|
277
201
|
return self._resolve_instructions()
|
|
278
202
|
|
|
279
203
|
def _resolve_instructions(self) -> str:
|
|
280
|
-
|
|
281
|
-
instruction_path = self.instructions.expanduser()
|
|
282
|
-
try:
|
|
283
|
-
return instruction_path.read_text(encoding="utf-8")
|
|
284
|
-
except OSError as exc:
|
|
285
|
-
raise ValueError(
|
|
286
|
-
f"Unable to read instructions at '{instruction_path}': {exc}"
|
|
287
|
-
) from exc
|
|
288
|
-
return self.instructions
|
|
204
|
+
return resolve_instructions_from_path(self.instructions)
|
|
289
205
|
|
|
290
206
|
def gen_response(
|
|
291
207
|
self,
|
|
@@ -328,6 +244,8 @@ class ResponseConfiguration(Generic[TIn, TOut]):
|
|
|
328
244
|
instructions=instructions,
|
|
329
245
|
tools=self.tools,
|
|
330
246
|
output_structure=self.output_structure,
|
|
247
|
+
system_vector_store=self.system_vector_store,
|
|
248
|
+
data_path=self.data_path,
|
|
331
249
|
tool_handlers=tool_handlers,
|
|
332
250
|
openai_settings=openai_settings,
|
|
333
251
|
)
|
|
@@ -50,6 +50,14 @@ class ResponseMessage(JSONSerializable):
|
|
|
50
50
|
-------
|
|
51
51
|
to_openai_format()
|
|
52
52
|
Return the message content in OpenAI API format.
|
|
53
|
+
to_json()
|
|
54
|
+
Return a JSON-compatible dict representation (inherited from JSONSerializable).
|
|
55
|
+
to_json_file(filepath)
|
|
56
|
+
Write serialized JSON data to a file path (inherited from JSONSerializable).
|
|
57
|
+
from_json(data)
|
|
58
|
+
Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
|
|
59
|
+
from_json_file(filepath)
|
|
60
|
+
Load an instance from a JSON file (class method, inherited from JSONSerializable).
|
|
53
61
|
"""
|
|
54
62
|
|
|
55
63
|
role: str # "user", "assistant", "tool", etc.
|
|
@@ -113,6 +121,14 @@ class ResponseMessages(JSONSerializable):
|
|
|
113
121
|
Return the most recent tool message or None.
|
|
114
122
|
get_last_user_message()
|
|
115
123
|
Return the most recent user message or None.
|
|
124
|
+
to_json()
|
|
125
|
+
Return a JSON-compatible dict representation (inherited from JSONSerializable).
|
|
126
|
+
to_json_file(filepath)
|
|
127
|
+
Write serialized JSON data to a file path (inherited from JSONSerializable).
|
|
128
|
+
from_json(data)
|
|
129
|
+
Create an instance from a JSON-compatible dict (class method, inherited from JSONSerializable).
|
|
130
|
+
from_json_file(filepath)
|
|
131
|
+
Load an instance from a JSON file (class method, inherited from JSONSerializable).
|
|
116
132
|
"""
|
|
117
133
|
|
|
118
134
|
messages: list[ResponseMessage] = field(default_factory=list)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Utilities for resolving instructions from strings or file paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def resolve_instructions_from_path(instructions: str | Path) -> str:
|
|
9
|
+
"""Resolve instructions from a string or file path.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
instructions : str or Path
|
|
14
|
+
Either plain-text instructions or a path to a file containing
|
|
15
|
+
instructions.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
str
|
|
20
|
+
The resolved instruction text.
|
|
21
|
+
|
|
22
|
+
Raises
|
|
23
|
+
------
|
|
24
|
+
ValueError
|
|
25
|
+
If instructions is a Path that cannot be read.
|
|
26
|
+
"""
|
|
27
|
+
if isinstance(instructions, Path):
|
|
28
|
+
instruction_path = instructions.expanduser()
|
|
29
|
+
try:
|
|
30
|
+
return instruction_path.read_text(encoding="utf-8")
|
|
31
|
+
except OSError as exc:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"Unable to read instructions at '{instruction_path}': {exc}"
|
|
34
|
+
) from exc
|
|
35
|
+
return instructions
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
from dataclasses import asdict, is_dataclass
|
|
6
|
+
from dataclasses import asdict, fields, is_dataclass
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from enum import Enum
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
|
|
11
11
|
|
|
12
12
|
from .path_utils import check_filepath
|
|
13
13
|
|
|
14
|
+
T = TypeVar("T", bound="JSONSerializable")
|
|
15
|
+
|
|
14
16
|
|
|
15
17
|
def _to_jsonable(value: Any) -> Any:
|
|
16
18
|
"""Convert common helper types to JSON-serializable forms."""
|
|
@@ -65,7 +67,19 @@ class customJSONEncoder(json.JSONEncoder):
|
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
class JSONSerializable:
|
|
68
|
-
"""Mixin for classes that can be serialized to JSON.
|
|
70
|
+
"""Mixin for classes that can be serialized to and from JSON.
|
|
71
|
+
|
|
72
|
+
Methods
|
|
73
|
+
-------
|
|
74
|
+
to_json()
|
|
75
|
+
Return a JSON-compatible dict representation.
|
|
76
|
+
to_json_file(filepath)
|
|
77
|
+
Write serialized JSON data to a file path.
|
|
78
|
+
from_json(data)
|
|
79
|
+
Create an instance from a JSON-compatible dict (class method).
|
|
80
|
+
from_json_file(filepath)
|
|
81
|
+
Load an instance from a JSON file (class method).
|
|
82
|
+
"""
|
|
69
83
|
|
|
70
84
|
def to_json(self) -> dict[str, Any]:
|
|
71
85
|
"""Return a JSON-compatible dict representation."""
|
|
@@ -77,7 +91,18 @@ class JSONSerializable:
|
|
|
77
91
|
return _to_jsonable(self.__dict__)
|
|
78
92
|
|
|
79
93
|
def to_json_file(self, filepath: str | Path) -> str:
|
|
80
|
-
"""Write serialized JSON data to a file path.
|
|
94
|
+
"""Write serialized JSON data to a file path.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
filepath : str or Path
|
|
99
|
+
Path where the JSON file will be written.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
str
|
|
104
|
+
Absolute path to the written file.
|
|
105
|
+
"""
|
|
81
106
|
target = Path(filepath)
|
|
82
107
|
check_filepath(fullfilepath=str(target))
|
|
83
108
|
with open(target, "w", encoding="utf-8") as handle:
|
|
@@ -90,6 +115,113 @@ class JSONSerializable:
|
|
|
90
115
|
)
|
|
91
116
|
return str(target)
|
|
92
117
|
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_json(cls: type[T], data: dict[str, Any]) -> T:
|
|
120
|
+
"""Create an instance from a JSON-compatible dict.
|
|
121
|
+
|
|
122
|
+
For dataclasses, this reconstructs Path objects and passes the
|
|
123
|
+
dict keys directly as constructor arguments.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
data : dict[str, Any]
|
|
128
|
+
JSON-compatible dictionary containing the instance data.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
T
|
|
133
|
+
New instance of the class.
|
|
134
|
+
|
|
135
|
+
Examples
|
|
136
|
+
--------
|
|
137
|
+
>>> json_data = {"name": "test", "path": "/tmp/data"}
|
|
138
|
+
>>> instance = MyClass.from_json(json_data)
|
|
139
|
+
"""
|
|
140
|
+
if is_dataclass(cls):
|
|
141
|
+
# Get resolved field types using get_type_hints
|
|
142
|
+
try:
|
|
143
|
+
field_types = get_type_hints(cls)
|
|
144
|
+
except Exception:
|
|
145
|
+
# Fallback to raw annotations if get_type_hints fails
|
|
146
|
+
field_types = {f.name: f.type for f in fields(cls)}
|
|
147
|
+
|
|
148
|
+
converted_data = {}
|
|
149
|
+
|
|
150
|
+
for key, value in data.items():
|
|
151
|
+
if key in field_types:
|
|
152
|
+
field_type = field_types[key]
|
|
153
|
+
|
|
154
|
+
# Check if this field should be converted to Path
|
|
155
|
+
should_convert_to_path = False
|
|
156
|
+
|
|
157
|
+
if field_type is Path:
|
|
158
|
+
should_convert_to_path = True
|
|
159
|
+
else:
|
|
160
|
+
# Handle Union/Optional types
|
|
161
|
+
origin = get_origin(field_type)
|
|
162
|
+
if origin is Union:
|
|
163
|
+
type_args = get_args(field_type)
|
|
164
|
+
# Only convert to Path if:
|
|
165
|
+
# 1. Path is in the union AND
|
|
166
|
+
# 2. str is NOT in the union (to avoid converting string fields)
|
|
167
|
+
# OR the field name suggests it's a path (contains "path")
|
|
168
|
+
if Path in type_args:
|
|
169
|
+
if str not in type_args:
|
|
170
|
+
# Path-only union (e.g., Union[Path, None])
|
|
171
|
+
should_convert_to_path = True
|
|
172
|
+
elif "path" in key.lower():
|
|
173
|
+
# Field name contains "path", likely meant to be a path
|
|
174
|
+
should_convert_to_path = True
|
|
175
|
+
|
|
176
|
+
# Convert string to Path if needed
|
|
177
|
+
if (
|
|
178
|
+
should_convert_to_path
|
|
179
|
+
and value is not None
|
|
180
|
+
and isinstance(value, str)
|
|
181
|
+
):
|
|
182
|
+
converted_data[key] = Path(value)
|
|
183
|
+
else:
|
|
184
|
+
converted_data[key] = value
|
|
185
|
+
else:
|
|
186
|
+
converted_data[key] = value
|
|
187
|
+
|
|
188
|
+
return cls(**converted_data) # type: ignore[return-value]
|
|
189
|
+
|
|
190
|
+
# For non-dataclass types, try to instantiate with data as kwargs
|
|
191
|
+
return cls(**data) # type: ignore[return-value]
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_json_file(cls: type[T], filepath: str | Path) -> T:
|
|
195
|
+
"""Load an instance from a JSON file.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
filepath : str or Path
|
|
200
|
+
Path to the JSON file to load.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
T
|
|
205
|
+
New instance of the class loaded from the file.
|
|
206
|
+
|
|
207
|
+
Raises
|
|
208
|
+
------
|
|
209
|
+
FileNotFoundError
|
|
210
|
+
If the file does not exist.
|
|
211
|
+
|
|
212
|
+
Examples
|
|
213
|
+
--------
|
|
214
|
+
>>> instance = MyClass.from_json_file("config.json")
|
|
215
|
+
"""
|
|
216
|
+
target = Path(filepath)
|
|
217
|
+
if not target.exists():
|
|
218
|
+
raise FileNotFoundError(f"JSON file not found: {target}")
|
|
219
|
+
|
|
220
|
+
with open(target, "r", encoding="utf-8") as handle:
|
|
221
|
+
data = json.load(handle)
|
|
222
|
+
|
|
223
|
+
return cls.from_json(data)
|
|
224
|
+
|
|
93
225
|
|
|
94
226
|
__all__ = [
|
|
95
227
|
"coerce_jsonable",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Base registry class for managing configuration instances."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Generic, TypeVar
|
|
8
|
+
|
|
9
|
+
from .path_utils import ensure_directory
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseRegistry(Generic[T]):
|
|
15
|
+
"""Base registry for managing configuration instances.
|
|
16
|
+
|
|
17
|
+
Provides centralized storage and retrieval of configurations,
|
|
18
|
+
enabling reusable specs across the application. Configurations
|
|
19
|
+
are stored by name and can be retrieved or listed as needed.
|
|
20
|
+
|
|
21
|
+
Type Parameters
|
|
22
|
+
---------------
|
|
23
|
+
T
|
|
24
|
+
The configuration type this registry manages.
|
|
25
|
+
|
|
26
|
+
Methods
|
|
27
|
+
-------
|
|
28
|
+
register(config)
|
|
29
|
+
Add a configuration to the registry.
|
|
30
|
+
get(name)
|
|
31
|
+
Retrieve a configuration by name.
|
|
32
|
+
list_names()
|
|
33
|
+
Return all registered configuration names.
|
|
34
|
+
clear()
|
|
35
|
+
Remove all registered configurations.
|
|
36
|
+
save_to_directory(path)
|
|
37
|
+
Export all registered configurations to JSON files.
|
|
38
|
+
load_from_directory(path)
|
|
39
|
+
Load configurations from JSON files in a directory.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
"""Initialize an empty registry."""
|
|
44
|
+
self._configs: dict[str, T] = {}
|
|
45
|
+
|
|
46
|
+
def register(self, config: T) -> None:
|
|
47
|
+
"""Add a configuration to the registry.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
config : T
|
|
52
|
+
Configuration to register. Must have a 'name' attribute.
|
|
53
|
+
|
|
54
|
+
Raises
|
|
55
|
+
------
|
|
56
|
+
ValueError
|
|
57
|
+
If a configuration with the same name is already registered.
|
|
58
|
+
"""
|
|
59
|
+
name = getattr(config, "name")
|
|
60
|
+
if name in self._configs:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Configuration '{name}' is already registered. "
|
|
63
|
+
"Use a unique name or clear the registry first."
|
|
64
|
+
)
|
|
65
|
+
self._configs[name] = config
|
|
66
|
+
|
|
67
|
+
def get(self, name: str) -> T:
|
|
68
|
+
"""Retrieve a configuration by name.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
name : str
|
|
73
|
+
Configuration name to look up.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
T
|
|
78
|
+
The registered configuration.
|
|
79
|
+
|
|
80
|
+
Raises
|
|
81
|
+
------
|
|
82
|
+
KeyError
|
|
83
|
+
If no configuration with the given name exists.
|
|
84
|
+
"""
|
|
85
|
+
if name not in self._configs:
|
|
86
|
+
raise KeyError(
|
|
87
|
+
f"No configuration named '{name}' found. "
|
|
88
|
+
f"Available: {list(self._configs.keys())}"
|
|
89
|
+
)
|
|
90
|
+
return self._configs[name]
|
|
91
|
+
|
|
92
|
+
def list_names(self) -> list[str]:
|
|
93
|
+
"""Return all registered configuration names.
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
list[str]
|
|
98
|
+
Sorted list of configuration names.
|
|
99
|
+
"""
|
|
100
|
+
return sorted(self._configs.keys())
|
|
101
|
+
|
|
102
|
+
def clear(self) -> None:
|
|
103
|
+
"""Remove all registered configurations."""
|
|
104
|
+
self._configs.clear()
|
|
105
|
+
|
|
106
|
+
def save_to_directory(self, path: Path | str) -> None:
|
|
107
|
+
"""Export all registered configurations to JSON files in a directory.
|
|
108
|
+
|
|
109
|
+
Serializes each registered configuration to an individual JSON file
|
|
110
|
+
named after the configuration. Creates the directory if it does not exist.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
path : Path or str
|
|
115
|
+
Directory path where JSON files will be saved. Will be created if
|
|
116
|
+
it does not already exist.
|
|
117
|
+
|
|
118
|
+
Raises
|
|
119
|
+
------
|
|
120
|
+
OSError
|
|
121
|
+
If the directory cannot be created or files cannot be written.
|
|
122
|
+
"""
|
|
123
|
+
dir_path = ensure_directory(Path(path))
|
|
124
|
+
config_names = self.list_names()
|
|
125
|
+
|
|
126
|
+
if not config_names:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
for config_name in config_names:
|
|
130
|
+
config = self.get(config_name)
|
|
131
|
+
filename = f"{config_name}.json"
|
|
132
|
+
filepath = dir_path / filename
|
|
133
|
+
# Call to_json_file on the config
|
|
134
|
+
getattr(config, "to_json_file")(filepath)
|
|
135
|
+
|
|
136
|
+
def load_from_directory(self, path: Path | str, *, config_class: type[T]) -> int:
|
|
137
|
+
"""Load all configurations from JSON files in a directory.
|
|
138
|
+
|
|
139
|
+
Scans the directory for JSON files and attempts to load each as a
|
|
140
|
+
configuration. Successfully loaded configurations are registered.
|
|
141
|
+
If a file fails to load, a warning is issued and processing continues
|
|
142
|
+
with the remaining files.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
path : Path or str
|
|
147
|
+
Directory path containing JSON configuration files.
|
|
148
|
+
config_class : type[T]
|
|
149
|
+
The configuration class to use for deserialization.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
int
|
|
154
|
+
Number of configurations successfully loaded and registered.
|
|
155
|
+
|
|
156
|
+
Raises
|
|
157
|
+
------
|
|
158
|
+
FileNotFoundError
|
|
159
|
+
If the directory does not exist.
|
|
160
|
+
NotADirectoryError
|
|
161
|
+
If the path is not a directory.
|
|
162
|
+
"""
|
|
163
|
+
dir_path = Path(path)
|
|
164
|
+
if not dir_path.exists():
|
|
165
|
+
raise FileNotFoundError(f"Directory not found: {dir_path}")
|
|
166
|
+
|
|
167
|
+
if not dir_path.is_dir():
|
|
168
|
+
raise NotADirectoryError(f"Path is not a directory: {dir_path}")
|
|
169
|
+
|
|
170
|
+
count = 0
|
|
171
|
+
for json_file in sorted(dir_path.glob("*.json")):
|
|
172
|
+
try:
|
|
173
|
+
config = getattr(config_class, "from_json_file")(json_file)
|
|
174
|
+
self.register(config)
|
|
175
|
+
count += 1
|
|
176
|
+
except Exception as exc:
|
|
177
|
+
# Log warning but continue processing other files
|
|
178
|
+
warnings.warn(
|
|
179
|
+
f"Failed to load configuration from {json_file}: {exc}",
|
|
180
|
+
stacklevel=2,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return count
|