microsoft-teams-common 0.0.1a1__tar.gz

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 (20) hide show
  1. microsoft_teams_common-0.0.1a1/.gitignore +34 -0
  2. microsoft_teams_common-0.0.1a1/PKG-INFO +22 -0
  3. microsoft_teams_common-0.0.1a1/README.md +7 -0
  4. microsoft_teams_common-0.0.1a1/pyproject.toml +38 -0
  5. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/__init__.py +17 -0
  6. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/events/__init__.py +8 -0
  7. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/events/event_emitter.py +227 -0
  8. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/http/__init__.py +19 -0
  9. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/http/client.py +438 -0
  10. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/http/client_token.py +43 -0
  11. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/http/interceptor.py +42 -0
  12. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/logging/__init__.py +11 -0
  13. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/logging/ansi.py +41 -0
  14. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/logging/console.py +56 -0
  15. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/logging/filter.py +35 -0
  16. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/logging/formatter.py +43 -0
  17. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/storage/__init__.py +10 -0
  18. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/storage/list_local_storage.py +67 -0
  19. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/storage/local_storage.py +77 -0
  20. microsoft_teams_common-0.0.1a1/src/microsoft/teams/common/storage/storage.py +98 -0
@@ -0,0 +1,34 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+
7
+ # Environments
8
+ .env
9
+ .venv
10
+ env/
11
+ venv/
12
+ ENV/
13
+ env.bak/
14
+ venv.bak/
15
+
16
+ # mypy
17
+ .mypy_cache/
18
+ .dmypy.json
19
+ dmypy.json
20
+
21
+ .copilot-instructions.md
22
+
23
+ # other
24
+ .DS_STORE
25
+ *.bak
26
+ *~
27
+ *.tmp
28
+
29
+ ref/
30
+ py.typed
31
+ CLAUDE.md
32
+
33
+ .env.claude/
34
+ .claude/
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: microsoft-teams-common
3
+ Version: 0.0.1a1
4
+ Summary: Common package for Microsoft Teams
5
+ Project-URL: Homepage, https://github.com/microsoft/teams.py/tree/main/packages/common/src/microsoft/teams/common
6
+ Author-email: Microsoft <TeamsAISDKFeedback@microsoft.com>
7
+ License-Expression: MIT
8
+ Keywords: agents,ai,bot,microsoft,teams
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: coverage>=7.8.0
11
+ Requires-Dist: httpx>=0.28.1
12
+ Requires-Dist: pytest>=8.3.5
13
+ Requires-Dist: ruff>=0.11.5
14
+ Description-Content-Type: text/markdown
15
+
16
+ > [!CAUTION]
17
+ > This project is in active development and not ready for production use. It has not been publicly announced yet.
18
+
19
+ # Microsoft Teams Common Utilities
20
+
21
+ Shared utilities including HTTP client, logging, storage, and event handling.
22
+ Provides common functionality used across other Teams SDK packages.
@@ -0,0 +1,7 @@
1
+ > [!CAUTION]
2
+ > This project is in active development and not ready for production use. It has not been publicly announced yet.
3
+
4
+ # Microsoft Teams Common Utilities
5
+
6
+ Shared utilities including HTTP client, logging, storage, and event handling.
7
+ Provides common functionality used across other Teams SDK packages.
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "microsoft-teams-common"
3
+ version = "0.0.1-alpha.1"
4
+ description = "Common package for Microsoft Teams"
5
+ readme = "README.md"
6
+ repository = "https://github.com/microsoft/teams.py"
7
+ keywords = ["microsoft", "teams", "ai", "bot", "agents"]
8
+ license = "MIT"
9
+ authors = [
10
+ { name = "Microsoft", email = "TeamsAISDKFeedback@microsoft.com" }
11
+ ]
12
+ requires-python = ">=3.12"
13
+ dependencies = [
14
+ "coverage>=7.8.0",
15
+ "httpx>=0.28.1",
16
+ "pytest>=8.3.5",
17
+ "ruff>=0.11.5",
18
+ ]
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "pytest>=8.4.0",
23
+ "pytest-asyncio>=1.0.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/microsoft/teams.py/tree/main/packages/common/src/microsoft/teams/common"
28
+
29
+
30
+ [build-system]
31
+ requires = ["hatchling"]
32
+ build-backend = "hatchling.build"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/microsoft"]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ include = ["src"]
@@ -0,0 +1,17 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from . import events, http, logging, storage # noqa: E402
7
+ from .events import * # noqa: F401, F402, F403
8
+ from .http import * # noqa: F401, F402, F403
9
+ from .logging import * # noqa: F401, F402, F403
10
+ from .storage import * # noqa: F401, F402, F403
11
+
12
+ # Combine all exports from submodules
13
+ __all__: list[str] = []
14
+ __all__.extend(events.__all__)
15
+ __all__.extend(http.__all__)
16
+ __all__.extend(logging.__all__)
17
+ __all__.extend(storage.__all__)
@@ -0,0 +1,8 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from .event_emitter import EventEmitter, EventEmitterOptions, EventEmitterProtocol, EventHandler
7
+
8
+ __all__ = ["EventEmitter", "EventEmitterOptions", "EventHandler", "EventEmitterProtocol"]
@@ -0,0 +1,227 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ import asyncio
7
+ import logging
8
+ from typing import Any, Awaitable, Callable, Dict, Generic, List, Optional, Protocol, TypedDict, TypeVar, Union
9
+
10
+ from ..logging import ConsoleLogger
11
+
12
+ EventTypeT = TypeVar("EventTypeT", bound=str, contravariant=True)
13
+
14
+ EventHandler = Union[
15
+ Callable[[Any], None], # Sync handler
16
+ Callable[[Any], Awaitable[Any]], # Async handler
17
+ ]
18
+
19
+
20
+ class Subscription(TypedDict):
21
+ """A subscription entry for an event handler."""
22
+
23
+ id: int
24
+ handler: EventHandler
25
+
26
+
27
+ class EventEmitterProtocol(Protocol, Generic[EventTypeT]):
28
+ """Interface for event emitter functionality."""
29
+
30
+ def on(self, event: EventTypeT, handler: EventHandler) -> int:
31
+ """Register an event handler. Returns subscription ID."""
32
+ ...
33
+
34
+ def once(self, event: EventTypeT, handler: EventHandler) -> int:
35
+ """Register a one-time event handler. Returns subscription ID."""
36
+ ...
37
+
38
+ def off(self, subscription_id: int) -> None:
39
+ """Remove an event handler by subscription ID."""
40
+
41
+ def emit(self, event: EventTypeT, value: Any = None) -> None:
42
+ """Emit an event synchronously."""
43
+
44
+
45
+ class EventEmitterOptions(TypedDict, total=False):
46
+ """
47
+ Options for EventEmitter configuration.
48
+
49
+ :param logger: Custom logger instance to use for logging events
50
+ """
51
+
52
+ logger: logging.Logger
53
+
54
+
55
+ class EventEmitter(EventEmitterProtocol[EventTypeT]):
56
+ """
57
+ Event emitter implementation inspired by TypeScript/Node.js EventEmitter.
58
+
59
+ Provides both synchronous and asynchronous event emission capabilities.
60
+ Thread-safe for single-threaded use cases.
61
+ """
62
+
63
+ def __init__(self, options: Optional[EventEmitterOptions] = None) -> None:
64
+ self._index = -1
65
+ self._subscriptions: Dict[str, List[Subscription]] = {}
66
+
67
+ # Use provided logger or create default console logger
68
+ logger = options.get("logger") if options else None
69
+ if logger:
70
+ self._logger = logger.getChild("microsoft.teams.common.events.EventEmitter")
71
+ else:
72
+ self._logger = ConsoleLogger().create_logger("microsoft.teams.common.events.EventEmitter")
73
+
74
+ def on(self, event: EventTypeT, handler: EventHandler) -> int:
75
+ """
76
+ Register an event handler.
77
+
78
+ Args:
79
+ event: Event name to listen for
80
+ handler: Function to call when event is emitted
81
+
82
+ Returns:
83
+ Subscription ID for removing the handler later
84
+ """
85
+ subscription_id = self._get_next_id()
86
+
87
+ if event not in self._subscriptions:
88
+ self._subscriptions[event] = []
89
+
90
+ self._subscriptions[event].append({"id": subscription_id, "handler": handler})
91
+
92
+ self._logger.debug("Registered handler for event '%s' with id %d", event, subscription_id)
93
+ return subscription_id
94
+
95
+ def once(self, event: EventTypeT, handler: EventHandler) -> int:
96
+ """
97
+ Register a one-time event handler that will be removed after first execution.
98
+
99
+ Args:
100
+ event: Event name to listen for
101
+ handler: Function to call when event is emitted
102
+
103
+ Returns:
104
+ Subscription ID for removing the handler later
105
+ """
106
+
107
+ def once_wrapper(value: Any) -> None:
108
+ self.off(subscription_id)
109
+ handler(value)
110
+
111
+ subscription_id = self._get_next_id()
112
+
113
+ if event not in self._subscriptions:
114
+ self._subscriptions[event] = []
115
+
116
+ self._subscriptions[event].append({"id": subscription_id, "handler": once_wrapper})
117
+
118
+ self._logger.debug("Registered one-time handler for event '%s' with id %d", event, subscription_id)
119
+ return subscription_id
120
+
121
+ def off(self, subscription_id: int) -> None:
122
+ """
123
+ Remove an event handler by subscription ID.
124
+
125
+ Args:
126
+ subscription_id: ID returned from on() or once()
127
+ """
128
+ for event_name, subscriptions in list(self._subscriptions.items()):
129
+ for i, subscription in enumerate(subscriptions):
130
+ if subscription["id"] == subscription_id:
131
+ subscriptions.pop(i)
132
+ self._logger.debug("Removed handler with id %d from event '%s'", subscription_id, event_name)
133
+ if not subscriptions:
134
+ del self._subscriptions[event_name]
135
+ return
136
+
137
+ def emit(self, event: EventTypeT, value: Any = None) -> None:
138
+ """
139
+ Emit an event synchronously to all registered handlers.
140
+
141
+ Async handlers are run using asyncio.run() in a separate event loop.
142
+
143
+ Args:
144
+ event: Event name to emit
145
+ value: Data to pass to event handlers
146
+ """
147
+ if event not in self._subscriptions:
148
+ return
149
+
150
+ handler_count = len(self._subscriptions[event])
151
+ self._logger.debug("Emitting event '%s' to %d handler(s)", event, handler_count)
152
+
153
+ awaitables: list[Awaitable[None]] = []
154
+
155
+ for subscription in self._subscriptions[event][:]: # Copy to avoid modification during iteration
156
+ try:
157
+ handler = subscription["handler"]
158
+
159
+ if asyncio.iscoroutinefunction(handler):
160
+ awaitables.append(handler(value))
161
+ else:
162
+ handler(value)
163
+ except Exception as e:
164
+ # Continue executing other handlers even if one fails
165
+ self._logger.error("Handler failed for event '%s': %s", event, e)
166
+
167
+ # Handle awaitables if any exist
168
+ if awaitables:
169
+ self._logger.debug("Running %d async handler(s) for event '%s'", len(awaitables), event)
170
+
171
+ async def run_async_handlers() -> None:
172
+ results = await asyncio.gather(*awaitables, return_exceptions=True)
173
+ for i, result in enumerate(results):
174
+ if isinstance(result, Exception):
175
+ self._logger.error("Async handler %d failed for event '%s': %s", i, event, result)
176
+
177
+ try:
178
+ # Try to get the current event loop
179
+ loop = asyncio.get_running_loop()
180
+ # If loop is running, schedule the async handlers as tasks
181
+ loop.create_task(run_async_handlers())
182
+ # Note: tasks run in background, emit() doesn't wait for completion
183
+ except RuntimeError:
184
+ # No event loop running, create one
185
+ asyncio.run(run_async_handlers())
186
+
187
+ def listener_count(self, event: str) -> int:
188
+ """
189
+ Get the number of listeners for an event.
190
+
191
+ Args:
192
+ event: Event name
193
+
194
+ Returns:
195
+ Number of registered listeners
196
+ """
197
+ return len(self._subscriptions.get(event, []))
198
+
199
+ def event_names(self) -> List[str]:
200
+ """
201
+ Get list of event names that have listeners.
202
+
203
+ Returns:
204
+ List of event names
205
+ """
206
+ return list(self._subscriptions.keys())
207
+
208
+ def remove_all_listeners(self, event: Optional[str] = None) -> None:
209
+ """
210
+ Remove all listeners for a specific event or all events.
211
+
212
+ Args:
213
+ event: Event name to clear. If None, clears all events.
214
+ """
215
+ if event is None:
216
+ total_handlers = sum(len(handlers) for handlers in self._subscriptions.values())
217
+ self._subscriptions.clear()
218
+ self._logger.debug("Removed all %d handler(s) from all events", total_handlers)
219
+ elif event in self._subscriptions:
220
+ handler_count = len(self._subscriptions[event])
221
+ del self._subscriptions[event]
222
+ self._logger.debug("Removed all %d handler(s) from event '%s'", handler_count, event)
223
+
224
+ def _get_next_id(self) -> int:
225
+ """Get next unique subscription ID."""
226
+ self._index += 1
227
+ return self._index
@@ -0,0 +1,19 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from .client import Client, ClientOptions
7
+ from .client_token import Token, TokenFactory, resolve_token
8
+ from .interceptor import Interceptor, InterceptorRequestContext, InterceptorResponseContext
9
+
10
+ __all__ = [
11
+ "Client",
12
+ "ClientOptions",
13
+ "Interceptor",
14
+ "InterceptorRequestContext",
15
+ "InterceptorResponseContext",
16
+ "Token",
17
+ "TokenFactory",
18
+ "resolve_token",
19
+ ]
@@ -0,0 +1,438 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ import inspect
7
+ import json
8
+ import logging
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, Awaitable, Callable, Dict, List, Optional
11
+
12
+ import httpx
13
+ from httpx._models import Request, Response
14
+ from httpx._types import QueryParamTypes, RequestContent, RequestData, RequestFiles
15
+
16
+ from ..logging import ConsoleLogger
17
+ from .client_token import Token, resolve_token
18
+ from .interceptor import Interceptor, InterceptorRequestContext, InterceptorResponseContext
19
+
20
+ console_logger = ConsoleLogger()
21
+
22
+
23
+ def _wrap_response_json(response: httpx.Response, logger: logging.Logger) -> None:
24
+ """
25
+ Wrap the response.json method to handle JSONDecodeError gracefully.
26
+
27
+ Args:
28
+ response: The httpx.Response object to wrap.
29
+ logger: Logger instance for warning messages.
30
+ """
31
+ original_json = response.json
32
+
33
+ def safe_json(**kwargs: Any) -> Any:
34
+ try:
35
+ return original_json(**kwargs)
36
+ except json.JSONDecodeError as e:
37
+ if e.pos == 0:
38
+ logger.warning(f"Failed to decode JSON response from {response.url}. Returning empty dict.")
39
+ return {}
40
+ else:
41
+ raise
42
+
43
+ response.json = safe_json
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class ClientOptions:
48
+ """
49
+ Configuration options for the HTTP Client.
50
+
51
+ Attributes:
52
+ base_url: The base URL for all requests.
53
+ headers: Default headers to include with every request.
54
+ timeout: Default request timeout in seconds.
55
+ logger: Logger instance for request/response/error logging.
56
+ token: Default authorization token (string, string-like, or callable).
57
+ interceptors: List of interceptors for request/response middleware.
58
+ """
59
+
60
+ base_url: Optional[str] = None
61
+ headers: Dict[str, str] = field(default_factory=dict[str, str])
62
+ timeout: Optional[float] = None
63
+ logger: Optional[logging.Logger] = None
64
+ token: Optional[Token] = None
65
+ interceptors: Optional[List[Interceptor]] = field(default_factory=list[Interceptor])
66
+
67
+
68
+ class Client:
69
+ """
70
+ HTTP Client abstraction for making requests with configurable options.
71
+
72
+ Args:
73
+ options: ClientOptions dataclass with configuration for the client.
74
+ """
75
+
76
+ def __init__(self, options: Optional[ClientOptions] = None):
77
+ """
78
+ Initialize the HTTP Client.
79
+
80
+ Args:
81
+ options: Optional ClientOptions dataclass with configuration for the client.
82
+ """
83
+ if options is None:
84
+ options = ClientOptions()
85
+
86
+ self._options = options
87
+ self._logger = options.logger or console_logger.create_logger("http-client")
88
+ self._token = options.token
89
+
90
+ # Configure httpx logging to match our logger's level
91
+ httpx_logger = logging.getLogger("httpx")
92
+ httpx_logger.setLevel(self._logger.level)
93
+
94
+ # Maintain interceptors as a separate instance attribute (do not mutate options)
95
+ self._interceptors = list(options.interceptors or [])
96
+
97
+ self.http = httpx.AsyncClient(
98
+ base_url=httpx.URL(options.base_url) if options.base_url else "",
99
+ headers=options.headers,
100
+ timeout=options.timeout,
101
+ )
102
+ self._update_event_hooks()
103
+
104
+ async def _prepare_headers(self, headers: Optional[Dict[str, str]], token: Optional[Token]) -> Dict[str, str]:
105
+ """
106
+ Merge default and per-request headers, resolve token, and inject Authorization header if needed.
107
+
108
+ Args:
109
+ headers: Optional per-request headers.
110
+ token: Optional per-request token.
111
+
112
+ Returns:
113
+ Final headers dict for the request.
114
+ """
115
+ req_headers = {**self._options.headers, **(headers or {})}
116
+ resolved_token = await self._resolve_token(token)
117
+ if resolved_token:
118
+ req_headers["Authorization"] = f"Bearer {resolved_token}"
119
+ return req_headers
120
+
121
+ async def get(
122
+ self,
123
+ url: str,
124
+ *,
125
+ headers: Optional[Dict[str, str]] = None,
126
+ token: Optional[Token] = None,
127
+ params: Optional[QueryParamTypes] = None,
128
+ **kwargs: Any,
129
+ ) -> httpx.Response:
130
+ """
131
+ Send a GET request.
132
+
133
+ Args:
134
+ url: The URL path or full URL.
135
+ headers: Optional per-request headers.
136
+ params: Optional query parameters.
137
+ token: Optional per-request token (overrides default).
138
+ **kwargs: Additional httpx.AsyncClient.get arguments.
139
+
140
+ Returns:
141
+ httpx.Response
142
+ """
143
+ req_headers = await self._prepare_headers(headers, token)
144
+ response = await self.http.get(url, headers=req_headers, params=params, **kwargs)
145
+ response.raise_for_status()
146
+ _wrap_response_json(response, self._logger)
147
+ return response
148
+
149
+ async def post(
150
+ self,
151
+ url: str,
152
+ *,
153
+ content: Optional[RequestContent] = None,
154
+ data: Optional[RequestData] = None,
155
+ files: Optional[RequestFiles] = None,
156
+ json: Optional[Any] = None,
157
+ params: Optional[QueryParamTypes] = None,
158
+ headers: Optional[Dict[str, str]] = None,
159
+ token: Optional[Token] = None,
160
+ **kwargs: Any,
161
+ ) -> httpx.Response:
162
+ """
163
+ Send a POST request.
164
+
165
+ Args:
166
+ url: The URL path or full URL.
167
+ data: The request body.
168
+ headers: Optional per-request headers.
169
+ params: Optional query parameters.
170
+ content: The request body.
171
+ files: The request files.
172
+ json: The request JSON body.
173
+ token: Optional per-request token (overrides default).
174
+ **kwargs: Additional httpx.AsyncClient.post arguments.
175
+
176
+ Returns:
177
+ httpx.Response
178
+ """
179
+ req_headers = await self._prepare_headers(headers, token)
180
+ response = await self.http.post(
181
+ url,
182
+ data=data,
183
+ files=files,
184
+ json=json,
185
+ content=content,
186
+ params=params,
187
+ headers=req_headers,
188
+ **kwargs,
189
+ )
190
+ response.raise_for_status()
191
+ _wrap_response_json(response, self._logger)
192
+ return response
193
+
194
+ async def put(
195
+ self,
196
+ url: str,
197
+ *,
198
+ content: Optional[RequestContent] = None,
199
+ data: Optional[RequestData] = None,
200
+ files: Optional[RequestFiles] = None,
201
+ json: Optional[Any] = None,
202
+ params: Optional[QueryParamTypes] = None,
203
+ headers: Optional[Dict[str, str]] = None,
204
+ token: Optional[Token] = None,
205
+ **kwargs: Any,
206
+ ) -> httpx.Response:
207
+ """
208
+ Send a PUT request.
209
+
210
+ Args:
211
+ url: The URL path or full URL.
212
+ data: The request body.
213
+ headers: Optional per-request headers.
214
+ params: Optional query parameters.
215
+ content: The request body.
216
+ files: The request files.
217
+ json: The request JSON body.
218
+ token: Optional per-request token (overrides default).
219
+ **kwargs: Additional httpx.AsyncClient.put arguments.
220
+
221
+ Returns:
222
+ httpx.Response
223
+ """
224
+ req_headers = await self._prepare_headers(headers, token)
225
+ response = await self.http.put(
226
+ url,
227
+ data=data,
228
+ files=files,
229
+ json=json,
230
+ content=content,
231
+ params=params,
232
+ headers=req_headers,
233
+ **kwargs,
234
+ )
235
+ response.raise_for_status()
236
+ _wrap_response_json(response, self._logger)
237
+ return response
238
+
239
+ async def patch(
240
+ self,
241
+ url: str,
242
+ *,
243
+ content: Optional[RequestContent] = None,
244
+ data: Optional[RequestData] = None,
245
+ files: Optional[RequestFiles] = None,
246
+ json: Optional[Any] = None,
247
+ params: Optional[QueryParamTypes] = None,
248
+ headers: Optional[Dict[str, str]] = None,
249
+ token: Optional[Token] = None,
250
+ **kwargs: Any,
251
+ ) -> httpx.Response:
252
+ """
253
+ Send a PATCH request.
254
+
255
+ Args:
256
+ url: The URL path or full URL.
257
+ data: The request body.
258
+ headers: Optional per-request headers.
259
+ params: Optional query parameters.
260
+ content: The request body.
261
+ files: The request files.
262
+ json: The request JSON body.
263
+ token: Optional per-request token (overrides default).
264
+ **kwargs: Additional httpx.AsyncClient.patch arguments.
265
+
266
+ Returns:
267
+ httpx.Response
268
+ """
269
+ req_headers = await self._prepare_headers(headers, token)
270
+ response = await self.http.patch(
271
+ url,
272
+ data=data,
273
+ files=files,
274
+ json=json,
275
+ content=content,
276
+ params=params,
277
+ headers=req_headers,
278
+ **kwargs,
279
+ )
280
+ response.raise_for_status()
281
+ _wrap_response_json(response, self._logger)
282
+ return response
283
+
284
+ async def delete(
285
+ self,
286
+ url: str,
287
+ *,
288
+ headers: Optional[Dict[str, str]] = None,
289
+ token: Optional[Token] = None,
290
+ params: Optional[QueryParamTypes] = None,
291
+ **kwargs: Any,
292
+ ) -> httpx.Response:
293
+ """
294
+ Send a DELETE request.
295
+
296
+ Args:
297
+ url: The URL path or full URL.
298
+ headers: Optional per-request headers.
299
+ params: Optional query parameters.
300
+ token: Optional per-request token (overrides default).
301
+ **kwargs: Additional httpx.AsyncClient.delete arguments.
302
+
303
+ Returns:
304
+ httpx.Response
305
+ """
306
+ req_headers = await self._prepare_headers(headers, token)
307
+ response = await self.http.delete(url, headers=req_headers, params=params, **kwargs)
308
+ response.raise_for_status()
309
+ _wrap_response_json(response, self._logger)
310
+ return response
311
+
312
+ async def request(
313
+ self,
314
+ method: str,
315
+ url: str,
316
+ *,
317
+ params: Optional[QueryParamTypes] = None,
318
+ headers: Optional[Dict[str, str]] = None,
319
+ token: Optional[Token] = None,
320
+ content: Optional[RequestContent] = None,
321
+ data: Optional[RequestData] = None,
322
+ files: Optional[RequestFiles] = None,
323
+ json: Optional[Any] = None,
324
+ **kwargs: Any,
325
+ ) -> httpx.Response:
326
+ """
327
+ Send a custom HTTP request.
328
+
329
+ Args:
330
+ method: HTTP method (GET, POST, etc).
331
+ url: The URL path or full URL.
332
+ headers: Optional per-request headers.
333
+ params: Optional query parameters.
334
+ content: The request body.
335
+ data: The request body.
336
+ files: The request files.
337
+ json: The request JSON body.
338
+ token: Optional per-request token (overrides default).
339
+ **kwargs: Additional httpx.AsyncClient.request arguments.
340
+
341
+ Returns:
342
+ httpx.Response
343
+ """
344
+ req_headers = await self._prepare_headers(headers, token)
345
+ response = await self.http.request(
346
+ method,
347
+ url,
348
+ headers=req_headers,
349
+ params=params,
350
+ content=content,
351
+ data=data,
352
+ files=files,
353
+ json=json,
354
+ **kwargs,
355
+ )
356
+ response.raise_for_status()
357
+ _wrap_response_json(response, self._logger)
358
+ return response
359
+
360
+ async def _resolve_token(self, token: Optional[Token]) -> Optional[str]:
361
+ """
362
+ Resolve the token to a string, using per-request or default token.
363
+
364
+ Args:
365
+ token: Per-request token or None.
366
+
367
+ Returns:
368
+ The resolved token string or None.
369
+ """
370
+ use_token = token if token is not None else self._token
371
+ if use_token is None:
372
+ return None
373
+ return await resolve_token(use_token)
374
+
375
+ def use_interceptor(self, interceptor: Interceptor) -> None:
376
+ """
377
+ Register an interceptor for request/response middleware.
378
+
379
+ Args:
380
+ interceptor: An object with optional request/response methods.
381
+ """
382
+ self._interceptors.append(interceptor)
383
+ self._update_event_hooks()
384
+
385
+ def _update_event_hooks(self) -> None:
386
+ """
387
+ Internal: Update the httpx.AsyncClient event_hooks to match current interceptors.
388
+ """
389
+ event_hooks_dict: Dict[str, List[Callable[[Any], Any]]] = {}
390
+ for hook in self._interceptors:
391
+ if hasattr(hook, "request"):
392
+
393
+ def _make_request_wrapper(h: Interceptor) -> Callable[[Request], Awaitable[None]]:
394
+ async def wrapper(request: Request) -> None:
395
+ ctx = InterceptorRequestContext(request, self._logger)
396
+ result = h.request(ctx)
397
+ if inspect.isawaitable(result):
398
+ await result
399
+
400
+ return wrapper
401
+
402
+ event_hooks_dict.setdefault("request", []).append(_make_request_wrapper(hook))
403
+ if hasattr(hook, "response"):
404
+
405
+ def _make_response_wrapper(h: Interceptor) -> Callable[[Response], Awaitable[None]]:
406
+ async def wrapper(response: Response) -> None:
407
+ ctx = InterceptorResponseContext(response, self._logger)
408
+ result = h.response(ctx)
409
+ if inspect.isawaitable(result):
410
+ await result
411
+
412
+ return wrapper
413
+
414
+ event_hooks_dict.setdefault("response", []).append(_make_response_wrapper(hook))
415
+ self.http.event_hooks = event_hooks_dict
416
+
417
+ def clone(self, overrides: Optional[ClientOptions] = None) -> "Client":
418
+ """
419
+ Create a new Client instance with merged configuration.
420
+
421
+ Args:
422
+ overrides: Optional ClientOptions object to override fields.
423
+
424
+ Returns:
425
+ A new Client instance with merged options and a cloned interceptor list.
426
+ """
427
+ overrides = overrides or ClientOptions()
428
+ merged_options = ClientOptions(
429
+ base_url=overrides.base_url if overrides.base_url is not None else self._options.base_url,
430
+ headers={**self._options.headers, **(overrides.headers or {})},
431
+ timeout=overrides.timeout if overrides.timeout is not None else self._options.timeout,
432
+ logger=overrides.logger if overrides.logger is not None else self._options.logger,
433
+ token=overrides.token if overrides.token is not None else self._options.token,
434
+ interceptors=list(overrides.interceptors)
435
+ if overrides.interceptors is not None
436
+ else list(self._interceptors),
437
+ )
438
+ return Client(merged_options)
@@ -0,0 +1,43 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ import inspect
7
+ from typing import Awaitable, Callable, Optional, Protocol, Union, runtime_checkable
8
+
9
+
10
+ # String-like protocol: any object with __str__
11
+ @runtime_checkable
12
+ class StringLike(Protocol):
13
+ def __str__(self) -> str: ...
14
+
15
+
16
+ TokenFactory = Callable[
17
+ [],
18
+ Union[
19
+ str,
20
+ StringLike,
21
+ None,
22
+ Awaitable[Union[str, StringLike, None]],
23
+ ],
24
+ ]
25
+
26
+ Token = Union[str, StringLike, TokenFactory, None]
27
+
28
+
29
+ async def resolve_token(token: Token) -> Optional[str]:
30
+ """
31
+ Resolves a token value to a string, handling callables and awaitables.
32
+ Always used as an async function for uniform async usage.
33
+ """
34
+ value = token
35
+ if callable(value):
36
+ called_value = value()
37
+ if inspect.isawaitable(called_value):
38
+ resolved = await called_value
39
+ return str(resolved) if resolved is not None else None
40
+ return str(called_value) if called_value is not None else None
41
+ if value is None:
42
+ return None
43
+ return str(value)
@@ -0,0 +1,42 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ import logging
7
+ from typing import Any, Awaitable, Callable, TypeVar, Union
8
+
9
+ from httpx import Request, Response
10
+
11
+ T = TypeVar("T")
12
+ D = TypeVar("D")
13
+
14
+
15
+ class InterceptorRequestContext:
16
+ def __init__(self, request: Request, log: logging.Logger):
17
+ self.request = request
18
+ self.log = log
19
+
20
+
21
+ class InterceptorResponseContext:
22
+ def __init__(self, response: Response, log: logging.Logger):
23
+ self.response = response
24
+ self.log = log
25
+
26
+
27
+ RequestInterceptor = Callable[[InterceptorRequestContext], Union[Any, Awaitable[Any]]]
28
+ ResponseInterceptor = Callable[[InterceptorResponseContext], Union[Any, Awaitable[Any]]]
29
+
30
+
31
+ class Interceptor:
32
+ """
33
+ Protocol for HTTP interceptors.
34
+
35
+ Optionally implement any of:
36
+ - request(ctx): mutate or observe outgoing request (ctx: InterceptorRequestContext)
37
+ - response(ctx): mutate or observe incoming response (ctx: InterceptorResponseContext)
38
+ """
39
+
40
+ def request(self, ctx: InterceptorRequestContext) -> Union[None, Awaitable[None]]: ...
41
+
42
+ def response(self, ctx: InterceptorResponseContext) -> Union[None, Awaitable[None]]: ...
@@ -0,0 +1,11 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from .ansi import ANSI
7
+ from .console import ConsoleLogger
8
+ from .filter import ConsoleFilter
9
+ from .formatter import ConsoleFormatter
10
+
11
+ __all__ = ["ANSI", "ConsoleLogger", "ConsoleFormatter", "ConsoleFilter"]
@@ -0,0 +1,41 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from enum import Enum
7
+
8
+
9
+ class ANSI(str, Enum):
10
+ RESET = "\033[0m"
11
+
12
+ BOLD = "\033[1m"
13
+ BOLD_RESET = "\033[22m"
14
+ ITALIC = "\033[3m"
15
+ ITALIC_RESET = "\033[23m"
16
+ UNDERLINE = "\033[4m"
17
+ UNDERLINE_RESET = "\033[24m"
18
+ STRIKE = "\033[9m"
19
+ STRIKE_RESET = "\033[29m"
20
+
21
+ FOREGROUND_RESET = "\033[0m"
22
+ BACKGROUND_RESET = "\033[0m"
23
+ FOREGROUND_BLACK = "\033[30m"
24
+ BACKGROUND_BLACK = "\033[40m"
25
+ FOREGROUND_RED = "\033[31m"
26
+ BACKGROUND_RED = "\033[41m"
27
+ FOREGROUND_GREEN = "\033[32m"
28
+ BACKGROUND_GREEN = "\033[42m"
29
+ FOREGROUND_YELLOW = "\033[33m"
30
+ BACKGROUND_YELLOW = "\033[43m"
31
+ FOREGROUND_BLUE = "\033[34m"
32
+ BACKGROUND_BLUE = "\033[44m"
33
+ FOREGROUND_MAGENTA = "\033[35m"
34
+ BACKGROUND_MAGENTA = "\033[45m"
35
+ FOREGROUND_CYAN = "\033[36m"
36
+ BACKGROUND_CYAN = "\033[46m"
37
+ FOREGROUND_WHITE = "\033[37m"
38
+ BACKGROUND_WHITE = "\033[47m"
39
+ FOREGROUND_GRAY = "\033[90m"
40
+ FOREGROUND_DEFAULT = "\033[39m"
41
+ BACKGROUND_DEFAULT = "\033[49m"
@@ -0,0 +1,56 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ from typing import Optional, TypedDict
9
+
10
+ from .filter import ConsoleFilter
11
+ from .formatter import ConsoleFormatter
12
+
13
+
14
+ class ConsoleLoggerOptions(TypedDict, total=False):
15
+ """
16
+ ConsoleLoggerOptions is a dictionary that contains the options for the ConsoleLogger.
17
+
18
+ :param level: The level of the logger (error, warn, info, debug)
19
+ :param pattern: The pattern of the logger (e.g. "my_module.*")
20
+ """
21
+
22
+ level: str
23
+ pattern: str
24
+
25
+
26
+ class ConsoleLogger:
27
+ """
28
+ ConsoleLogger is a class that creates a logger for the console.
29
+ """
30
+
31
+ _levels = {"error": logging.ERROR, "warn": logging.WARNING, "info": logging.INFO, "debug": logging.DEBUG}
32
+
33
+ def create_logger(self, name: str, options: Optional[ConsoleLoggerOptions] = None) -> logging.Logger:
34
+ """
35
+ Create a logger for the console.
36
+
37
+ :param name: The name of the logger
38
+ :param options: The options for the logger
39
+ """
40
+
41
+ logger = logging.getLogger(name)
42
+ logger.handlers = []
43
+
44
+ handler = logging.StreamHandler()
45
+ handler.setFormatter(ConsoleFormatter())
46
+ logger.addHandler(handler)
47
+
48
+ options_level = options.get("level", "info") if options else "info"
49
+ level = (os.environ.get("LOG_LEVEL") or options_level).lower()
50
+ logger.setLevel(self._levels.get(level, logging.INFO))
51
+
52
+ options_pattern = options.get("pattern", "*") if options else "*"
53
+ pattern = os.environ.get("LOG") or options_pattern
54
+ logger.addFilter(ConsoleFilter(pattern))
55
+
56
+ return logger
@@ -0,0 +1,35 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ import logging
7
+ import re
8
+
9
+
10
+ class ConsoleFilter(logging.Filter):
11
+ """
12
+ A logging filter that matches log records against a pattern.
13
+
14
+ Attributes:
15
+ pattern (str): The pattern to match against log record names.
16
+ """
17
+
18
+ def __init__(self, pattern: str = "*"):
19
+ super().__init__()
20
+ self.pattern = self._parse_magic_expr(pattern)
21
+
22
+ def filter(self, record: logging.LogRecord) -> bool:
23
+ """
24
+ Filter log records based on a pattern.
25
+ Args:
26
+ record (logging.LogRecord): The log record to filter.
27
+ Returns:
28
+ bool: True if the record matches the pattern, False otherwise.
29
+ """
30
+ return bool(self.pattern.match(record.name))
31
+
32
+ @staticmethod
33
+ def _parse_magic_expr(pattern: str) -> re.Pattern[str]:
34
+ pattern = pattern.replace("*", ".*")
35
+ return re.compile(f"^{pattern}$")
@@ -0,0 +1,43 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+
9
+ from .ansi import ANSI
10
+
11
+
12
+ class ConsoleFormatter(logging.Formatter):
13
+ """
14
+ A custom logging formatter that formats log messages with colors and prefixes.
15
+ """
16
+
17
+ _colors = {
18
+ "ERROR": ANSI.FOREGROUND_RED,
19
+ "WARNING": ANSI.FOREGROUND_YELLOW,
20
+ "INFO": ANSI.FOREGROUND_CYAN,
21
+ "DEBUG": ANSI.FOREGROUND_MAGENTA,
22
+ }
23
+
24
+ def format(self, record: logging.LogRecord) -> str:
25
+ """
26
+ Format the log record with colors and prefixes.
27
+ Args:
28
+ record (logging.LogRecord): The log record to format.
29
+ Returns:
30
+ str: The formatted log message.
31
+ """
32
+ if isinstance(record.msg, (dict, list)):
33
+ record.msg = json.dumps(record.msg, indent=2) # pyright: ignore[reportUnknownMemberType]
34
+
35
+ level_name = record.levelname.upper()
36
+ color = self._colors.get(level_name, ANSI.FOREGROUND_CYAN)
37
+ prefix = f"{color.value}{ANSI.BOLD.value}[{level_name}]"
38
+ name = f"{record.name}{ANSI.FOREGROUND_RESET.value}{ANSI.BOLD_RESET.value}"
39
+
40
+ message = record.getMessage()
41
+ lines = message.split("\n")
42
+ formatted_lines = [f"{prefix} {name} {line}" for line in lines]
43
+ return "\n".join(formatted_lines)
@@ -0,0 +1,10 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from .list_local_storage import ListLocalStorage
7
+ from .local_storage import LocalStorage, LocalStorageOptions
8
+ from .storage import ListStorage, Storage
9
+
10
+ __all__ = ["Storage", "ListStorage", "LocalStorage", "ListLocalStorage", "LocalStorageOptions"]
@@ -0,0 +1,67 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from typing import Callable, List, Optional, TypeVar
7
+
8
+ from .storage import ListStorage
9
+
10
+ V = TypeVar("V")
11
+
12
+
13
+ class ListLocalStorage(ListStorage[V]):
14
+ """A local storage implementation for a list of items."""
15
+
16
+ def __init__(self, items: Optional[List[V]] = None):
17
+ self._items = items or []
18
+
19
+ def get(self, key: int) -> Optional[V]:
20
+ if key < 0 or key >= len(self._items):
21
+ return None
22
+ return self._items[key]
23
+
24
+ async def async_get(self, key: int) -> Optional[V]:
25
+ return self.get(key)
26
+
27
+ def set(self, key: int, value: V) -> None:
28
+ self._items[key] = value
29
+
30
+ async def async_set(self, key: int, value: V) -> None:
31
+ return self.set(key, value)
32
+
33
+ def delete(self, key: int) -> None:
34
+ del self._items[key]
35
+
36
+ async def async_delete(self, key: int) -> None:
37
+ return self.delete(key)
38
+
39
+ def append(self, value: V) -> None:
40
+ return self._items.append(value)
41
+
42
+ async def async_append(self, value: V) -> None:
43
+ return self.append(value)
44
+
45
+ def pop(self) -> Optional[V]:
46
+ return self._items.pop()
47
+
48
+ async def async_pop(self) -> Optional[V]:
49
+ return self.pop()
50
+
51
+ def items(self) -> List[V]:
52
+ return self._items
53
+
54
+ async def async_items(self) -> List[V]:
55
+ return self.items()
56
+
57
+ def length(self) -> int:
58
+ return len(self._items)
59
+
60
+ async def async_length(self) -> int:
61
+ return self.length()
62
+
63
+ def filter(self, predicate: Callable[[V, int], bool]) -> List[V]:
64
+ return [item for i, item in enumerate(self._items) if predicate(item, i)]
65
+
66
+ async def async_filter(self, predicate: Callable[[V, int], bool]) -> List[V]:
67
+ return self.filter(predicate)
@@ -0,0 +1,77 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from collections import OrderedDict
7
+ from dataclasses import dataclass
8
+ from typing import Dict, List, Optional, TypeVar
9
+
10
+ from .storage import Storage
11
+
12
+ V = TypeVar("V")
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class LocalStorageOptions:
17
+ max: Optional[int] = None
18
+ """Maximum number of items in the storage"""
19
+
20
+
21
+ class LocalStorage(Storage[str, V]):
22
+ """
23
+ A key-value storage with optional size limit and LRU behavior.
24
+ """
25
+
26
+ @property
27
+ def store(self) -> OrderedDict[str, V]:
28
+ return self._store
29
+
30
+ @property
31
+ def options(self) -> LocalStorageOptions:
32
+ return self._options
33
+
34
+ @property
35
+ def keys(self) -> List[str]:
36
+ return list(self._store.keys())
37
+
38
+ @property
39
+ def size(self) -> int:
40
+ return len(self._store)
41
+
42
+ def __init__(
43
+ self,
44
+ data: Optional[Dict[str, V]] = None,
45
+ options: Optional[LocalStorageOptions] = None,
46
+ ):
47
+ self._store = OrderedDict(data or {})
48
+ self._options = options or LocalStorageOptions()
49
+
50
+ def get(self, key: str) -> Optional[V]:
51
+ if key not in self._store:
52
+ return None
53
+
54
+ value = self._store.pop(key)
55
+ self._store[key] = value
56
+ return value
57
+
58
+ async def async_get(self, key: str) -> Optional[V]:
59
+ return self.get(key)
60
+
61
+ def set(self, key: str, value: V) -> None:
62
+ if key in self._store:
63
+ del self._store[key]
64
+ elif self._options.max and len(self._store) >= self._options.max:
65
+ self._store.popitem(last=False)
66
+
67
+ self._store[key] = value
68
+
69
+ async def async_set(self, key: str, value: V) -> None:
70
+ return self.set(key, value)
71
+
72
+ def delete(self, key: str) -> None:
73
+ if key in self._store:
74
+ del self._store[key]
75
+
76
+ async def async_delete(self, key: str) -> None:
77
+ return self.delete(key)
@@ -0,0 +1,98 @@
1
+ """
2
+ Copyright (c) Microsoft Corporation. All rights reserved.
3
+ Licensed under the MIT License.
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Callable, Generic, List, Optional, TypeVar
8
+
9
+ K = TypeVar("K")
10
+ V = TypeVar("V")
11
+
12
+
13
+ class Storage(Generic[K, V], ABC):
14
+ """A storage container that can get/set/delete items by a unique key."""
15
+
16
+ @abstractmethod
17
+ def get(self, key: K) -> Optional[V]:
18
+ """Synchronously get a value by key."""
19
+ pass
20
+
21
+ @abstractmethod
22
+ async def async_get(self, key: K) -> Optional[V]:
23
+ """Asynchronously get a value by key."""
24
+ pass
25
+
26
+ @abstractmethod
27
+ def set(self, key: K, value: V) -> None:
28
+ """Synchronously set a value by key."""
29
+ pass
30
+
31
+ @abstractmethod
32
+ async def async_set(self, key: K, value: V) -> None:
33
+ """Asynchronously set a value by key."""
34
+ pass
35
+
36
+ @abstractmethod
37
+ def delete(self, key: K) -> None:
38
+ """Synchronously delete a value by key."""
39
+ pass
40
+
41
+ @abstractmethod
42
+ async def async_delete(self, key: K) -> None:
43
+ """Asynchronously delete a value by key."""
44
+ pass
45
+
46
+
47
+ class ListStorage(Storage[int, V], ABC):
48
+ """A list storage container that can store/query iterable data."""
49
+
50
+ @abstractmethod
51
+ def append(self, value: V) -> None:
52
+ """Synchronously append a value to the list."""
53
+ pass
54
+
55
+ @abstractmethod
56
+ async def async_append(self, value: V) -> None:
57
+ """Asynchronously append a value to the list."""
58
+ pass
59
+
60
+ @abstractmethod
61
+ def pop(self) -> Optional[V]:
62
+ """Synchronously remove and return the last value."""
63
+ pass
64
+
65
+ @abstractmethod
66
+ async def async_pop(self) -> Optional[V]:
67
+ """Asynchronously remove and return the last value."""
68
+ pass
69
+
70
+ @abstractmethod
71
+ def items(self) -> List[V]:
72
+ """Synchronously return all items as a list."""
73
+ pass
74
+
75
+ @abstractmethod
76
+ async def async_items(self) -> List[V]:
77
+ """Asynchronously return all items as a list."""
78
+ pass
79
+
80
+ @abstractmethod
81
+ def length(self) -> int:
82
+ """Return the number of items."""
83
+ pass
84
+
85
+ @abstractmethod
86
+ async def async_length(self) -> int:
87
+ """Return the number of items."""
88
+ pass
89
+
90
+ @abstractmethod
91
+ def filter(self, predicate: Callable[[V, int], bool]) -> List[V]:
92
+ """Synchronously filter items using predicate."""
93
+ pass
94
+
95
+ @abstractmethod
96
+ async def async_filter(self, predicate: Callable[[V, int], bool]) -> List[V]:
97
+ """Asynchronously filter items using predicate."""
98
+ pass