uipath-langchain 0.0.85__py3-none-any.whl → 0.0.88__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.

Potentially problematic release.


This version of uipath-langchain might be problematic. Click here for more details.

Files changed (30) hide show
  1. uipath_langchain/_cli/_runtime/_context.py +1 -1
  2. uipath_langchain/_cli/_runtime/_escalation.py +3 -3
  3. uipath_langchain/_cli/_runtime/_exception.py +1 -1
  4. uipath_langchain/_cli/_runtime/_input.py +3 -3
  5. uipath_langchain/_cli/_runtime/_output.py +34 -11
  6. uipath_langchain/_cli/_runtime/_runtime.py +1 -1
  7. uipath_langchain/_cli/cli_init.py +2 -2
  8. uipath_langchain/_cli/cli_run.py +2 -2
  9. uipath_langchain/middlewares.py +1 -1
  10. uipath_langchain/retrievers/context_grounding_retriever.py +30 -4
  11. uipath_langchain/tracers/AsyncUiPathTracer.py +2 -2
  12. uipath_langchain/tracers/UiPathTracer.py +1 -1
  13. uipath_langchain/tracers/_events.py +33 -33
  14. uipath_langchain/tracers/_instrument_traceable.py +285 -285
  15. uipath_langchain/tracers/_utils.py +52 -52
  16. uipath_langchain/utils/__init__.py +3 -0
  17. uipath_langchain/utils/_request_mixin.py +488 -0
  18. uipath_langchain/utils/_settings.py +91 -0
  19. uipath_langchain/utils/_sleep_policy.py +41 -0
  20. uipath_langchain/vectorstores/context_grounding_vectorstore.py +265 -0
  21. uipath_langchain-0.0.88.dist-info/METADATA +136 -0
  22. uipath_langchain-0.0.88.dist-info/RECORD +40 -0
  23. {uipath_langchain-0.0.85.dist-info → uipath_langchain-0.0.88.dist-info}/entry_points.txt +1 -1
  24. uipath_langchain-0.0.88.dist-info/licenses/LICENSE +21 -0
  25. uipath_langchain/_utils/tests/cached_embeddings/text-embedding-3-large5034ec3c-85c9-54b8-ac89-5e0cbcf99e3b +0 -3
  26. uipath_langchain/_utils/tests/cached_embeddings/text-embedding-3-largec48857ed-1302-5954-9e24-69fa9b45e457 +0 -3
  27. uipath_langchain/_utils/tests/tests_uipath_cache.db +0 -3
  28. uipath_langchain-0.0.85.dist-info/METADATA +0 -29
  29. uipath_langchain-0.0.85.dist-info/RECORD +0 -37
  30. {uipath_langchain-0.0.85.dist-info → uipath_langchain-0.0.88.dist-info}/WHEEL +0 -0
@@ -1,285 +1,285 @@
1
- import functools
2
- import importlib
3
- import inspect
4
- import logging
5
- import sys
6
- import uuid
7
- from typing import Any, Dict, List, Literal, Optional
8
-
9
- from langchain_core.callbacks import dispatch_custom_event
10
-
11
- from ._events import CustomTraceEvents, FunctionCallEventData
12
-
13
- # Original module and traceable function references
14
- original_langsmith: Any = None
15
- original_traceable: Any = None
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- def dispatch_trace_event(
21
- func_name,
22
- inputs: Dict[str, Any],
23
- event_type="call",
24
- call_uuid=None,
25
- result=None,
26
- exception=None,
27
- run_type: Optional[
28
- Literal["tool", "chain", "llm", "retriever", "embedding", "prompt", "parser"]
29
- ] = None,
30
- tags: Optional[List[str]] = None,
31
- metadata: Optional[Dict[str, Any]] = None,
32
- ):
33
- """Dispatch trace event to our server."""
34
-
35
- event_data = FunctionCallEventData(
36
- function_name=func_name,
37
- event_type=event_type,
38
- inputs=inputs,
39
- call_uuid=call_uuid,
40
- output=result,
41
- error=str(exception) if exception else None,
42
- run_type=run_type,
43
- tags=tags,
44
- metadata=metadata,
45
- )
46
- dispatch_custom_event(CustomTraceEvents.UIPATH_TRACE_FUNCTION_CALL, event_data)
47
-
48
-
49
- def format_args_for_trace(
50
- signature: inspect.Signature, *args: Any, **kwargs: Any
51
- ) -> Dict[str, Any]:
52
- try:
53
- """Return a dictionary of inputs from the function signature."""
54
- # Create a parameter mapping by partially binding the arguments
55
- parameter_binding = signature.bind_partial(*args, **kwargs)
56
-
57
- # Fill in default values for any unspecified parameters
58
- parameter_binding.apply_defaults()
59
-
60
- # Extract the input parameters, skipping special Python parameters
61
- result = {}
62
- for name, value in parameter_binding.arguments.items():
63
- # Skip class and instance references
64
- if name in ("self", "cls"):
65
- continue
66
-
67
- # Handle **kwargs parameters specially
68
- param_info = signature.parameters.get(name)
69
- if param_info and param_info.kind == inspect.Parameter.VAR_KEYWORD:
70
- # Flatten nested kwargs directly into the result
71
- if isinstance(value, dict):
72
- result.update(value)
73
- else:
74
- # Regular parameter
75
- result[name] = value
76
-
77
- return result
78
- except Exception as e:
79
- logger.warning(
80
- f"Error formatting arguments for trace: {e}. Using args and kwargs directly."
81
- )
82
- return {"args": args, "kwargs": kwargs}
83
-
84
-
85
- # Create patched version of traceable
86
- def patched_traceable(*decorator_args, **decorator_kwargs):
87
- # Handle the case when @traceable is used directly as decorator without arguments
88
- if (
89
- len(decorator_args) == 1
90
- and callable(decorator_args[0])
91
- and not decorator_kwargs
92
- ):
93
- func = decorator_args[0]
94
- return _create_appropriate_wrapper(func, original_traceable(func), {})
95
-
96
- # Handle the case when @traceable(args) is used with parameters
97
- original_decorated = original_traceable(*decorator_args, **decorator_kwargs)
98
-
99
- def uipath_trace_decorator(func):
100
- # Apply the original decorator with its arguments
101
- wrapped_func = original_decorated(func)
102
- return _create_appropriate_wrapper(func, wrapped_func, decorator_kwargs)
103
-
104
- return uipath_trace_decorator
105
-
106
-
107
- def _create_appropriate_wrapper(
108
- original_func: Any, wrapped_func: Any, decorator_kwargs: Dict[str, Any]
109
- ):
110
- """Create the appropriate wrapper based on function type."""
111
-
112
- # Get the function name and tags from decorator arguments
113
- func_name = decorator_kwargs.get("name", original_func.__name__)
114
- tags = decorator_kwargs.get("tags", None)
115
- metadata = decorator_kwargs.get("metadata", None)
116
- run_type = decorator_kwargs.get("run_type", None)
117
-
118
- # Async generator function
119
- if inspect.isasyncgenfunction(wrapped_func):
120
-
121
- @functools.wraps(wrapped_func)
122
- async def async_gen_wrapper(*args, **kwargs):
123
- try:
124
- call_uuid = str(uuid.uuid4())
125
-
126
- inputs = format_args_for_trace(
127
- inspect.signature(original_func), *args, **kwargs
128
- )
129
-
130
- dispatch_trace_event(
131
- func_name,
132
- inputs,
133
- "call",
134
- call_uuid,
135
- run_type=run_type,
136
- tags=tags,
137
- metadata=metadata,
138
- )
139
- async_gen = wrapped_func(*args, **kwargs)
140
-
141
- results = []
142
-
143
- async for item in async_gen:
144
- results.append(item)
145
- yield item
146
-
147
- dispatch_trace_event(
148
- func_name, inputs, "completion", call_uuid, results
149
- )
150
- except Exception as e:
151
- dispatch_trace_event(
152
- func_name, inputs, "completion", call_uuid, exception=e
153
- )
154
- raise
155
-
156
- return async_gen_wrapper
157
-
158
- # Sync generator function
159
- elif inspect.isgeneratorfunction(wrapped_func):
160
-
161
- @functools.wraps(wrapped_func)
162
- def gen_wrapper(*args, **kwargs):
163
- try:
164
- call_uuid = str(uuid.uuid4())
165
-
166
- inputs = format_args_for_trace(
167
- inspect.signature(original_func), *args, **kwargs
168
- )
169
-
170
- results = []
171
-
172
- dispatch_trace_event(
173
- func_name,
174
- inputs,
175
- "call",
176
- call_uuid,
177
- run_type=run_type,
178
- tags=tags,
179
- metadata=metadata,
180
- )
181
- gen = wrapped_func(*args, **kwargs)
182
- for item in gen:
183
- results.append(item)
184
- yield item
185
- dispatch_trace_event(
186
- func_name, inputs, "completion", call_uuid, results
187
- )
188
- except Exception as e:
189
- dispatch_trace_event(
190
- func_name, inputs, "completion", call_uuid, exception=e
191
- )
192
- raise
193
-
194
- return gen_wrapper
195
-
196
- # Async function
197
- elif inspect.iscoroutinefunction(wrapped_func):
198
-
199
- @functools.wraps(wrapped_func)
200
- async def async_wrapper(*args, **kwargs):
201
- try:
202
- call_uuid = str(uuid.uuid4())
203
-
204
- inputs = format_args_for_trace(
205
- inspect.signature(original_func), *args, **kwargs
206
- )
207
-
208
- dispatch_trace_event(
209
- func_name,
210
- inputs,
211
- "call",
212
- call_uuid,
213
- run_type=run_type,
214
- tags=tags,
215
- metadata=metadata,
216
- )
217
- result = await wrapped_func(*args, **kwargs)
218
- dispatch_trace_event(func_name, inputs, "completion", call_uuid, result)
219
- return result
220
- except Exception as e:
221
- dispatch_trace_event(
222
- func_name, inputs, "completion", call_uuid, exception=e
223
- )
224
- raise
225
-
226
- return async_wrapper
227
-
228
- # Regular sync function (default case)
229
- else:
230
-
231
- @functools.wraps(wrapped_func)
232
- def sync_wrapper(*args, **kwargs):
233
- try:
234
- call_uuid = str(uuid.uuid4())
235
-
236
- inputs = format_args_for_trace(
237
- inspect.signature(original_func), *args, **kwargs
238
- )
239
-
240
- dispatch_trace_event(
241
- func_name,
242
- inputs,
243
- "call",
244
- call_uuid,
245
- run_type=run_type,
246
- tags=tags,
247
- metadata=metadata,
248
- )
249
- result = wrapped_func(*args, **kwargs)
250
- dispatch_trace_event(func_name, inputs, "completion", call_uuid, result)
251
- return result
252
- except Exception as e:
253
- dispatch_trace_event(
254
- func_name, inputs, "completion", call_uuid, exception=e
255
- )
256
- raise
257
-
258
- return sync_wrapper
259
-
260
-
261
- # Apply the patch
262
- def _instrument_traceable():
263
- """Apply the patch to langsmith module at import time."""
264
- global original_langsmith, original_traceable
265
-
266
- # Import the original module if not already done
267
- if original_langsmith is None:
268
- # Temporarily remove our custom module from sys.modules
269
- if "langsmith" in sys.modules:
270
- original_langsmith = sys.modules["langsmith"]
271
- del sys.modules["langsmith"]
272
-
273
- # Import the original module
274
- original_langsmith = importlib.import_module("langsmith")
275
-
276
- # Store the original traceable
277
- original_traceable = original_langsmith.traceable
278
-
279
- # Replace the traceable function with our patched version
280
- original_langsmith.traceable = patched_traceable
281
-
282
- # Put our modified module back
283
- sys.modules["langsmith"] = original_langsmith
284
-
285
- return original_langsmith
1
+ import functools
2
+ import importlib
3
+ import inspect
4
+ import logging
5
+ import sys
6
+ import uuid
7
+ from typing import Any, Dict, List, Literal, Optional
8
+
9
+ from langchain_core.callbacks import dispatch_custom_event
10
+
11
+ from ._events import CustomTraceEvents, FunctionCallEventData
12
+
13
+ # Original module and traceable function references
14
+ original_langsmith: Any = None
15
+ original_traceable: Any = None
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def dispatch_trace_event(
21
+ func_name,
22
+ inputs: Dict[str, Any],
23
+ event_type="call",
24
+ call_uuid=None,
25
+ result=None,
26
+ exception=None,
27
+ run_type: Optional[
28
+ Literal["tool", "chain", "llm", "retriever", "embedding", "prompt", "parser"]
29
+ ] = None,
30
+ tags: Optional[List[str]] = None,
31
+ metadata: Optional[Dict[str, Any]] = None,
32
+ ):
33
+ """Dispatch trace event to our server."""
34
+
35
+ event_data = FunctionCallEventData(
36
+ function_name=func_name,
37
+ event_type=event_type,
38
+ inputs=inputs,
39
+ call_uuid=call_uuid,
40
+ output=result,
41
+ error=str(exception) if exception else None,
42
+ run_type=run_type,
43
+ tags=tags,
44
+ metadata=metadata,
45
+ )
46
+ dispatch_custom_event(CustomTraceEvents.UIPATH_TRACE_FUNCTION_CALL, event_data)
47
+
48
+
49
+ def format_args_for_trace(
50
+ signature: inspect.Signature, *args: Any, **kwargs: Any
51
+ ) -> Dict[str, Any]:
52
+ try:
53
+ """Return a dictionary of inputs from the function signature."""
54
+ # Create a parameter mapping by partially binding the arguments
55
+ parameter_binding = signature.bind_partial(*args, **kwargs)
56
+
57
+ # Fill in default values for any unspecified parameters
58
+ parameter_binding.apply_defaults()
59
+
60
+ # Extract the input parameters, skipping special Python parameters
61
+ result = {}
62
+ for name, value in parameter_binding.arguments.items():
63
+ # Skip class and instance references
64
+ if name in ("self", "cls"):
65
+ continue
66
+
67
+ # Handle **kwargs parameters specially
68
+ param_info = signature.parameters.get(name)
69
+ if param_info and param_info.kind == inspect.Parameter.VAR_KEYWORD:
70
+ # Flatten nested kwargs directly into the result
71
+ if isinstance(value, dict):
72
+ result.update(value)
73
+ else:
74
+ # Regular parameter
75
+ result[name] = value
76
+
77
+ return result
78
+ except Exception as e:
79
+ logger.warning(
80
+ f"Error formatting arguments for trace: {e}. Using args and kwargs directly."
81
+ )
82
+ return {"args": args, "kwargs": kwargs}
83
+
84
+
85
+ # Create patched version of traceable
86
+ def patched_traceable(*decorator_args, **decorator_kwargs):
87
+ # Handle the case when @traceable is used directly as decorator without arguments
88
+ if (
89
+ len(decorator_args) == 1
90
+ and callable(decorator_args[0])
91
+ and not decorator_kwargs
92
+ ):
93
+ func = decorator_args[0]
94
+ return _create_appropriate_wrapper(func, original_traceable(func), {})
95
+
96
+ # Handle the case when @traceable(args) is used with parameters
97
+ original_decorated = original_traceable(*decorator_args, **decorator_kwargs)
98
+
99
+ def uipath_trace_decorator(func):
100
+ # Apply the original decorator with its arguments
101
+ wrapped_func = original_decorated(func)
102
+ return _create_appropriate_wrapper(func, wrapped_func, decorator_kwargs)
103
+
104
+ return uipath_trace_decorator
105
+
106
+
107
+ def _create_appropriate_wrapper(
108
+ original_func: Any, wrapped_func: Any, decorator_kwargs: Dict[str, Any]
109
+ ):
110
+ """Create the appropriate wrapper based on function type."""
111
+
112
+ # Get the function name and tags from decorator arguments
113
+ func_name = decorator_kwargs.get("name", original_func.__name__)
114
+ tags = decorator_kwargs.get("tags", None)
115
+ metadata = decorator_kwargs.get("metadata", None)
116
+ run_type = decorator_kwargs.get("run_type", None)
117
+
118
+ # Async generator function
119
+ if inspect.isasyncgenfunction(wrapped_func):
120
+
121
+ @functools.wraps(wrapped_func)
122
+ async def async_gen_wrapper(*args, **kwargs):
123
+ try:
124
+ call_uuid = str(uuid.uuid4())
125
+
126
+ inputs = format_args_for_trace(
127
+ inspect.signature(original_func), *args, **kwargs
128
+ )
129
+
130
+ dispatch_trace_event(
131
+ func_name,
132
+ inputs,
133
+ "call",
134
+ call_uuid,
135
+ run_type=run_type,
136
+ tags=tags,
137
+ metadata=metadata,
138
+ )
139
+ async_gen = wrapped_func(*args, **kwargs)
140
+
141
+ results = []
142
+
143
+ async for item in async_gen:
144
+ results.append(item)
145
+ yield item
146
+
147
+ dispatch_trace_event(
148
+ func_name, inputs, "completion", call_uuid, results
149
+ )
150
+ except Exception as e:
151
+ dispatch_trace_event(
152
+ func_name, inputs, "completion", call_uuid, exception=e
153
+ )
154
+ raise
155
+
156
+ return async_gen_wrapper
157
+
158
+ # Sync generator function
159
+ elif inspect.isgeneratorfunction(wrapped_func):
160
+
161
+ @functools.wraps(wrapped_func)
162
+ def gen_wrapper(*args, **kwargs):
163
+ try:
164
+ call_uuid = str(uuid.uuid4())
165
+
166
+ inputs = format_args_for_trace(
167
+ inspect.signature(original_func), *args, **kwargs
168
+ )
169
+
170
+ results = []
171
+
172
+ dispatch_trace_event(
173
+ func_name,
174
+ inputs,
175
+ "call",
176
+ call_uuid,
177
+ run_type=run_type,
178
+ tags=tags,
179
+ metadata=metadata,
180
+ )
181
+ gen = wrapped_func(*args, **kwargs)
182
+ for item in gen:
183
+ results.append(item)
184
+ yield item
185
+ dispatch_trace_event(
186
+ func_name, inputs, "completion", call_uuid, results
187
+ )
188
+ except Exception as e:
189
+ dispatch_trace_event(
190
+ func_name, inputs, "completion", call_uuid, exception=e
191
+ )
192
+ raise
193
+
194
+ return gen_wrapper
195
+
196
+ # Async function
197
+ elif inspect.iscoroutinefunction(wrapped_func):
198
+
199
+ @functools.wraps(wrapped_func)
200
+ async def async_wrapper(*args, **kwargs):
201
+ try:
202
+ call_uuid = str(uuid.uuid4())
203
+
204
+ inputs = format_args_for_trace(
205
+ inspect.signature(original_func), *args, **kwargs
206
+ )
207
+
208
+ dispatch_trace_event(
209
+ func_name,
210
+ inputs,
211
+ "call",
212
+ call_uuid,
213
+ run_type=run_type,
214
+ tags=tags,
215
+ metadata=metadata,
216
+ )
217
+ result = await wrapped_func(*args, **kwargs)
218
+ dispatch_trace_event(func_name, inputs, "completion", call_uuid, result)
219
+ return result
220
+ except Exception as e:
221
+ dispatch_trace_event(
222
+ func_name, inputs, "completion", call_uuid, exception=e
223
+ )
224
+ raise
225
+
226
+ return async_wrapper
227
+
228
+ # Regular sync function (default case)
229
+ else:
230
+
231
+ @functools.wraps(wrapped_func)
232
+ def sync_wrapper(*args, **kwargs):
233
+ try:
234
+ call_uuid = str(uuid.uuid4())
235
+
236
+ inputs = format_args_for_trace(
237
+ inspect.signature(original_func), *args, **kwargs
238
+ )
239
+
240
+ dispatch_trace_event(
241
+ func_name,
242
+ inputs,
243
+ "call",
244
+ call_uuid,
245
+ run_type=run_type,
246
+ tags=tags,
247
+ metadata=metadata,
248
+ )
249
+ result = wrapped_func(*args, **kwargs)
250
+ dispatch_trace_event(func_name, inputs, "completion", call_uuid, result)
251
+ return result
252
+ except Exception as e:
253
+ dispatch_trace_event(
254
+ func_name, inputs, "completion", call_uuid, exception=e
255
+ )
256
+ raise
257
+
258
+ return sync_wrapper
259
+
260
+
261
+ # Apply the patch
262
+ def _instrument_traceable():
263
+ """Apply the patch to langsmith module at import time."""
264
+ global original_langsmith, original_traceable
265
+
266
+ # Import the original module if not already done
267
+ if original_langsmith is None:
268
+ # Temporarily remove our custom module from sys.modules
269
+ if "langsmith" in sys.modules:
270
+ original_langsmith = sys.modules["langsmith"]
271
+ del sys.modules["langsmith"]
272
+
273
+ # Import the original module
274
+ original_langsmith = importlib.import_module("langsmith")
275
+
276
+ # Store the original traceable
277
+ original_traceable = original_langsmith.traceable
278
+
279
+ # Replace the traceable function with our patched version
280
+ original_langsmith.traceable = patched_traceable
281
+
282
+ # Put our modified module back
283
+ sys.modules["langsmith"] = original_langsmith
284
+
285
+ return original_langsmith
@@ -1,52 +1,52 @@
1
- import datetime
2
- import logging
3
- from zoneinfo import ZoneInfo
4
-
5
-
6
- class IgnoreSpecificUrl(logging.Filter):
7
- def __init__(self, url_to_ignore):
8
- super().__init__()
9
- self.url_to_ignore = url_to_ignore
10
-
11
- def filter(self, record):
12
- try:
13
- if record.msg == 'HTTP Request: %s %s "%s %d %s"':
14
- # Ignore the log if the URL matches the one we want to ignore
15
- method = record.args[0]
16
- url = record.args[1]
17
-
18
- if method == "POST" and url.path.endswith(self.url_to_ignore):
19
- # Check if the URL contains the specific path we want to ignore
20
- return True
21
- return False
22
-
23
- except Exception:
24
- return False
25
-
26
-
27
- def _setup_tracer_httpx_logging(url: str):
28
- # Create a custom logger for httpx
29
- # Add the custom filter to the root logger
30
- logging.getLogger("httpx").addFilter(IgnoreSpecificUrl(url))
31
-
32
-
33
- def _simple_serialize_defaults(obj):
34
- if hasattr(obj, "model_dump"):
35
- return obj.model_dump(exclude_none=True, mode="json")
36
- if hasattr(obj, "dict"):
37
- return obj.dict()
38
- if hasattr(obj, "to_dict"):
39
- return obj.to_dict()
40
-
41
- if isinstance(obj, (set, tuple)):
42
- if hasattr(obj, "_asdict") and callable(obj._asdict):
43
- return obj._asdict()
44
- return list(obj)
45
-
46
- if isinstance(obj, datetime.datetime):
47
- return obj.isoformat()
48
-
49
- if isinstance(obj, (datetime.timezone, ZoneInfo)):
50
- return obj.tzname(None)
51
-
52
- return str(obj)
1
+ import datetime
2
+ import logging
3
+ from zoneinfo import ZoneInfo
4
+
5
+
6
+ class IgnoreSpecificUrl(logging.Filter):
7
+ def __init__(self, url_to_ignore):
8
+ super().__init__()
9
+ self.url_to_ignore = url_to_ignore
10
+
11
+ def filter(self, record):
12
+ try:
13
+ if record.msg == 'HTTP Request: %s %s "%s %d %s"':
14
+ # Ignore the log if the URL matches the one we want to ignore
15
+ method = record.args[0]
16
+ url = record.args[1]
17
+
18
+ if method == "POST" and url.path.endswith(self.url_to_ignore):
19
+ # Check if the URL contains the specific path we want to ignore
20
+ return True
21
+ return False
22
+
23
+ except Exception:
24
+ return False
25
+
26
+
27
+ def _setup_tracer_httpx_logging(url: str):
28
+ # Create a custom logger for httpx
29
+ # Add the custom filter to the root logger
30
+ logging.getLogger("httpx").addFilter(IgnoreSpecificUrl(url))
31
+
32
+
33
+ def _simple_serialize_defaults(obj):
34
+ if hasattr(obj, "model_dump"):
35
+ return obj.model_dump(exclude_none=True, mode="json")
36
+ if hasattr(obj, "dict"):
37
+ return obj.dict()
38
+ if hasattr(obj, "to_dict"):
39
+ return obj.to_dict()
40
+
41
+ if isinstance(obj, (set, tuple)):
42
+ if hasattr(obj, "_asdict") and callable(obj._asdict):
43
+ return obj._asdict()
44
+ return list(obj)
45
+
46
+ if isinstance(obj, datetime.datetime):
47
+ return obj.isoformat()
48
+
49
+ if isinstance(obj, (datetime.timezone, ZoneInfo)):
50
+ return obj.tzname(None)
51
+
52
+ return str(obj)