openai-sdk-helpers 0.0.9__py3-none-any.whl → 0.1.1__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 (46) hide show
  1. openai_sdk_helpers/__init__.py +63 -5
  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 +120 -31
  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/logging_config.py +24 -95
  14. openai_sdk_helpers/prompt/base.py +56 -6
  15. openai_sdk_helpers/response/__init__.py +5 -2
  16. openai_sdk_helpers/response/base.py +84 -115
  17. openai_sdk_helpers/response/config.py +142 -0
  18. openai_sdk_helpers/response/messages.py +1 -0
  19. openai_sdk_helpers/response/tool_call.py +15 -4
  20. openai_sdk_helpers/retry.py +1 -1
  21. openai_sdk_helpers/streamlit_app/app.py +14 -3
  22. openai_sdk_helpers/streamlit_app/streamlit_web_search.py +15 -8
  23. openai_sdk_helpers/structure/__init__.py +3 -0
  24. openai_sdk_helpers/structure/base.py +6 -6
  25. openai_sdk_helpers/structure/plan/__init__.py +15 -1
  26. openai_sdk_helpers/structure/plan/helpers.py +173 -0
  27. openai_sdk_helpers/structure/plan/plan.py +13 -9
  28. openai_sdk_helpers/structure/plan/task.py +7 -7
  29. openai_sdk_helpers/structure/plan/types.py +15 -0
  30. openai_sdk_helpers/tools.py +296 -0
  31. openai_sdk_helpers/utils/__init__.py +82 -31
  32. openai_sdk_helpers/{async_utils.py → utils/async_utils.py} +5 -6
  33. openai_sdk_helpers/utils/coercion.py +138 -0
  34. openai_sdk_helpers/utils/deprecation.py +167 -0
  35. openai_sdk_helpers/utils/json_utils.py +98 -0
  36. openai_sdk_helpers/utils/output_validation.py +448 -0
  37. openai_sdk_helpers/utils/path_utils.py +46 -0
  38. openai_sdk_helpers/{validation.py → utils/validation.py} +7 -3
  39. openai_sdk_helpers/vector_storage/storage.py +9 -6
  40. {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.1.dist-info}/METADATA +59 -3
  41. openai_sdk_helpers-0.1.1.dist-info/RECORD +76 -0
  42. openai_sdk_helpers-0.1.1.dist-info/entry_points.txt +2 -0
  43. openai_sdk_helpers/utils/core.py +0 -468
  44. openai_sdk_helpers-0.0.9.dist-info/RECORD +0 -66
  45. {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.1.dist-info}/WHEEL +0 -0
  46. {openai_sdk_helpers-0.0.9.dist-info → openai_sdk_helpers-0.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,51 +1,79 @@
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, and logging. These utilities are used
5
- 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.
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.
23
10
 
24
- Classes
25
- -------
26
- JSONSerializable
27
- Mixin for classes that can be serialized to JSON.
28
- customJSONEncoder
29
- JSON encoder for common helper types like enums and paths.
11
+ Import style
12
+ ------------
13
+ Public helpers are re-exported from ``openai_sdk_helpers.utils`` for a
14
+ consistent import surface. You can import from submodules when you need a
15
+ smaller surface area, but top-level imports remain stable.
16
+
17
+ Submodules
18
+ ----------
19
+ coercion
20
+ Numeric coercion helpers and list normalization.
21
+ path_utils
22
+ File and path helpers.
23
+ json_utils
24
+ JSON encoding helpers and mixins.
25
+ logging_config
26
+ Centralized logger factory and convenience log helper.
27
+ validation
28
+ Input validation helpers for strings, URLs, collections, and paths.
29
+ async_utils
30
+ Async-to-sync bridging helpers.
31
+ output_validation
32
+ JSON Schema and semantic output validation utilities.
33
+ deprecation
34
+ Deprecation helpers and warning utilities.
30
35
  """
31
36
 
32
37
  from __future__ import annotations
33
38
 
34
- from .core import (
35
- JSONSerializable,
36
- check_filepath,
37
- coerce_jsonable,
39
+ from .coercion import (
38
40
  coerce_dict,
39
41
  coerce_optional_float,
40
42
  coerce_optional_int,
41
- customJSONEncoder,
42
43
  ensure_list,
43
- log,
44
44
  )
45
+ from .json_utils import (
46
+ JSONSerializable,
47
+ coerce_jsonable,
48
+ customJSONEncoder,
49
+ )
50
+ from .path_utils import check_filepath, ensure_directory
51
+ from openai_sdk_helpers.logging_config import log
52
+ from .validation import (
53
+ validate_choice,
54
+ validate_dict_mapping,
55
+ validate_list_items,
56
+ validate_max_length,
57
+ validate_non_empty_string,
58
+ validate_safe_path,
59
+ validate_url_format,
60
+ )
61
+ from .async_utils import run_coroutine_thread_safe, run_coroutine_with_fallback
62
+ from .output_validation import (
63
+ JSONSchemaValidator,
64
+ LengthValidator,
65
+ OutputValidator,
66
+ SemanticValidator,
67
+ ValidationResult,
68
+ ValidationRule,
69
+ validate_output,
70
+ )
71
+ from .deprecation import DeprecationHelper, deprecated, warn_deprecated
45
72
 
46
73
  __all__ = [
47
74
  "ensure_list",
48
75
  "check_filepath",
76
+ "ensure_directory",
49
77
  "coerce_optional_float",
50
78
  "coerce_optional_int",
51
79
  "coerce_dict",
@@ -53,4 +81,27 @@ __all__ = [
53
81
  "JSONSerializable",
54
82
  "customJSONEncoder",
55
83
  "log",
84
+ # Validation helpers
85
+ "validate_non_empty_string",
86
+ "validate_max_length",
87
+ "validate_url_format",
88
+ "validate_dict_mapping",
89
+ "validate_list_items",
90
+ "validate_choice",
91
+ "validate_safe_path",
92
+ # Async helpers
93
+ "run_coroutine_thread_safe",
94
+ "run_coroutine_with_fallback",
95
+ # Output validation
96
+ "ValidationResult",
97
+ "ValidationRule",
98
+ "JSONSchemaValidator",
99
+ "SemanticValidator",
100
+ "LengthValidator",
101
+ "OutputValidator",
102
+ "validate_output",
103
+ # Deprecation
104
+ "deprecated",
105
+ "warn_deprecated",
106
+ "DeprecationHelper",
56
107
  ]
@@ -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)
@@ -0,0 +1,98 @@
1
+ """JSON serialization helpers for helper types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, is_dataclass
7
+ from datetime import datetime
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .path_utils import check_filepath
13
+
14
+
15
+ def _to_jsonable(value: Any) -> Any:
16
+ """Convert common helper types to JSON-serializable forms."""
17
+ from openai_sdk_helpers.structure.base import BaseStructure
18
+
19
+ if value is None:
20
+ return None
21
+ if isinstance(value, Enum):
22
+ return value.value
23
+ if isinstance(value, Path):
24
+ return str(value)
25
+ if isinstance(value, datetime):
26
+ return value.isoformat()
27
+ if is_dataclass(value) and not isinstance(value, type):
28
+ return {k: _to_jsonable(v) for k, v in asdict(value).items()}
29
+ if hasattr(value, "model_dump"):
30
+ model_dump = getattr(value, "model_dump")
31
+ return model_dump()
32
+ if isinstance(value, dict):
33
+ return {str(k): _to_jsonable(v) for k, v in value.items()}
34
+ if isinstance(value, (list, tuple, set)):
35
+ return [_to_jsonable(v) for v in value]
36
+ if isinstance(value, BaseStructure):
37
+ return value.model_dump()
38
+ return value
39
+
40
+
41
+ def coerce_jsonable(value: Any) -> Any:
42
+ """Convert value into a JSON-serializable representation."""
43
+ from openai_sdk_helpers.response.base import BaseResponse
44
+
45
+ if value is None:
46
+ return None
47
+ if isinstance(value, BaseResponse):
48
+ return coerce_jsonable(value.messages.to_json())
49
+ if is_dataclass(value) and not isinstance(value, type):
50
+ return {key: coerce_jsonable(item) for key, item in asdict(value).items()}
51
+ coerced = _to_jsonable(value)
52
+ try:
53
+ json.dumps(coerced)
54
+ return coerced
55
+ except TypeError:
56
+ return str(coerced)
57
+
58
+
59
+ class customJSONEncoder(json.JSONEncoder):
60
+ """JSON encoder for common helper types like enums and paths."""
61
+
62
+ def default(self, o: Any) -> Any: # noqa: D401
63
+ """Return JSON-serializable representation of ``o``."""
64
+ return _to_jsonable(o)
65
+
66
+
67
+ class JSONSerializable:
68
+ """Mixin for classes that can be serialized to JSON."""
69
+
70
+ def to_json(self) -> dict[str, Any]:
71
+ """Return a JSON-compatible dict representation."""
72
+ if is_dataclass(self) and not isinstance(self, type):
73
+ return {k: _to_jsonable(v) for k, v in asdict(self).items()}
74
+ if hasattr(self, "model_dump"):
75
+ model_dump = getattr(self, "model_dump")
76
+ return _to_jsonable(model_dump())
77
+ return _to_jsonable(self.__dict__)
78
+
79
+ def to_json_file(self, filepath: str | Path) -> str:
80
+ """Write serialized JSON data to a file path."""
81
+ target = Path(filepath)
82
+ check_filepath(fullfilepath=str(target))
83
+ with open(target, "w", encoding="utf-8") as handle:
84
+ json.dump(
85
+ self.to_json(),
86
+ handle,
87
+ indent=2,
88
+ ensure_ascii=False,
89
+ cls=customJSONEncoder,
90
+ )
91
+ return str(target)
92
+
93
+
94
+ __all__ = [
95
+ "coerce_jsonable",
96
+ "JSONSerializable",
97
+ "customJSONEncoder",
98
+ ]