openai-sdk-helpers 0.1.0__py3-none-any.whl → 0.1.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 (44) hide show
  1. openai_sdk_helpers/__init__.py +44 -7
  2. openai_sdk_helpers/agent/base.py +5 -1
  3. openai_sdk_helpers/agent/coordination.py +4 -5
  4. openai_sdk_helpers/agent/runner.py +4 -1
  5. openai_sdk_helpers/agent/search/base.py +1 -0
  6. openai_sdk_helpers/agent/search/vector.py +2 -0
  7. openai_sdk_helpers/cli.py +265 -0
  8. openai_sdk_helpers/config.py +93 -2
  9. openai_sdk_helpers/context_manager.py +1 -1
  10. openai_sdk_helpers/deprecation.py +167 -0
  11. openai_sdk_helpers/environment.py +3 -2
  12. openai_sdk_helpers/errors.py +0 -12
  13. openai_sdk_helpers/files_api.py +373 -0
  14. openai_sdk_helpers/logging_config.py +24 -95
  15. openai_sdk_helpers/prompt/base.py +1 -1
  16. openai_sdk_helpers/response/__init__.py +7 -3
  17. openai_sdk_helpers/response/base.py +217 -147
  18. openai_sdk_helpers/response/config.py +16 -1
  19. openai_sdk_helpers/response/files.py +392 -0
  20. openai_sdk_helpers/response/messages.py +1 -0
  21. openai_sdk_helpers/retry.py +1 -1
  22. openai_sdk_helpers/streamlit_app/app.py +97 -7
  23. openai_sdk_helpers/streamlit_app/streamlit_web_search.py +15 -8
  24. openai_sdk_helpers/structure/base.py +6 -6
  25. openai_sdk_helpers/structure/plan/helpers.py +1 -0
  26. openai_sdk_helpers/structure/plan/task.py +7 -7
  27. openai_sdk_helpers/tools.py +116 -13
  28. openai_sdk_helpers/utils/__init__.py +100 -35
  29. openai_sdk_helpers/{async_utils.py → utils/async_utils.py} +5 -6
  30. openai_sdk_helpers/utils/coercion.py +138 -0
  31. openai_sdk_helpers/utils/deprecation.py +167 -0
  32. openai_sdk_helpers/utils/encoding.py +189 -0
  33. openai_sdk_helpers/utils/json_utils.py +98 -0
  34. openai_sdk_helpers/utils/output_validation.py +448 -0
  35. openai_sdk_helpers/utils/path_utils.py +46 -0
  36. openai_sdk_helpers/{validation.py → utils/validation.py} +7 -3
  37. openai_sdk_helpers/vector_storage/storage.py +59 -28
  38. {openai_sdk_helpers-0.1.0.dist-info → openai_sdk_helpers-0.1.2.dist-info}/METADATA +152 -3
  39. openai_sdk_helpers-0.1.2.dist-info/RECORD +79 -0
  40. openai_sdk_helpers-0.1.2.dist-info/entry_points.txt +2 -0
  41. openai_sdk_helpers/utils/core.py +0 -596
  42. openai_sdk_helpers-0.1.0.dist-info/RECORD +0 -69
  43. {openai_sdk_helpers-0.1.0.dist-info → openai_sdk_helpers-0.1.2.dist-info}/WHEEL +0 -0
  44. {openai_sdk_helpers-0.1.0.dist-info → openai_sdk_helpers-0.1.2.dist-info}/licenses/LICENSE +0 -0
@@ -140,13 +140,13 @@ class TaskStructure(BaseStructure):
140
140
  """
141
141
  return "\n".join(
142
142
  [
143
- BaseStructure.format_output("Task type", self.task_type),
144
- BaseStructure.format_output("Prompt", self.prompt),
145
- BaseStructure.format_output("Context", self.context),
146
- BaseStructure.format_output("Status", self.status),
147
- BaseStructure.format_output("Start date", self.start_date),
148
- BaseStructure.format_output("End date", self.end_date),
149
- BaseStructure.format_output("Results", self.results),
143
+ BaseStructure.format_output("Task type", value=self.task_type),
144
+ BaseStructure.format_output("Prompt", value=self.prompt),
145
+ BaseStructure.format_output("Context", value=self.context),
146
+ BaseStructure.format_output("Status", value=self.status),
147
+ BaseStructure.format_output("Start date", value=self.start_date),
148
+ BaseStructure.format_output("End date", value=self.end_date),
149
+ BaseStructure.format_output("Results", value=self.results),
150
150
  ]
151
151
  )
152
152
 
@@ -3,19 +3,26 @@
3
3
  This module provides generic tool handling infrastructure including argument
4
4
  parsing, Pydantic validation, function execution, and result serialization.
5
5
  These utilities reduce boilerplate and ensure consistent tool behavior.
6
+
7
+ Also provides declarative tool specification helpers for building tool
8
+ definitions from named metadata structures.
6
9
  """
7
10
 
8
11
  from __future__ import annotations
9
12
 
10
13
  import inspect
11
- import json
12
- from typing import Any, Callable, TypeVar
14
+ from dataclasses import dataclass
15
+ from typing import Any, Callable, TypeAlias, TypeVar
13
16
 
14
17
  from pydantic import BaseModel, ValidationError
15
18
 
16
19
  from openai_sdk_helpers.response.tool_call import parse_tool_arguments
20
+ from openai_sdk_helpers.structure.base import BaseStructure
21
+ from openai_sdk_helpers.utils import coerce_jsonable, customJSONEncoder
22
+ import json
17
23
 
18
24
  T = TypeVar("T", bound=BaseModel)
25
+ StructureType: TypeAlias = type[BaseStructure]
19
26
 
20
27
 
21
28
  def serialize_tool_result(result: Any) -> str:
@@ -53,24 +60,16 @@ def serialize_tool_result(result: Any) -> str:
53
60
  >>> serialize_tool_result({"key": "value"})
54
61
  '{"key": "value"}'
55
62
  """
56
- # Handle Pydantic models
57
63
  if isinstance(result, BaseModel):
58
64
  return result.model_dump_json()
59
65
 
60
- # Handle strings - wrap in JSON string format
61
- if isinstance(result, str):
62
- return json.dumps(result)
63
-
64
- # Handle other JSON-serializable types (lists, dicts, primitives)
65
- try:
66
- return json.dumps(result)
67
- except (TypeError, ValueError):
68
- # Fallback to string representation for non-JSON types
69
- return json.dumps(str(result))
66
+ payload = coerce_jsonable(result)
67
+ return json.dumps(payload, cls=customJSONEncoder)
70
68
 
71
69
 
72
70
  def tool_handler_factory(
73
71
  func: Callable[..., Any],
72
+ *,
74
73
  input_model: type[T] | None = None,
75
74
  ) -> Callable[[Any], str]:
76
75
  """Create a generic tool handler that parses, validates, and serializes.
@@ -187,7 +186,111 @@ def tool_handler_factory(
187
186
  return handler
188
187
 
189
188
 
189
+ @dataclass(frozen=True)
190
+ class ToolSpec:
191
+ """Capture tool metadata for response configuration.
192
+
193
+ Provides a named structure for representing tool specifications, making
194
+ tool definitions explicit and eliminating ambiguous tuple ordering.
195
+
196
+ Supports tools with separate input and output structures, where the input
197
+ structure defines the tool's parameter schema and the output structure
198
+ documents the expected return type (for reference only).
199
+
200
+ Attributes
201
+ ----------
202
+ structure : StructureType
203
+ The BaseStructure class that defines the tool's input parameter schema.
204
+ Used to generate the OpenAI tool definition.
205
+ tool_name : str
206
+ Name identifier for the tool.
207
+ tool_description : str
208
+ Human-readable description of what the tool does.
209
+ output_structure : StructureType or None, default=None
210
+ Optional BaseStructure class that defines the tool's output schema.
211
+ This is for documentation/reference only and is not sent to OpenAI.
212
+ Useful when a tool accepts one type of input but returns a different
213
+ structured output.
214
+
215
+ Examples
216
+ --------
217
+ Define a tool with same input/output structure:
218
+
219
+ >>> from openai_sdk_helpers import ToolSpec
220
+ >>> from openai_sdk_helpers.structure import PromptStructure
221
+ >>> spec = ToolSpec(
222
+ ... structure=PromptStructure,
223
+ ... tool_name="web_agent",
224
+ ... tool_description="Run a web research workflow"
225
+ ... )
226
+
227
+ Define a tool with different input and output structures:
228
+
229
+ >>> from openai_sdk_helpers.structure import PromptStructure, SummaryStructure
230
+ >>> spec = ToolSpec(
231
+ ... structure=PromptStructure,
232
+ ... tool_name="summarizer",
233
+ ... tool_description="Summarize the provided prompt",
234
+ ... output_structure=SummaryStructure
235
+ ... )
236
+ """
237
+
238
+ structure: StructureType
239
+ tool_name: str
240
+ tool_description: str
241
+ output_structure: StructureType | None = None
242
+
243
+
244
+ def build_tool_definitions(tool_specs: list[ToolSpec]) -> list[dict]:
245
+ """Build tool definitions from named tool specs.
246
+
247
+ Converts a list of ToolSpec objects into OpenAI-compatible tool
248
+ definitions for use in response configurations. Each ToolSpec is
249
+ transformed into a tool definition using the structure's
250
+ response_tool_definition method.
251
+
252
+ Parameters
253
+ ----------
254
+ tool_specs : list[ToolSpec]
255
+ List of tool specifications to convert.
256
+
257
+ Returns
258
+ -------
259
+ list[dict]
260
+ List of tool definition dictionaries ready for OpenAI API.
261
+
262
+ Examples
263
+ --------
264
+ Build multiple tool definitions:
265
+
266
+ >>> from openai_sdk_helpers import ToolSpec, build_tool_definitions
267
+ >>> from openai_sdk_helpers.structure import PromptStructure
268
+ >>> tools = build_tool_definitions([
269
+ ... ToolSpec(
270
+ ... structure=PromptStructure,
271
+ ... tool_name="web_agent",
272
+ ... tool_description="Run a web research workflow"
273
+ ... ),
274
+ ... ToolSpec(
275
+ ... structure=PromptStructure,
276
+ ... tool_name="vector_agent",
277
+ ... tool_description="Run a vector search workflow"
278
+ ... ),
279
+ ... ])
280
+ """
281
+ return [
282
+ spec.structure.response_tool_definition(
283
+ tool_name=spec.tool_name,
284
+ tool_description=spec.tool_description,
285
+ )
286
+ for spec in tool_specs
287
+ ]
288
+
289
+
190
290
  __all__ = [
191
291
  "serialize_tool_result",
192
292
  "tool_handler_factory",
293
+ "StructureType",
294
+ "ToolSpec",
295
+ "build_tool_definitions",
193
296
  ]
@@ -1,54 +1,90 @@
1
1
  """Utility helpers for openai-sdk-helpers.
2
2
 
3
- This package provides common utility functions for type coercion, file
4
- handling, JSON serialization, logging, and OpenAI settings construction.
5
- These utilities are used throughout the openai_sdk_helpers package.
3
+ The utils package collects cross-cutting helpers used across the project:
6
4
 
7
- Functions
8
- ---------
9
- ensure_list(value)
10
- Normalize a single item or iterable into a list.
11
- check_filepath(filepath, fullfilepath)
12
- Ensure the parent directory for a file path exists.
13
- coerce_optional_float(value)
14
- Convert a value to float or None.
15
- coerce_optional_int(value)
16
- Convert a value to int or None.
17
- coerce_dict(value)
18
- Convert a value to a string-keyed dictionary.
19
- coerce_jsonable(value)
20
- Convert a value into a JSON-serializable representation.
21
- log(message, level)
22
- Log a message with basic configuration.
23
- build_openai_settings(**kwargs)
24
- Build OpenAI settings from environment with validation.
5
+ * Core helpers: coercion, path handling, JSON encoding, and logging basics.
6
+ * Validation: input validation helpers for strings, choices, URLs, etc.
7
+ * Concurrency: async bridging helpers.
8
+ * Output validation: JSON Schema and semantic validators.
9
+ * Instrumentation helpers: deprecation utilities.
10
+ * Encoding: base64 encoding for images and files.
25
11
 
26
- Classes
27
- -------
28
- JSONSerializable
29
- Mixin for classes that can be serialized to JSON.
30
- customJSONEncoder
31
- JSON encoder for common helper types like enums and paths.
12
+ Import style
13
+ ------------
14
+ Public helpers are re-exported from ``openai_sdk_helpers.utils`` for a
15
+ consistent import surface. You can import from submodules when you need a
16
+ smaller surface area, but top-level imports remain stable.
17
+
18
+ Submodules
19
+ ----------
20
+ coercion
21
+ Numeric coercion helpers and list normalization.
22
+ path_utils
23
+ File and path helpers.
24
+ json_utils
25
+ JSON encoding helpers and mixins.
26
+ logging_config
27
+ Centralized logger factory and convenience log helper.
28
+ validation
29
+ Input validation helpers for strings, URLs, collections, and paths.
30
+ async_utils
31
+ Async-to-sync bridging helpers.
32
+ output_validation
33
+ JSON Schema and semantic output validation utilities.
34
+ deprecation
35
+ Deprecation helpers and warning utilities.
36
+ encoding
37
+ Base64 encoding helpers for images and files.
32
38
  """
33
39
 
34
40
  from __future__ import annotations
35
41
 
36
- from .core import (
37
- JSONSerializable,
38
- build_openai_settings,
39
- check_filepath,
40
- coerce_jsonable,
42
+ from .coercion import (
41
43
  coerce_dict,
42
44
  coerce_optional_float,
43
45
  coerce_optional_int,
44
- customJSONEncoder,
45
46
  ensure_list,
46
- log,
47
+ )
48
+ from .json_utils import (
49
+ JSONSerializable,
50
+ coerce_jsonable,
51
+ customJSONEncoder,
52
+ )
53
+ from .path_utils import check_filepath, ensure_directory
54
+ from openai_sdk_helpers.logging_config import log
55
+ from .validation import (
56
+ validate_choice,
57
+ validate_dict_mapping,
58
+ validate_list_items,
59
+ validate_max_length,
60
+ validate_non_empty_string,
61
+ validate_safe_path,
62
+ validate_url_format,
63
+ )
64
+ from .async_utils import run_coroutine_thread_safe, run_coroutine_with_fallback
65
+ from .output_validation import (
66
+ JSONSchemaValidator,
67
+ LengthValidator,
68
+ OutputValidator,
69
+ SemanticValidator,
70
+ ValidationResult,
71
+ ValidationRule,
72
+ validate_output,
73
+ )
74
+ from .deprecation import DeprecationHelper, deprecated, warn_deprecated
75
+ from .encoding import (
76
+ create_file_data_url,
77
+ create_image_data_url,
78
+ encode_file,
79
+ encode_image,
80
+ get_mime_type,
81
+ is_image_file,
47
82
  )
48
83
 
49
84
  __all__ = [
50
85
  "ensure_list",
51
86
  "check_filepath",
87
+ "ensure_directory",
52
88
  "coerce_optional_float",
53
89
  "coerce_optional_int",
54
90
  "coerce_dict",
@@ -56,5 +92,34 @@ __all__ = [
56
92
  "JSONSerializable",
57
93
  "customJSONEncoder",
58
94
  "log",
59
- "build_openai_settings",
95
+ # Validation helpers
96
+ "validate_non_empty_string",
97
+ "validate_max_length",
98
+ "validate_url_format",
99
+ "validate_dict_mapping",
100
+ "validate_list_items",
101
+ "validate_choice",
102
+ "validate_safe_path",
103
+ # Async helpers
104
+ "run_coroutine_thread_safe",
105
+ "run_coroutine_with_fallback",
106
+ # Output validation
107
+ "ValidationResult",
108
+ "ValidationRule",
109
+ "JSONSchemaValidator",
110
+ "SemanticValidator",
111
+ "LengthValidator",
112
+ "OutputValidator",
113
+ "validate_output",
114
+ # Deprecation
115
+ "deprecated",
116
+ "warn_deprecated",
117
+ "DeprecationHelper",
118
+ # Encoding
119
+ "encode_image",
120
+ "encode_file",
121
+ "get_mime_type",
122
+ "create_image_data_url",
123
+ "create_file_data_url",
124
+ "is_image_file",
60
125
  ]
@@ -10,17 +10,17 @@ import threading
10
10
  from typing import Any, Coroutine, Generic, TypeVar
11
11
 
12
12
  from openai_sdk_helpers.errors import AsyncExecutionError
13
- from openai_sdk_helpers.utils.core import log
14
13
 
15
14
  T = TypeVar("T")
16
15
 
17
16
  # Default timeout constants
18
17
  DEFAULT_COROUTINE_TIMEOUT = 300.0 # 5 minutes
19
- THREAD_JOIN_TIMEOUT = 5.0 # 5 seconds
18
+ THREAD_JOIN_TIMEOUT = 0.5 # 0.5 seconds
20
19
 
21
20
 
22
21
  def run_coroutine_thread_safe(
23
22
  coro: Coroutine[Any, Any, T],
23
+ *,
24
24
  timeout: float = DEFAULT_COROUTINE_TIMEOUT,
25
25
  ) -> T:
26
26
  """Run a coroutine in a thread-safe manner from a sync context.
@@ -80,10 +80,9 @@ def run_coroutine_thread_safe(
80
80
  # Ensure thread is cleaned up
81
81
  thread.join(timeout=THREAD_JOIN_TIMEOUT)
82
82
  if thread.is_alive():
83
- log(
84
- f"Thread {thread.name} did not terminate within {THREAD_JOIN_TIMEOUT} seconds",
85
- level=20, # logging.INFO
86
- )
83
+ # Thread did not terminate, likely still running async operation
84
+ # This is expected for timeout scenarios, so we don't log here
85
+ pass
87
86
 
88
87
 
89
88
  def run_coroutine_with_fallback(
@@ -0,0 +1,138 @@
1
+ """Type coercion and collection normalization helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ from typing import Any, TypeVar
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ def coerce_optional_float(value: object) -> float | None:
12
+ """Return a float when the provided value can be coerced, otherwise None.
13
+
14
+ Handles float, int, and string inputs. Empty strings or None return None.
15
+
16
+ Parameters
17
+ ----------
18
+ value : object
19
+ Value to convert into a float. Strings must be parseable as floats.
20
+
21
+ Returns
22
+ -------
23
+ float or None
24
+ Converted float value or None if the input is None.
25
+
26
+ Raises
27
+ ------
28
+ ValueError
29
+ If a non-empty string cannot be converted to a float.
30
+ TypeError
31
+ If the value is not a float-compatible type.
32
+ """
33
+ if value is None:
34
+ return None
35
+ if isinstance(value, (float, int)):
36
+ return float(value)
37
+ if isinstance(value, str) and value.strip():
38
+ try:
39
+ return float(value)
40
+ except ValueError as exc:
41
+ raise ValueError("timeout must be a float-compatible value") from exc
42
+ raise TypeError("timeout must be a float, int, str, or None")
43
+
44
+
45
+ def coerce_optional_int(value: object) -> int | None:
46
+ """Return an int when the provided value can be coerced, otherwise None.
47
+
48
+ Handles int, float (if whole number), and string inputs. Empty strings
49
+ or None return None. Booleans are not considered valid integers.
50
+
51
+ Parameters
52
+ ----------
53
+ value : object
54
+ Value to convert into an int. Strings must be parseable as integers.
55
+
56
+ Returns
57
+ -------
58
+ int or None
59
+ Converted integer value or None if the input is None.
60
+
61
+ Raises
62
+ ------
63
+ ValueError
64
+ If a non-empty string cannot be converted to an integer.
65
+ TypeError
66
+ If the value is not an int-compatible type.
67
+ """
68
+ if value is None:
69
+ return None
70
+ if isinstance(value, int) and not isinstance(value, bool):
71
+ return value
72
+ if isinstance(value, float) and value.is_integer():
73
+ return int(value)
74
+ if isinstance(value, str) and value.strip():
75
+ try:
76
+ return int(value)
77
+ except ValueError as exc:
78
+ raise ValueError("max_retries must be an int-compatible value") from exc
79
+ raise TypeError("max_retries must be an int, str, or None")
80
+
81
+
82
+ def coerce_dict(value: object) -> dict[str, Any]:
83
+ """Return a string-keyed dictionary built from value if possible.
84
+
85
+ Converts Mapping objects to dictionaries. None returns an empty dict.
86
+
87
+ Parameters
88
+ ----------
89
+ value : object
90
+ Mapping-like value to convert. None yields an empty dictionary.
91
+
92
+ Returns
93
+ -------
94
+ dict[str, Any]
95
+ Dictionary representation of value.
96
+
97
+ Raises
98
+ ------
99
+ TypeError
100
+ If the value cannot be treated as a mapping.
101
+ """
102
+ if value is None:
103
+ return {}
104
+ if isinstance(value, Mapping):
105
+ return dict(value)
106
+ raise TypeError("extra_client_kwargs must be a mapping or None")
107
+
108
+
109
+ def ensure_list(value: Iterable[T] | T | None) -> list[T]:
110
+ """Normalize a single item or iterable into a list.
111
+
112
+ Converts None to empty list, tuples to lists, and wraps single items in a list.
113
+
114
+ Parameters
115
+ ----------
116
+ value : Iterable[T] | T | None
117
+ Item or iterable to wrap. None yields an empty list.
118
+
119
+ Returns
120
+ -------
121
+ list[T]
122
+ Normalized list representation of value.
123
+ """
124
+ if value is None:
125
+ return []
126
+ if isinstance(value, list):
127
+ return value
128
+ if isinstance(value, tuple):
129
+ return list(value)
130
+ return [value] # type: ignore[list-item]
131
+
132
+
133
+ __all__ = [
134
+ "coerce_optional_float",
135
+ "coerce_optional_int",
136
+ "coerce_dict",
137
+ "ensure_list",
138
+ ]
@@ -0,0 +1,167 @@
1
+ """Deprecation utilities for managing deprecated features.
2
+
3
+ This module provides infrastructure for marking and managing deprecated
4
+ functions, classes, and features with consistent warning messages.
5
+
6
+ Functions
7
+ ---------
8
+ deprecated
9
+ Decorator to mark functions or classes as deprecated.
10
+ warn_deprecated
11
+ Emit a deprecation warning with optional custom message.
12
+
13
+ Classes
14
+ -------
15
+ DeprecationHelper
16
+ Utility class for managing deprecation warnings and versions.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import functools
22
+ import warnings
23
+ from typing import Any, Callable, TypeVar
24
+
25
+ F = TypeVar("F", bound=Callable[..., Any])
26
+
27
+
28
+ class DeprecationHelper:
29
+ """Utility class for managing deprecation warnings.
30
+
31
+ Provides consistent formatting and control of deprecation warnings
32
+ across the package.
33
+
34
+ Methods
35
+ -------
36
+ warn
37
+ Emit a deprecation warning with standard formatting.
38
+ """
39
+
40
+ @staticmethod
41
+ def warn(
42
+ feature_name: str,
43
+ removal_version: str,
44
+ alternative: str | None = None,
45
+ extra_message: str | None = None,
46
+ ) -> None:
47
+ """Emit a deprecation warning for a feature.
48
+
49
+ Parameters
50
+ ----------
51
+ feature_name : str
52
+ Name of the deprecated feature (e.g., "MyClass.old_method").
53
+ removal_version : str
54
+ Version in which the feature will be removed.
55
+ alternative : str, optional
56
+ Recommended alternative to use instead.
57
+ extra_message : str, optional
58
+ Additional context or migration instructions.
59
+
60
+ Raises
61
+ ------
62
+ DeprecationWarning
63
+ Always issues a DeprecationWarning to stderr.
64
+ """
65
+ msg = f"{feature_name} is deprecated and will be removed in version {removal_version}."
66
+ if alternative:
67
+ msg += f" Use {alternative} instead."
68
+ if extra_message:
69
+ msg += f" {extra_message}"
70
+
71
+ warnings.warn(msg, DeprecationWarning, stacklevel=3)
72
+
73
+
74
+ def deprecated(
75
+ removal_version: str,
76
+ alternative: str | None = None,
77
+ extra_message: str | None = None,
78
+ ) -> Callable[[F], F]:
79
+ """Mark a function or class as deprecated.
80
+
81
+ Parameters
82
+ ----------
83
+ removal_version : str
84
+ Version in which the decorated feature will be removed.
85
+ alternative : str, optional
86
+ Recommended alternative to use instead.
87
+ extra_message : str, optional
88
+ Additional context or migration instructions.
89
+
90
+ Returns
91
+ -------
92
+ Callable
93
+ Decorator function that wraps the target function or class.
94
+
95
+ Examples
96
+ --------
97
+ >>> @deprecated("1.0.0", "new_function")
98
+ ... def old_function():
99
+ ... pass
100
+
101
+ >>> class OldClass:
102
+ ... @deprecated("1.0.0", "NewClass")
103
+ ... def old_method(self):
104
+ ... pass
105
+ """
106
+
107
+ def decorator(func_or_class: F) -> F:
108
+ feature_name = f"{func_or_class.__module__}.{func_or_class.__qualname__}"
109
+
110
+ if isinstance(func_or_class, type):
111
+ # Handle class deprecation
112
+ original_init = func_or_class.__init__
113
+
114
+ @functools.wraps(original_init)
115
+ def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
116
+ DeprecationHelper.warn(
117
+ feature_name,
118
+ removal_version,
119
+ alternative,
120
+ extra_message,
121
+ )
122
+ original_init(self, *args, **kwargs)
123
+
124
+ func_or_class.__init__ = new_init
125
+ else:
126
+ # Handle function deprecation
127
+ @functools.wraps(func_or_class)
128
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
129
+ DeprecationHelper.warn(
130
+ feature_name,
131
+ removal_version,
132
+ alternative,
133
+ extra_message,
134
+ )
135
+ return func_or_class(*args, **kwargs)
136
+
137
+ return wrapper # type: ignore
138
+
139
+ return func_or_class # type: ignore[return-value]
140
+
141
+ return decorator
142
+
143
+
144
+ def warn_deprecated(
145
+ feature_name: str,
146
+ removal_version: str,
147
+ alternative: str | None = None,
148
+ extra_message: str | None = None,
149
+ ) -> None:
150
+ """Issue a deprecation warning.
151
+
152
+ Parameters
153
+ ----------
154
+ feature_name : str
155
+ Name of the deprecated feature.
156
+ removal_version : str
157
+ Version in which the feature will be removed.
158
+ alternative : str, optional
159
+ Recommended alternative to use instead.
160
+ extra_message : str, optional
161
+ Additional context or migration instructions.
162
+
163
+ Examples
164
+ --------
165
+ >>> warn_deprecated("old_config_key", "1.0.0", "new_config_key")
166
+ """
167
+ DeprecationHelper.warn(feature_name, removal_version, alternative, extra_message)