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
@@ -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)
@@ -20,6 +20,8 @@ from __future__ import annotations
20
20
 
21
21
  from pathlib import Path
22
22
 
23
+ from openai_sdk_helpers.utils import ensure_directory
24
+
23
25
  DATETIME_FMT = "%Y%m%d_%H%M%S"
24
26
  DEFAULT_MODEL = "gpt-4o-mini"
25
27
 
@@ -50,5 +52,4 @@ def get_data_path(name: str) -> Path:
50
52
  """
51
53
  base = Path.home() / ".openai-sdk-helpers"
52
54
  path = base / name
53
- path.mkdir(parents=True, exist_ok=True)
54
- return path
55
+ return ensure_directory(path)
@@ -4,11 +4,8 @@ Provides specific exception types for different error scenarios,
4
4
  improving error handling and debugging capabilities.
5
5
  """
6
6
 
7
- import logging
8
7
  from collections.abc import Mapping
9
8
 
10
- from openai_sdk_helpers.utils.core import log
11
-
12
9
 
13
10
  class OpenAISDKError(Exception):
14
11
  """Base exception for openai-sdk-helpers library.
@@ -40,15 +37,6 @@ class OpenAISDKError(Exception):
40
37
  """Initialize the exception with message and optional context."""
41
38
  super().__init__(message)
42
39
  self.context = dict(context) if context is not None else {}
43
- self._log_context()
44
-
45
- def _log_context(self) -> None:
46
- """Log error with context for debugging."""
47
- context_str = f"\nContext: {self.context}" if self.context else ""
48
- log(
49
- f"{self.__class__.__name__}: {str(self)}{context_str}",
50
- level=logging.ERROR,
51
- )
52
40
 
53
41
 
54
42
  class ConfigurationError(OpenAISDKError):
@@ -0,0 +1,373 @@
1
+ """Comprehensive OpenAI Files API wrapper.
2
+
3
+ This module provides a complete, professional implementation of the OpenAI Files API
4
+ with automatic file tracking, lifecycle management, and cleanup capabilities.
5
+
6
+ References
7
+ ----------
8
+ OpenAI Files API: https://platform.openai.com/docs/api-reference/files
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from pathlib import Path
15
+ from typing import Any, BinaryIO, Literal, cast
16
+
17
+ from openai import OpenAI, NOT_GIVEN
18
+ from openai.types import FileDeleted, FileObject
19
+ from openai.pagination import SyncCursorPage
20
+
21
+ from .utils import log
22
+
23
+ # Valid purposes for file uploads
24
+ FilePurpose = Literal[
25
+ "assistants",
26
+ "batch",
27
+ "fine-tune",
28
+ "user_data",
29
+ "vision",
30
+ ]
31
+
32
+
33
+ class FilesAPIManager:
34
+ """Comprehensive manager for OpenAI Files API operations.
35
+
36
+ Provides full access to the OpenAI Files API with automatic file tracking,
37
+ lifecycle management, and cleanup capabilities. Tracks all uploaded files
38
+ and ensures proper deletion on cleanup.
39
+
40
+ Parameters
41
+ ----------
42
+ client : OpenAI
43
+ OpenAI client instance for API calls.
44
+ auto_track : bool, default True
45
+ Automatically track uploaded files for cleanup.
46
+
47
+ Attributes
48
+ ----------
49
+ tracked_files : dict[str, FileObject]
50
+ Dictionary of tracked file IDs to FileObject instances.
51
+
52
+ Methods
53
+ -------
54
+ create(file, purpose)
55
+ Upload a file to OpenAI Files API.
56
+ retrieve(file_id)
57
+ Retrieve information about a specific file.
58
+ list(purpose, limit)
59
+ List files, optionally filtered by purpose.
60
+ delete(file_id)
61
+ Delete a specific file.
62
+ retrieve_content(file_id)
63
+ Download file content.
64
+ cleanup()
65
+ Delete all tracked files.
66
+
67
+ Examples
68
+ --------
69
+ >>> from openai import OpenAI
70
+ >>> from openai_sdk_helpers.files_api import FilesAPIManager
71
+ >>>
72
+ >>> client = OpenAI()
73
+ >>> files_manager = FilesAPIManager(client)
74
+ >>>
75
+ >>> # Upload a file
76
+ >>> with open("document.pdf", "rb") as f:
77
+ ... file_obj = files_manager.create(f, purpose="user_data")
78
+ >>>
79
+ >>> # List all user data files
80
+ >>> user_files = files_manager.list(purpose="user_data")
81
+ >>>
82
+ >>> # Retrieve file content
83
+ >>> content = files_manager.retrieve_content(file_obj.id)
84
+ >>>
85
+ >>> # Clean up all tracked files
86
+ >>> files_manager.cleanup()
87
+ """
88
+
89
+ def __init__(self, client: OpenAI, auto_track: bool = True):
90
+ """Initialize the Files API manager.
91
+
92
+ Parameters
93
+ ----------
94
+ client : OpenAI
95
+ OpenAI client instance.
96
+ auto_track : bool, default True
97
+ Automatically track uploaded files for cleanup.
98
+ """
99
+ self._client = client
100
+ self._auto_track = auto_track
101
+ self.tracked_files: dict[str, FileObject] = {}
102
+
103
+ def create(
104
+ self,
105
+ file: BinaryIO | Path | str,
106
+ purpose: FilePurpose,
107
+ track: bool | None = None,
108
+ expires_after: int | None = None,
109
+ ) -> FileObject:
110
+ """Upload a file to the OpenAI Files API.
111
+
112
+ Parameters
113
+ ----------
114
+ file : BinaryIO, Path, or str
115
+ File-like object, path to file, or file path string.
116
+ purpose : FilePurpose
117
+ The intended purpose of the uploaded file.
118
+ Options: "assistants", "batch", "fine-tune", "user_data", "vision"
119
+ track : bool or None, default None
120
+ Override auto_track for this file. If None, uses instance setting.
121
+ expires_after : int or None, default None
122
+ Number of seconds after which the file expires and is deleted.
123
+ If None and purpose is "user_data", defaults to 86400 (24 hours).
124
+ For other purposes, files don't expire unless explicitly set.
125
+
126
+ Returns
127
+ -------
128
+ FileObject
129
+ Information about the uploaded file.
130
+
131
+ Raises
132
+ ------
133
+ FileNotFoundError
134
+ If file path doesn't exist.
135
+ ValueError
136
+ If purpose is invalid.
137
+
138
+ Examples
139
+ --------
140
+ >>> # Upload from file path (user_data expires in 24h by default)
141
+ >>> file_obj = manager.create("data.jsonl", purpose="user_data")
142
+ >>>
143
+ >>> # Upload with custom expiration (1 hour)
144
+ >>> file_obj = manager.create("temp.txt", purpose="user_data", expires_after=3600)
145
+ >>>
146
+ >>> # Upload from file handle
147
+ >>> with open("image.png", "rb") as f:
148
+ ... file_obj = manager.create(f, purpose="vision")
149
+ >>>
150
+ >>> # Upload without tracking
151
+ >>> file_obj = manager.create("temp.txt", purpose="user_data", track=False)
152
+ """
153
+ should_track = track if track is not None else self._auto_track
154
+
155
+ # Default to 24 hours expiration for user_data files
156
+ if expires_after is None and purpose == "user_data":
157
+ expires_after = 86400 # 24 hours in seconds
158
+
159
+ # Handle different file input types
160
+ # Prepare expires_after in OpenAI API format if provided
161
+ expires_after_param = None
162
+ if expires_after is not None:
163
+ expires_after_param = cast(
164
+ Any, {"anchor": "created_at", "seconds": expires_after}
165
+ )
166
+
167
+ if isinstance(file, (Path, str)):
168
+ file_path = Path(file).resolve()
169
+ if not file_path.exists():
170
+ raise FileNotFoundError(f"File not found: {file}")
171
+
172
+ # Use only the basename as filename (remove path)
173
+ filename = file_path.name
174
+ with open(file_path, "rb") as f:
175
+ # Pass tuple (filename, file_data) to set custom filename
176
+ if expires_after_param is not None:
177
+ file_obj = self._client.files.create(
178
+ file=(filename, f),
179
+ purpose=purpose,
180
+ expires_after=expires_after_param,
181
+ )
182
+ else:
183
+ file_obj = self._client.files.create(
184
+ file=(filename, f), purpose=purpose
185
+ )
186
+ else:
187
+ # Assume it's a BinaryIO
188
+ if expires_after_param is not None:
189
+ file_obj = self._client.files.create(
190
+ file=file,
191
+ purpose=purpose,
192
+ expires_after=expires_after_param,
193
+ )
194
+ else:
195
+ file_obj = self._client.files.create(file=file, purpose=purpose)
196
+
197
+ if should_track:
198
+ self.tracked_files[file_obj.id] = file_obj
199
+ expiry_msg = f" (expires in {expires_after}s)" if expires_after else ""
200
+ log(
201
+ f"Uploaded and tracking file {file_obj.id} ({file_obj.filename}) "
202
+ f"with purpose '{purpose}'{expiry_msg}"
203
+ )
204
+ else:
205
+ log(
206
+ f"Uploaded file {file_obj.id} ({file_obj.filename}) "
207
+ f"with purpose '{purpose}' (not tracked)"
208
+ )
209
+
210
+ return file_obj
211
+
212
+ def retrieve(self, file_id: str) -> FileObject:
213
+ """Retrieve information about a specific file.
214
+
215
+ Parameters
216
+ ----------
217
+ file_id : str
218
+ The ID of the file to retrieve.
219
+
220
+ Returns
221
+ -------
222
+ FileObject
223
+ Information about the file.
224
+
225
+ Examples
226
+ --------
227
+ >>> file_info = manager.retrieve("file-abc123")
228
+ >>> print(f"Filename: {file_info.filename}")
229
+ >>> print(f"Size: {file_info.bytes} bytes")
230
+ """
231
+ return self._client.files.retrieve(file_id)
232
+
233
+ def list(
234
+ self,
235
+ purpose: FilePurpose | None = None,
236
+ limit: int | None = None,
237
+ ) -> SyncCursorPage[FileObject]:
238
+ """List files, optionally filtered by purpose.
239
+
240
+ Parameters
241
+ ----------
242
+ purpose : FilePurpose or None, default None
243
+ Filter files by purpose. If None, returns all files.
244
+ limit : int or None, default None
245
+ Maximum number of files to return. If None, returns all.
246
+
247
+ Returns
248
+ -------
249
+ SyncCursorPage[FileObject]
250
+ Page of file objects matching the criteria.
251
+
252
+ Examples
253
+ --------
254
+ >>> # List all files
255
+ >>> all_files = manager.list()
256
+ >>>
257
+ >>> # List user data files
258
+ >>> user_files = manager.list(purpose="user_data")
259
+ >>>
260
+ >>> # List up to 10 files
261
+ >>> recent_files = manager.list(limit=10)
262
+ """
263
+ limit_param = NOT_GIVEN if limit is None else limit
264
+ if purpose is not None:
265
+ return self._client.files.list(
266
+ purpose=purpose, limit=cast(Any, limit_param)
267
+ )
268
+ return self._client.files.list(limit=cast(Any, limit_param))
269
+
270
+ def delete(self, file_id: str, untrack: bool = True) -> FileDeleted:
271
+ """Delete a specific file from OpenAI Files API.
272
+
273
+ Parameters
274
+ ----------
275
+ file_id : str
276
+ The ID of the file to delete.
277
+ untrack : bool, default True
278
+ Remove from tracked files after deletion.
279
+
280
+ Returns
281
+ -------
282
+ FileDeleted
283
+ Confirmation of file deletion.
284
+
285
+ Examples
286
+ --------
287
+ >>> result = manager.delete("file-abc123")
288
+ >>> print(f"Deleted: {result.deleted}")
289
+ """
290
+ result = self._client.files.delete(file_id)
291
+
292
+ if untrack and file_id in self.tracked_files:
293
+ del self.tracked_files[file_id]
294
+ log(f"Deleted and untracked file {file_id}")
295
+ else:
296
+ log(f"Deleted file {file_id}")
297
+
298
+ return result
299
+
300
+ def retrieve_content(self, file_id: str) -> bytes:
301
+ """Download and retrieve the content of a file.
302
+
303
+ Parameters
304
+ ----------
305
+ file_id : str
306
+ The ID of the file to download.
307
+
308
+ Returns
309
+ -------
310
+ bytes
311
+ The raw bytes of the file content.
312
+
313
+ Examples
314
+ --------
315
+ >>> content = manager.retrieve_content("file-abc123")
316
+ >>> with open("downloaded.pdf", "wb") as f:
317
+ ... f.write(content)
318
+ """
319
+ return self._client.files.content(file_id).read()
320
+
321
+ def cleanup(self) -> dict[str, bool]:
322
+ """Delete all tracked files.
323
+
324
+ Returns
325
+ -------
326
+ dict[str, bool]
327
+ Dictionary mapping file IDs to deletion success status.
328
+
329
+ Examples
330
+ --------
331
+ >>> results = manager.cleanup()
332
+ >>> print(f"Deleted {sum(results.values())} files")
333
+ """
334
+ results = {}
335
+ file_ids = list(self.tracked_files.keys())
336
+
337
+ for file_id in file_ids:
338
+ try:
339
+ self.delete(file_id, untrack=True)
340
+ results[file_id] = True
341
+ except Exception as exc:
342
+ log(
343
+ f"Error deleting tracked file {file_id}: {exc}",
344
+ level=logging.WARNING,
345
+ )
346
+ results[file_id] = False
347
+
348
+ if results:
349
+ successful = sum(results.values())
350
+ log(f"Cleanup complete: {successful}/{len(results)} files deleted")
351
+ else:
352
+ log("No tracked files to clean up")
353
+
354
+ return results
355
+
356
+ def __enter__(self) -> FilesAPIManager:
357
+ """Context manager entry."""
358
+ return self
359
+
360
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
361
+ """Context manager exit with automatic cleanup."""
362
+ self.cleanup()
363
+
364
+ def __len__(self) -> int:
365
+ """Return number of tracked files."""
366
+ return len(self.tracked_files)
367
+
368
+ def __repr__(self) -> str:
369
+ """Return string representation of the manager."""
370
+ return f"FilesAPIManager(tracked_files={len(self.tracked_files)})"
371
+
372
+
373
+ __all__ = ["FilesAPIManager", "FilePurpose"]