openai-sdk-helpers 0.0.7__py3-none-any.whl → 0.0.9__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 (63) hide show
  1. openai_sdk_helpers/__init__.py +85 -10
  2. openai_sdk_helpers/agent/__init__.py +8 -4
  3. openai_sdk_helpers/agent/base.py +81 -46
  4. openai_sdk_helpers/agent/config.py +6 -4
  5. openai_sdk_helpers/agent/{project_manager.py → coordination.py} +29 -45
  6. openai_sdk_helpers/agent/prompt_utils.py +7 -1
  7. openai_sdk_helpers/agent/runner.py +67 -141
  8. openai_sdk_helpers/agent/search/__init__.py +33 -0
  9. openai_sdk_helpers/agent/search/base.py +297 -0
  10. openai_sdk_helpers/agent/{vector_search.py → search/vector.py} +89 -157
  11. openai_sdk_helpers/agent/{web_search.py → search/web.py} +82 -162
  12. openai_sdk_helpers/agent/summarizer.py +29 -8
  13. openai_sdk_helpers/agent/translator.py +40 -13
  14. openai_sdk_helpers/agent/validation.py +32 -8
  15. openai_sdk_helpers/async_utils.py +132 -0
  16. openai_sdk_helpers/config.py +74 -36
  17. openai_sdk_helpers/context_manager.py +241 -0
  18. openai_sdk_helpers/enums/__init__.py +9 -1
  19. openai_sdk_helpers/enums/base.py +67 -8
  20. openai_sdk_helpers/environment.py +33 -6
  21. openai_sdk_helpers/errors.py +133 -0
  22. openai_sdk_helpers/logging_config.py +105 -0
  23. openai_sdk_helpers/prompt/__init__.py +10 -71
  24. openai_sdk_helpers/prompt/base.py +172 -0
  25. openai_sdk_helpers/response/__init__.py +37 -5
  26. openai_sdk_helpers/response/base.py +427 -189
  27. openai_sdk_helpers/response/config.py +176 -0
  28. openai_sdk_helpers/response/messages.py +104 -40
  29. openai_sdk_helpers/response/runner.py +79 -35
  30. openai_sdk_helpers/response/tool_call.py +75 -12
  31. openai_sdk_helpers/response/vector_store.py +29 -16
  32. openai_sdk_helpers/retry.py +175 -0
  33. openai_sdk_helpers/streamlit_app/__init__.py +30 -0
  34. openai_sdk_helpers/streamlit_app/app.py +345 -0
  35. openai_sdk_helpers/streamlit_app/config.py +502 -0
  36. openai_sdk_helpers/streamlit_app/streamlit_web_search.py +68 -0
  37. openai_sdk_helpers/structure/__init__.py +69 -3
  38. openai_sdk_helpers/structure/agent_blueprint.py +82 -19
  39. openai_sdk_helpers/structure/base.py +245 -91
  40. openai_sdk_helpers/structure/plan/__init__.py +15 -1
  41. openai_sdk_helpers/structure/plan/enum.py +41 -5
  42. openai_sdk_helpers/structure/plan/plan.py +101 -45
  43. openai_sdk_helpers/structure/plan/task.py +38 -6
  44. openai_sdk_helpers/structure/prompt.py +21 -2
  45. openai_sdk_helpers/structure/responses.py +52 -11
  46. openai_sdk_helpers/structure/summary.py +55 -7
  47. openai_sdk_helpers/structure/validation.py +34 -6
  48. openai_sdk_helpers/structure/vector_search.py +132 -18
  49. openai_sdk_helpers/structure/web_search.py +128 -12
  50. openai_sdk_helpers/types.py +57 -0
  51. openai_sdk_helpers/utils/__init__.py +32 -1
  52. openai_sdk_helpers/utils/core.py +200 -32
  53. openai_sdk_helpers/validation.py +302 -0
  54. openai_sdk_helpers/vector_storage/__init__.py +21 -1
  55. openai_sdk_helpers/vector_storage/cleanup.py +25 -13
  56. openai_sdk_helpers/vector_storage/storage.py +124 -66
  57. openai_sdk_helpers/vector_storage/types.py +20 -19
  58. openai_sdk_helpers-0.0.9.dist-info/METADATA +550 -0
  59. openai_sdk_helpers-0.0.9.dist-info/RECORD +66 -0
  60. openai_sdk_helpers-0.0.7.dist-info/METADATA +0 -193
  61. openai_sdk_helpers-0.0.7.dist-info/RECORD +0 -51
  62. {openai_sdk_helpers-0.0.7.dist-info → openai_sdk_helpers-0.0.9.dist-info}/WHEEL +0 -0
  63. {openai_sdk_helpers-0.0.7.dist-info → openai_sdk_helpers-0.0.9.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,132 @@
1
+ """Async/sync bridge utilities with proper error handling.
2
+
3
+ Provides thread-safe wrappers for running async code from sync contexts
4
+ with proper exception propagation and timeout handling.
5
+ """
6
+
7
+ import asyncio
8
+ import queue
9
+ import threading
10
+ from typing import Any, Coroutine, Generic, TypeVar
11
+
12
+ from openai_sdk_helpers.errors import AsyncExecutionError
13
+ from openai_sdk_helpers.utils.core import log
14
+
15
+ T = TypeVar("T")
16
+
17
+ # Default timeout constants
18
+ DEFAULT_COROUTINE_TIMEOUT = 300.0 # 5 minutes
19
+ THREAD_JOIN_TIMEOUT = 5.0 # 5 seconds
20
+
21
+
22
+ def run_coroutine_thread_safe(
23
+ coro: Coroutine[Any, Any, T],
24
+ timeout: float = DEFAULT_COROUTINE_TIMEOUT,
25
+ ) -> T:
26
+ """Run a coroutine in a thread-safe manner from a sync context.
27
+
28
+ Uses a queue to safely communicate results and exceptions between threads.
29
+ Ensures exceptions from the async operation are properly propagated.
30
+
31
+ Parameters
32
+ ----------
33
+ coro : Coroutine
34
+ The coroutine to execute.
35
+ timeout : float
36
+ Maximum time in seconds to wait for the coroutine to complete.
37
+ Default is 300 (5 minutes).
38
+
39
+ Returns
40
+ -------
41
+ Any
42
+ Result from the coroutine.
43
+
44
+ Raises
45
+ ------
46
+ AsyncExecutionError
47
+ If the coroutine fails or timeout occurs.
48
+
49
+ Examples
50
+ --------
51
+ >>> async def fetch_data():
52
+ ... return "data"
53
+ >>> result = run_coroutine_thread_safe(fetch_data())
54
+ """
55
+ result_queue: queue.Queue[T | Exception] = queue.Queue()
56
+
57
+ def _thread_runner() -> None:
58
+ """Run coroutine and put result in queue."""
59
+ try:
60
+ result = asyncio.run(coro)
61
+ result_queue.put(result)
62
+ except Exception as exc:
63
+ # Queue stores the exception to propagate later
64
+ result_queue.put(exc)
65
+
66
+ thread = threading.Thread(target=_thread_runner, daemon=False)
67
+ thread.start()
68
+
69
+ try:
70
+ result = result_queue.get(timeout=timeout)
71
+ if isinstance(result, Exception):
72
+ # Re-raise the exception from the thread
73
+ raise result
74
+ return result
75
+ except queue.Empty:
76
+ raise AsyncExecutionError(
77
+ f"Coroutine execution timed out after {timeout} seconds"
78
+ ) from None
79
+ finally:
80
+ # Ensure thread is cleaned up
81
+ thread.join(timeout=THREAD_JOIN_TIMEOUT)
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
+ )
87
+
88
+
89
+ def run_coroutine_with_fallback(
90
+ coro: Coroutine[Any, Any, T],
91
+ ) -> T:
92
+ """Run a coroutine, falling back to thread if event loop is already running.
93
+
94
+ Attempts to run the coroutine directly if no event loop is present.
95
+ If an event loop is already running (nested scenario), creates a new
96
+ thread to avoid the "RuntimeError: asyncio.run() cannot be called from a
97
+ running event loop" error.
98
+
99
+ Parameters
100
+ ----------
101
+ coro : Coroutine
102
+ The coroutine to execute.
103
+
104
+ Returns
105
+ -------
106
+ Any
107
+ Result from the coroutine.
108
+
109
+ Raises
110
+ ------
111
+ AsyncExecutionError
112
+ If execution fails or times out.
113
+
114
+ Examples
115
+ --------
116
+ >>> async def fetch_data():
117
+ ... return "data"
118
+ >>> result = run_coroutine_with_fallback(fetch_data())
119
+ """
120
+ try:
121
+ # Try to get currently running loop
122
+ loop = asyncio.get_running_loop()
123
+ except RuntimeError:
124
+ # No running loop, safe to use asyncio.run()
125
+ return asyncio.run(coro)
126
+
127
+ # Loop is already running, must use thread
128
+ if loop.is_running():
129
+ return run_coroutine_thread_safe(coro)
130
+
131
+ # This shouldn't happen but handle defensive
132
+ return loop.run_until_complete(coro)
@@ -1,10 +1,15 @@
1
- """Shared configuration for OpenAI SDK usage."""
1
+ """Shared configuration for OpenAI SDK usage.
2
+
3
+ This module provides the OpenAISettings class for centralized management of
4
+ OpenAI client configuration, reading from environment variables and .env files.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
5
9
  import os
10
+ from collections.abc import Mapping
6
11
  from pathlib import Path
7
- from typing import Any, Dict, Mapping, Optional
12
+ from typing import Any
8
13
 
9
14
  from dotenv import dotenv_values
10
15
  from openai import OpenAI
@@ -20,6 +25,25 @@ from openai_sdk_helpers.utils import (
20
25
  class OpenAISettings(BaseModel):
21
26
  """Configuration helpers for constructing OpenAI clients.
22
27
 
28
+ This class centralizes OpenAI SDK configuration by reading from environment
29
+ variables and optional `.env` files, enabling consistent client setup across
30
+ your application.
31
+
32
+ Examples
33
+ --------
34
+ Load settings from environment and create a client:
35
+
36
+ >>> from openai_sdk_helpers import OpenAISettings
37
+ >>> settings = OpenAISettings.from_env()
38
+ >>> client = settings.create_client()
39
+
40
+ Override specific settings:
41
+
42
+ >>> settings = OpenAISettings.from_env(
43
+ ... default_model="gpt-4o",
44
+ ... timeout=60.0
45
+ ... )
46
+
23
47
  Methods
24
48
  -------
25
49
  from_env(dotenv_path, **overrides)
@@ -32,84 +56,92 @@ class OpenAISettings(BaseModel):
32
56
 
33
57
  model_config = ConfigDict(extra="ignore")
34
58
 
35
- api_key: Optional[str] = Field(
59
+ api_key: str | None = Field(
36
60
  default=None,
37
61
  description=(
38
- "API key used to authenticate requests. Defaults to ``OPENAI_API_KEY``"
62
+ "API key used to authenticate requests. Defaults to OPENAI_API_KEY"
39
63
  " from the environment."
40
64
  ),
41
65
  )
42
- org_id: Optional[str] = Field(
66
+ org_id: str | None = Field(
43
67
  default=None,
44
68
  description=(
45
69
  "Organization identifier applied to outgoing requests. Defaults to"
46
- " ``OPENAI_ORG_ID``."
70
+ " OPENAI_ORG_ID."
47
71
  ),
48
72
  )
49
- project_id: Optional[str] = Field(
73
+ project_id: str | None = Field(
50
74
  default=None,
51
75
  description=(
52
76
  "Project identifier used for billing and resource scoping. Defaults to"
53
- " ``OPENAI_PROJECT_ID``."
77
+ " OPENAI_PROJECT_ID."
54
78
  ),
55
79
  )
56
- base_url: Optional[str] = Field(
80
+ base_url: str | None = Field(
57
81
  default=None,
58
82
  description=(
59
83
  "Custom base URL for self-hosted or proxied deployments. Defaults to"
60
- " ``OPENAI_BASE_URL``."
84
+ " OPENAI_BASE_URL."
61
85
  ),
62
86
  )
63
- default_model: Optional[str] = Field(
87
+ default_model: str | None = Field(
64
88
  default=None,
65
89
  description=(
66
90
  "Model name used when constructing agents if no model is explicitly"
67
- " provided. Defaults to ``OPENAI_MODEL``."
91
+ " provided. Defaults to OPENAI_MODEL."
68
92
  ),
69
93
  )
70
- timeout: Optional[float] = Field(
94
+ timeout: float | None = Field(
71
95
  default=None,
72
96
  description=(
73
97
  "Request timeout in seconds applied to all OpenAI client calls."
74
- " Defaults to ``OPENAI_TIMEOUT``."
98
+ " Defaults to OPENAI_TIMEOUT."
75
99
  ),
76
100
  )
77
- max_retries: Optional[int] = Field(
101
+ max_retries: int | None = Field(
78
102
  default=None,
79
103
  description=(
80
104
  "Maximum number of automatic retries for transient failures."
81
- " Defaults to ``OPENAI_MAX_RETRIES``."
105
+ " Defaults to OPENAI_MAX_RETRIES."
82
106
  ),
83
107
  )
84
- extra_client_kwargs: Dict[str, Any] = Field(
108
+ extra_client_kwargs: dict[str, Any] = Field(
85
109
  default_factory=dict,
86
110
  description=(
87
- "Additional keyword arguments forwarded to ``openai.OpenAI``. Use"
88
- " this for less common options like ``default_headers`` or"
89
- " ``http_client``."
111
+ "Additional keyword arguments forwarded to openai.OpenAI. Use"
112
+ " this for less common options like default_headers or"
113
+ " http_client."
90
114
  ),
91
115
  )
92
116
 
93
117
  @classmethod
94
118
  def from_env(
95
- cls, dotenv_path: Optional[Path] = None, **overrides: Any
96
- ) -> "OpenAISettings":
119
+ cls, dotenv_path: Path | None = None, **overrides: Any
120
+ ) -> OpenAISettings:
97
121
  """Load settings from the environment and optional overrides.
98
122
 
123
+ Reads configuration from environment variables and an optional .env
124
+ file, with explicit overrides taking precedence.
125
+
99
126
  Parameters
100
127
  ----------
101
- dotenv_path : Path | None
102
- Path to a ``.env`` file to load before reading environment
103
- variables. Default ``None``.
128
+ dotenv_path : Path or None, optional
129
+ Path to a .env file to load before reading environment
130
+ variables, by default None.
104
131
  overrides : Any
105
132
  Keyword overrides applied on top of environment values.
106
133
 
107
134
  Returns
108
135
  -------
109
136
  OpenAISettings
110
- Settings instance populated from environment variables and overrides.
137
+ Settings instance populated from environment variables and overrides.
138
+
139
+ Raises
140
+ ------
141
+ ValueError
142
+ If OPENAI_API_KEY is not found in environment or dotenv file.
111
143
  """
112
- env_file_values: Mapping[str, Optional[str]]
144
+ env_file_values: Mapping[str, str | None]
113
145
  if dotenv_path is not None:
114
146
  env_file_values = dotenv_values(dotenv_path)
115
147
  else:
@@ -126,7 +158,7 @@ class OpenAISettings(BaseModel):
126
158
  or os.getenv("OPENAI_MAX_RETRIES")
127
159
  )
128
160
 
129
- values: Dict[str, Any] = {
161
+ values: dict[str, Any] = {
130
162
  "api_key": overrides.get("api_key")
131
163
  or env_file_values.get("OPENAI_API_KEY")
132
164
  or os.getenv("OPENAI_API_KEY"),
@@ -161,16 +193,19 @@ class OpenAISettings(BaseModel):
161
193
 
162
194
  return settings
163
195
 
164
- def client_kwargs(self) -> Dict[str, Any]:
165
- """Return keyword arguments for constructing an ``OpenAI`` client.
196
+ def client_kwargs(self) -> dict[str, Any]:
197
+ """Return keyword arguments for constructing an OpenAI client.
198
+
199
+ Builds a dictionary containing all configured authentication and
200
+ routing parameters suitable for OpenAI client initialization.
166
201
 
167
202
  Returns
168
203
  -------
169
- dict
170
- Keyword arguments populated with available authentication and routing
171
- values.
204
+ dict[str, Any]
205
+ Keyword arguments populated with available authentication and
206
+ routing values.
172
207
  """
173
- kwargs: Dict[str, Any] = dict(self.extra_client_kwargs)
208
+ kwargs: dict[str, Any] = dict(self.extra_client_kwargs)
174
209
  if self.api_key:
175
210
  kwargs["api_key"] = self.api_key
176
211
  if self.org_id:
@@ -186,12 +221,15 @@ class OpenAISettings(BaseModel):
186
221
  return kwargs
187
222
 
188
223
  def create_client(self) -> OpenAI:
189
- """Instantiate an ``OpenAI`` client using the stored configuration.
224
+ """Instantiate an OpenAI client using the stored configuration.
225
+
226
+ Uses client_kwargs() to build the client with all configured
227
+ authentication and routing parameters.
190
228
 
191
229
  Returns
192
230
  -------
193
231
  OpenAI
194
- Client initialized with ``client_kwargs``.
232
+ Client initialized with the configured settings.
195
233
  """
196
234
  return OpenAI(**self.client_kwargs())
197
235
 
@@ -0,0 +1,241 @@
1
+ """Context manager utilities for resource cleanup.
2
+
3
+ Provides base classes and utilities for proper resource management
4
+ with guaranteed cleanup on exit or exception.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from contextlib import asynccontextmanager
11
+ from types import TracebackType
12
+ from typing import Any, AsyncIterator, Generic, Optional, TypeVar
13
+
14
+ from openai_sdk_helpers.utils.core import log
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ class ManagedResource(Generic[T]):
20
+ """Base class for resources that need cleanup.
21
+
22
+ Provides context manager support for guaranteed resource cleanup
23
+ even when exceptions occur.
24
+
25
+ Examples
26
+ --------
27
+ >>> class DatabaseConnection(ManagedResource[Connection]):
28
+ ... def __init__(self, connection):
29
+ ... self.connection = connection
30
+ ...
31
+ ... def close(self) -> None:
32
+ ... if self.connection:
33
+ ... self.connection.close()
34
+
35
+ >>> with DatabaseConnection(connect()) as db:
36
+ ... db.query("SELECT ...")
37
+ """
38
+
39
+ def __enter__(self) -> T:
40
+ """Enter context manager.
41
+
42
+ Returns
43
+ -------
44
+ T
45
+ The resource instance (self cast appropriately).
46
+ """
47
+ return self # type: ignore
48
+
49
+ def __exit__(
50
+ self,
51
+ exc_type: Optional[type[BaseException]],
52
+ exc_val: Optional[BaseException],
53
+ exc_tb: Optional[TracebackType],
54
+ ) -> bool:
55
+ """Exit context manager with cleanup.
56
+
57
+ Parameters
58
+ ----------
59
+ exc_type : type[BaseException] | None
60
+ Type of exception if one was raised, None otherwise.
61
+ exc_val : BaseException | None
62
+ Exception instance if one was raised, None otherwise.
63
+ exc_tb : TracebackType | None
64
+ Traceback if exception was raised, None otherwise.
65
+
66
+ Returns
67
+ -------
68
+ bool
69
+ False to re-raise exceptions, True to suppress them.
70
+ """
71
+ try:
72
+ self.close()
73
+ except Exception as exc:
74
+ log(f"Error during cleanup: {exc}", level=30) # logging.WARNING
75
+ # Don't suppress cleanup errors
76
+ if exc_type is None:
77
+ raise
78
+
79
+ return False # Re-raise exceptions
80
+
81
+ def close(self) -> None:
82
+ """Close and cleanup the resource.
83
+
84
+ Should be overridden by subclasses to perform actual cleanup.
85
+ Should not raise exceptions, but may log them.
86
+
87
+ Raises
88
+ ------
89
+ Exception
90
+ May raise if cleanup fails catastrophically.
91
+ """
92
+ pass
93
+
94
+
95
+ class AsyncManagedResource(Generic[T]):
96
+ """Base class for async resources that need cleanup.
97
+
98
+ Provides async context manager support for guaranteed resource cleanup
99
+ even when exceptions occur.
100
+
101
+ Examples
102
+ --------
103
+ >>> class AsyncDatabaseConnection(AsyncManagedResource[AsyncConnection]):
104
+ ... def __init__(self, connection):
105
+ ... self.connection = connection
106
+ ...
107
+ ... async def close(self) -> None:
108
+ ... if self.connection:
109
+ ... await self.connection.close()
110
+
111
+ >>> async with AsyncDatabaseConnection(await connect()) as db:
112
+ ... await db.query("SELECT ...")
113
+ """
114
+
115
+ async def __aenter__(self) -> T:
116
+ """Enter async context manager.
117
+
118
+ Returns
119
+ -------
120
+ T
121
+ The resource instance (self cast appropriately).
122
+ """
123
+ return self # type: ignore
124
+
125
+ async def __aexit__(
126
+ self,
127
+ exc_type: Optional[type[BaseException]],
128
+ exc_val: Optional[BaseException],
129
+ exc_tb: Optional[TracebackType],
130
+ ) -> bool:
131
+ """Exit async context manager with cleanup.
132
+
133
+ Parameters
134
+ ----------
135
+ exc_type : type[BaseException] | None
136
+ Type of exception if one was raised, None otherwise.
137
+ exc_val : BaseException | None
138
+ Exception instance if one was raised, None otherwise.
139
+ exc_tb : TracebackType | None
140
+ Traceback if exception was raised, None otherwise.
141
+
142
+ Returns
143
+ -------
144
+ bool
145
+ False to re-raise exceptions, True to suppress them.
146
+ """
147
+ try:
148
+ await self.close()
149
+ except Exception as exc:
150
+ log(f"Error during async cleanup: {exc}", level=30) # logging.WARNING
151
+ # Don't suppress cleanup errors
152
+ if exc_type is None:
153
+ raise
154
+
155
+ return False # Re-raise exceptions
156
+
157
+ async def close(self) -> None:
158
+ """Close and cleanup the resource asynchronously.
159
+
160
+ Should be overridden by subclasses to perform actual cleanup.
161
+ Should not raise exceptions, but may log them.
162
+
163
+ Raises
164
+ ------
165
+ Exception
166
+ May raise if cleanup fails catastrophically.
167
+ """
168
+ pass
169
+
170
+
171
+ def ensure_closed(resource: Any) -> None:
172
+ """Safely close a resource if it has a close method.
173
+
174
+ Logs errors but doesn't raise them.
175
+
176
+ Parameters
177
+ ----------
178
+ resource : Any
179
+ Object that may have a close() method.
180
+ """
181
+ if resource is None:
182
+ return
183
+
184
+ close_method = getattr(resource, "close", None)
185
+ if callable(close_method):
186
+ try:
187
+ close_method()
188
+ except Exception as exc:
189
+ log(f"Error closing {type(resource).__name__}: {exc}", level=30)
190
+
191
+
192
+ async def ensure_closed_async(resource: Any) -> None:
193
+ """Safely close a resource asynchronously if it has an async close method.
194
+
195
+ Logs errors but doesn't raise them.
196
+
197
+ Parameters
198
+ ----------
199
+ resource : Any
200
+ Object that may have an async close() method.
201
+ """
202
+ if resource is None:
203
+ return
204
+
205
+ close_method = getattr(resource, "close", None)
206
+ if callable(close_method):
207
+ try:
208
+ if asyncio.iscoroutinefunction(close_method):
209
+ await close_method()
210
+ else:
211
+ close_method()
212
+ except Exception as exc:
213
+ log(
214
+ f"Error closing async {type(resource).__name__}: {exc}",
215
+ level=30,
216
+ )
217
+
218
+
219
+ @asynccontextmanager
220
+ async def async_context(resource: AsyncManagedResource[T]) -> AsyncIterator[T]:
221
+ """Context manager for async resources.
222
+
223
+ Parameters
224
+ ----------
225
+ resource : AsyncManagedResource
226
+ Async resource to manage.
227
+
228
+ Yields
229
+ ------
230
+ T
231
+ The resource instance.
232
+
233
+ Examples
234
+ --------
235
+ >>> async with async_context(my_resource) as resource:
236
+ ... await resource.do_something()
237
+ """
238
+ try:
239
+ yield await resource.__aenter__()
240
+ finally:
241
+ await resource.__aexit__(None, None, None)
@@ -1,4 +1,12 @@
1
- """Enum helpers for openai-sdk-helpers."""
1
+ """Enum utilities for OpenAI SDK helpers.
2
+
3
+ This module provides specialized enum base classes with metadata capabilities.
4
+
5
+ Classes
6
+ -------
7
+ CrosswalkJSONEnum
8
+ String-based enum with crosswalk metadata support.
9
+ """
2
10
 
3
11
  from __future__ import annotations
4
12
 
@@ -1,29 +1,88 @@
1
- """Base enum helpers."""
1
+ """Base enum classes with metadata support.
2
+
3
+ This module defines specialized enum base classes that extend the standard
4
+ library's Enum class with additional metadata and serialization capabilities.
5
+ """
2
6
 
3
7
  from __future__ import annotations
4
8
 
5
9
  from enum import Enum
10
+ from typing import Any
6
11
 
7
12
 
8
13
  class CrosswalkJSONEnum(str, Enum):
9
- """Enum base class with crosswalk metadata hooks.
14
+ """String-based enum with crosswalk metadata support.
15
+
16
+ Extends both str and Enum to provide string-compatible enum values
17
+ with optional metadata mappings. Subclasses must implement the
18
+ CROSSWALK class method to provide structured metadata for each
19
+ enum member.
20
+
21
+ This design allows enums to carry additional context beyond their
22
+ values, useful for configuration, validation, or documentation purposes.
10
23
 
11
24
  Methods
12
25
  -------
13
26
  CROSSWALK()
14
- Return metadata describing enum values keyed by name.
27
+ Return metadata describing enum values keyed by member name.
28
+
29
+ Examples
30
+ --------
31
+ >>> class Status(CrosswalkJSONEnum):
32
+ ... ACTIVE = "active"
33
+ ... INACTIVE = "inactive"
34
+ ...
35
+ ... @classmethod
36
+ ... def CROSSWALK(cls):
37
+ ... return {
38
+ ... "ACTIVE": {"description": "Currently active"},
39
+ ... "INACTIVE": {"description": "Not active"}
40
+ ... }
41
+ >>> Status.ACTIVE.value
42
+ 'active'
43
+ >>> Status.CROSSWALK()["ACTIVE"]["description"]
44
+ 'Currently active'
15
45
  """
16
46
 
17
47
  @classmethod
18
- def CROSSWALK(cls) -> dict[str, dict[str, object]]:
19
- """Return metadata describing enum values keyed by name.
48
+ def CROSSWALK(cls) -> dict[str, dict[str, Any]]:
49
+ """Return metadata describing enum values keyed by member name.
50
+
51
+ Subclasses must override this method to provide structured
52
+ metadata for each enum member. The outer dictionary keys
53
+ correspond to enum member names, and values are dictionaries
54
+ containing arbitrary metadata.
20
55
 
21
56
  Returns
22
57
  -------
23
- dict[str, dict[str, object]]
24
- Mapping of enum member names to structured metadata details.
58
+ dict[str, dict[str, Any]]
59
+ Mapping of enum member names to their metadata dictionaries.
60
+ Each metadata dictionary can contain any relevant key-value
61
+ pairs describing that enum member.
62
+
63
+ Raises
64
+ ------
65
+ NotImplementedError
66
+ If called on a subclass that has not implemented this method.
67
+
68
+ Examples
69
+ --------
70
+ >>> class Priority(CrosswalkJSONEnum):
71
+ ... HIGH = "high"
72
+ ... LOW = "low"
73
+ ...
74
+ ... @classmethod
75
+ ... def CROSSWALK(cls):
76
+ ... return {
77
+ ... "HIGH": {"value": "high", "weight": 10},
78
+ ... "LOW": {"value": "low", "weight": 1}
79
+ ... }
80
+ >>> Priority.CROSSWALK()["HIGH"]["weight"]
81
+ 10
25
82
  """
26
- raise NotImplementedError("CROSSWALK must be implemented by subclasses.")
83
+ raise NotImplementedError(
84
+ f"{cls.__name__}.CROSSWALK() must be implemented by subclasses"
85
+ )
27
86
 
28
87
 
29
88
  __all__ = ["CrosswalkJSONEnum"]