optimizely-opal.opal-tools-sdk 0.1.27.dev0__tar.gz → 0.1.30.dev0__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 (22) hide show
  1. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/PKG-INFO +190 -2
  2. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/README.md +186 -0
  3. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/__init__.py +11 -3
  4. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/_registry.py +3 -1
  5. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/auth.py +8 -9
  6. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/decorators.py +101 -66
  7. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/logging.py +11 -8
  8. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/models.py +26 -19
  9. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/proteus.py +1411 -1343
  10. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/service.py +59 -34
  11. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/PKG-INFO +190 -2
  12. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/requires.txt +3 -1
  13. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/pyproject.toml +30 -2
  14. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/setup.py +2 -2
  15. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/tests/test_integration.py +5 -6
  16. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/tests/test_nested_schema.py +12 -9
  17. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/tests/test_proteus.py +5 -5
  18. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/opal_tools_sdk/ui.py +0 -0
  19. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/SOURCES.txt +0 -0
  20. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/dependency_links.txt +0 -0
  21. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/optimizely_opal.opal_tools_sdk.egg-info/top_level.txt +0 -0
  22. {optimizely_opal_opal_tools_sdk-0.1.27.dev0 → optimizely_opal_opal_tools_sdk-0.1.30.dev0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: optimizely-opal.opal-tools-sdk
3
- Version: 0.1.27.dev0
3
+ Version: 0.1.30.dev0
4
4
  Summary: SDK for creating Opal-compatible tools services
5
5
  Home-page: https://github.com/optimizely/opal-tools-sdk
6
6
  Author: Optimizely
@@ -19,9 +19,11 @@ Requires-Dist: fastapi>=0.100.0
19
19
  Requires-Dist: pydantic>=2.0.0
20
20
  Requires-Dist: httpx>=0.24.1
21
21
  Provides-Extra: dev
22
- Requires-Dist: datamodel-code-generator>=0.25.0; extra == "dev"
22
+ Requires-Dist: datamodel-code-generator>=0.56.0; extra == "dev"
23
23
  Requires-Dist: pytest>=7.0.0; extra == "dev"
24
24
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
25
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
26
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
25
27
  Dynamic: author
26
28
  Dynamic: home-page
27
29
  Dynamic: requires-python
@@ -144,6 +146,78 @@ async def get_email(parameters: EmailParameters, auth_data: Optional[AuthData] =
144
146
  return {"emails": ["Email 1", "Email 2"]}
145
147
  ```
146
148
 
149
+ ## Interactions
150
+
151
+ Interactions are app-only handlers — actions callable from the UI but hidden from the LLM. Use them when a tool surface needs to expose a button, form submit, or other follow-up action that the model should not be able to call. They follow the MCP "app-only tools" pattern (`visibility: ["app"]`) and are not listed in the `/discovery` endpoint.
152
+
153
+ **When to use `@interaction` vs `@tool`:**
154
+
155
+ - Use `@tool` when the LLM should be able to call the function on its own.
156
+ - Use `@interaction` when only the UI (action cards, islands) should call it — the model should never see it.
157
+
158
+ ### Declaring an interaction
159
+
160
+ ```python
161
+ from typing import Optional
162
+ from opal_tools_sdk import ToolsService, interaction, InteractionContext
163
+ from pydantic import BaseModel, Field
164
+ from fastapi import FastAPI
165
+
166
+ app = FastAPI()
167
+ tools_service = ToolsService(app)
168
+
169
+
170
+ class TaskFormInput(BaseModel):
171
+ title: str = Field(description="Task title")
172
+ priority: str = Field(default="medium", description="Task priority")
173
+ assignee: Optional[str] = Field(default=None, description="Assignee email")
174
+
175
+
176
+ @interaction(
177
+ name="submit_task_form",
178
+ description="Handle task form submission",
179
+ )
180
+ async def handle_task_submission(parameters: TaskFormInput, context: InteractionContext):
181
+ # parameters is validated against TaskFormInput
182
+ # context.auth_data carries credentials when auth is configured
183
+ return {"task_id": "task-123", "message": f"Task '{parameters.title}' created"}
184
+ ```
185
+
186
+ The first parameter is introspected from the Pydantic model the same way `@tool` does it (typing the parameter as `dict` skips schema extraction). The second parameter is an `InteractionContext`.
187
+
188
+ ### Handler signature & `InteractionContext`
189
+
190
+ `InteractionContext` carries the auth data resolved by TMS from the parent tool's `auth_requirements`:
191
+
192
+ ```python
193
+ @dataclass
194
+ class InteractionContext:
195
+ auth_data: AuthData | None = None
196
+ ```
197
+
198
+ If the parent tool did not declare auth requirements (or no credentials are available), `context.auth_data` is `None`.
199
+
200
+ ### Generated endpoint
201
+
202
+ The SDK exposes a single shared endpoint for all interactions:
203
+
204
+ ```
205
+ POST /interactions/execute
206
+ Content-Type: application/json
207
+
208
+ {
209
+ "name": "submit_task_form",
210
+ "parameters": {"title": "Ship docs", "priority": "high"},
211
+ "auth": {"provider": "...", "credentials": {...}}
212
+ }
213
+ ```
214
+
215
+ The response is whatever the handler returns, serialized as JSON. Interactions are **not** listed in `/discovery`.
216
+
217
+ ### Name uniqueness
218
+
219
+ Interaction names must be unique across both tools and interactions in the same service, since both can be exported as MCP tools. Registering an interaction with a name that conflicts with an existing tool or interaction raises `ValueError`.
220
+
147
221
  ## Island Components
148
222
 
149
223
  The SDK includes Island components for creating interactive UI responses that allow users to input data and trigger actions.
@@ -235,6 +309,120 @@ The response wrapper for islands:
235
309
  - Use `IslandResponse.create([islands])` to create responses
236
310
  - Supports multiple islands per response
237
311
 
312
+ ## Resources & Proteus UI
313
+
314
+ The SDK supports defining MCP resources that serve dynamic UI specifications using the Proteus framework. This enables tools to render rich, interactive interfaces without hardcoded frontend integrations.
315
+
316
+ For the full Proteus component reference and visual designer, see the [Proteus documentation](https://optimizely-axiom.github.io/optiaxiom/guides/proteus/).
317
+
318
+ ### Defining a Resource with `@resource`
319
+
320
+ Use the `@resource` decorator to register a function as an MCP resource:
321
+
322
+ ```python
323
+ from opal_tools_sdk import UI
324
+ from opal_tools_sdk.decorators import resource
325
+
326
+ @resource(
327
+ uri="ui://my-app/create-form",
328
+ name="create-form",
329
+ description="Form for creating new items",
330
+ )
331
+ async def get_create_form():
332
+ return UI.Document(
333
+ title="Create Item",
334
+ body=[
335
+ UI.Heading(children="New Item"),
336
+ UI.Field(
337
+ label="Item Name",
338
+ children=UI.Input(name="item_name", placeholder="Enter item name"),
339
+ ),
340
+ UI.Field(
341
+ label="Description",
342
+ children=UI.Textarea(name="description", placeholder="Enter description"),
343
+ ),
344
+ ],
345
+ actions=[
346
+ UI.Action(children="Save", appearance="primary"),
347
+ UI.CancelAction(children="Cancel"),
348
+ ],
349
+ )
350
+ ```
351
+
352
+ **Parameters:**
353
+ - `uri` (required): Unique URI for the resource (e.g., `"ui://my-app/create-form"`)
354
+ - `name` (required): Name of the resource
355
+ - `description` (optional): Description of the resource
356
+ - `mime_type` (optional): MIME type of the content. Auto-set to `"application/vnd.opal.proteus+json"` when returning a `UI.Document`
357
+ - `title` (optional): Human-readable title
358
+
359
+ The handler function can return either a `str` (manual JSON serialization) or a `UI.Document` (automatic serialization with MIME type set automatically).
360
+
361
+ ### Linking a Tool to a UI Resource
362
+
363
+ Use the `ui_resource` parameter on `@tool` to associate a tool with a Proteus UI resource. The frontend fetches and renders the resource when the tool is invoked:
364
+
365
+ ```python
366
+ from opal_tools_sdk import tool
367
+ from pydantic import BaseModel, Field
368
+
369
+ class CreateItemParams(BaseModel):
370
+ item_name: str = Field(description="Name of the item")
371
+ description: str = Field(description="Item description")
372
+
373
+ @tool(
374
+ "create_item",
375
+ "Create a new item",
376
+ ui_resource="ui://my-app/create-form",
377
+ )
378
+ async def create_item(parameters: CreateItemParams):
379
+ return {"id": "item-123", "name": parameters.item_name, "status": "created"}
380
+ ```
381
+
382
+ ### Building UI with `UI.Document`
383
+
384
+ Import the `UI` namespace from `opal_tools_sdk` or `opal_tools_sdk.ui`. It provides type-safe builders for all Proteus components:
385
+
386
+ ```python
387
+ from opal_tools_sdk import UI
388
+ ```
389
+
390
+ **Available components:**
391
+
392
+ | Category | Components |
393
+ |----------|-----------|
394
+ | Layout | `UI.Document`, `UI.Group`, `UI.Card`, `UI.CardHeader`, `UI.CardLink`, `UI.Separator` |
395
+ | Typography | `UI.Heading`, `UI.Text`, `UI.Link` |
396
+ | Data Display | `UI.Avatar`, `UI.Badge`, `UI.DataTable`, `UI.Chart`, `UI.IconCalendar`, `UI.Image`, `UI.ImageCarousel`, `UI.Time` |
397
+ | Form Controls | `UI.Field`, `UI.Input`, `UI.Textarea`, `UI.Select`, `UI.SelectTrigger`, `UI.SelectContent`, `UI.Switch`, `UI.Range`, `UI.Question` |
398
+ | Actions | `UI.Action`, `UI.CancelAction` |
399
+ | Dynamic | `UI.Value`, `UI.Map`, `UI.MapIndex`, `UI.Show`, `UI.Concat`, `UI.Zip` |
400
+
401
+ **Data binding** with `UI.Value` resolves paths from the tool response:
402
+
403
+ ```python
404
+ @resource(uri="ui://my-app/results", name="results")
405
+ async def get_results():
406
+ return UI.Document(
407
+ title=UI.Value(path="/title"),
408
+ body=UI.Map(
409
+ path="/items",
410
+ children=UI.Text(children=UI.Value(path="name")),
411
+ ),
412
+ )
413
+ ```
414
+
415
+ **Conditional rendering** with `UI.Show`:
416
+
417
+ ```python
418
+ UI.Show(
419
+ when={"!!": UI.Value(path="/error")},
420
+ children=UI.Text(children="An error occurred", color="fg.error"),
421
+ )
422
+ ```
423
+
424
+ The MIME type constant is available as `UI.MIME_TYPE` (`"application/vnd.opal.proteus+json"`).
425
+
238
426
  ## Type Definitions
239
427
 
240
428
  The SDK provides several TypedDict and dataclass definitions for better type safety:
@@ -116,6 +116,78 @@ async def get_email(parameters: EmailParameters, auth_data: Optional[AuthData] =
116
116
  return {"emails": ["Email 1", "Email 2"]}
117
117
  ```
118
118
 
119
+ ## Interactions
120
+
121
+ Interactions are app-only handlers — actions callable from the UI but hidden from the LLM. Use them when a tool surface needs to expose a button, form submit, or other follow-up action that the model should not be able to call. They follow the MCP "app-only tools" pattern (`visibility: ["app"]`) and are not listed in the `/discovery` endpoint.
122
+
123
+ **When to use `@interaction` vs `@tool`:**
124
+
125
+ - Use `@tool` when the LLM should be able to call the function on its own.
126
+ - Use `@interaction` when only the UI (action cards, islands) should call it — the model should never see it.
127
+
128
+ ### Declaring an interaction
129
+
130
+ ```python
131
+ from typing import Optional
132
+ from opal_tools_sdk import ToolsService, interaction, InteractionContext
133
+ from pydantic import BaseModel, Field
134
+ from fastapi import FastAPI
135
+
136
+ app = FastAPI()
137
+ tools_service = ToolsService(app)
138
+
139
+
140
+ class TaskFormInput(BaseModel):
141
+ title: str = Field(description="Task title")
142
+ priority: str = Field(default="medium", description="Task priority")
143
+ assignee: Optional[str] = Field(default=None, description="Assignee email")
144
+
145
+
146
+ @interaction(
147
+ name="submit_task_form",
148
+ description="Handle task form submission",
149
+ )
150
+ async def handle_task_submission(parameters: TaskFormInput, context: InteractionContext):
151
+ # parameters is validated against TaskFormInput
152
+ # context.auth_data carries credentials when auth is configured
153
+ return {"task_id": "task-123", "message": f"Task '{parameters.title}' created"}
154
+ ```
155
+
156
+ The first parameter is introspected from the Pydantic model the same way `@tool` does it (typing the parameter as `dict` skips schema extraction). The second parameter is an `InteractionContext`.
157
+
158
+ ### Handler signature & `InteractionContext`
159
+
160
+ `InteractionContext` carries the auth data resolved by TMS from the parent tool's `auth_requirements`:
161
+
162
+ ```python
163
+ @dataclass
164
+ class InteractionContext:
165
+ auth_data: AuthData | None = None
166
+ ```
167
+
168
+ If the parent tool did not declare auth requirements (or no credentials are available), `context.auth_data` is `None`.
169
+
170
+ ### Generated endpoint
171
+
172
+ The SDK exposes a single shared endpoint for all interactions:
173
+
174
+ ```
175
+ POST /interactions/execute
176
+ Content-Type: application/json
177
+
178
+ {
179
+ "name": "submit_task_form",
180
+ "parameters": {"title": "Ship docs", "priority": "high"},
181
+ "auth": {"provider": "...", "credentials": {...}}
182
+ }
183
+ ```
184
+
185
+ The response is whatever the handler returns, serialized as JSON. Interactions are **not** listed in `/discovery`.
186
+
187
+ ### Name uniqueness
188
+
189
+ Interaction names must be unique across both tools and interactions in the same service, since both can be exported as MCP tools. Registering an interaction with a name that conflicts with an existing tool or interaction raises `ValueError`.
190
+
119
191
  ## Island Components
120
192
 
121
193
  The SDK includes Island components for creating interactive UI responses that allow users to input data and trigger actions.
@@ -207,6 +279,120 @@ The response wrapper for islands:
207
279
  - Use `IslandResponse.create([islands])` to create responses
208
280
  - Supports multiple islands per response
209
281
 
282
+ ## Resources & Proteus UI
283
+
284
+ The SDK supports defining MCP resources that serve dynamic UI specifications using the Proteus framework. This enables tools to render rich, interactive interfaces without hardcoded frontend integrations.
285
+
286
+ For the full Proteus component reference and visual designer, see the [Proteus documentation](https://optimizely-axiom.github.io/optiaxiom/guides/proteus/).
287
+
288
+ ### Defining a Resource with `@resource`
289
+
290
+ Use the `@resource` decorator to register a function as an MCP resource:
291
+
292
+ ```python
293
+ from opal_tools_sdk import UI
294
+ from opal_tools_sdk.decorators import resource
295
+
296
+ @resource(
297
+ uri="ui://my-app/create-form",
298
+ name="create-form",
299
+ description="Form for creating new items",
300
+ )
301
+ async def get_create_form():
302
+ return UI.Document(
303
+ title="Create Item",
304
+ body=[
305
+ UI.Heading(children="New Item"),
306
+ UI.Field(
307
+ label="Item Name",
308
+ children=UI.Input(name="item_name", placeholder="Enter item name"),
309
+ ),
310
+ UI.Field(
311
+ label="Description",
312
+ children=UI.Textarea(name="description", placeholder="Enter description"),
313
+ ),
314
+ ],
315
+ actions=[
316
+ UI.Action(children="Save", appearance="primary"),
317
+ UI.CancelAction(children="Cancel"),
318
+ ],
319
+ )
320
+ ```
321
+
322
+ **Parameters:**
323
+ - `uri` (required): Unique URI for the resource (e.g., `"ui://my-app/create-form"`)
324
+ - `name` (required): Name of the resource
325
+ - `description` (optional): Description of the resource
326
+ - `mime_type` (optional): MIME type of the content. Auto-set to `"application/vnd.opal.proteus+json"` when returning a `UI.Document`
327
+ - `title` (optional): Human-readable title
328
+
329
+ The handler function can return either a `str` (manual JSON serialization) or a `UI.Document` (automatic serialization with MIME type set automatically).
330
+
331
+ ### Linking a Tool to a UI Resource
332
+
333
+ Use the `ui_resource` parameter on `@tool` to associate a tool with a Proteus UI resource. The frontend fetches and renders the resource when the tool is invoked:
334
+
335
+ ```python
336
+ from opal_tools_sdk import tool
337
+ from pydantic import BaseModel, Field
338
+
339
+ class CreateItemParams(BaseModel):
340
+ item_name: str = Field(description="Name of the item")
341
+ description: str = Field(description="Item description")
342
+
343
+ @tool(
344
+ "create_item",
345
+ "Create a new item",
346
+ ui_resource="ui://my-app/create-form",
347
+ )
348
+ async def create_item(parameters: CreateItemParams):
349
+ return {"id": "item-123", "name": parameters.item_name, "status": "created"}
350
+ ```
351
+
352
+ ### Building UI with `UI.Document`
353
+
354
+ Import the `UI` namespace from `opal_tools_sdk` or `opal_tools_sdk.ui`. It provides type-safe builders for all Proteus components:
355
+
356
+ ```python
357
+ from opal_tools_sdk import UI
358
+ ```
359
+
360
+ **Available components:**
361
+
362
+ | Category | Components |
363
+ |----------|-----------|
364
+ | Layout | `UI.Document`, `UI.Group`, `UI.Card`, `UI.CardHeader`, `UI.CardLink`, `UI.Separator` |
365
+ | Typography | `UI.Heading`, `UI.Text`, `UI.Link` |
366
+ | Data Display | `UI.Avatar`, `UI.Badge`, `UI.DataTable`, `UI.Chart`, `UI.IconCalendar`, `UI.Image`, `UI.ImageCarousel`, `UI.Time` |
367
+ | Form Controls | `UI.Field`, `UI.Input`, `UI.Textarea`, `UI.Select`, `UI.SelectTrigger`, `UI.SelectContent`, `UI.Switch`, `UI.Range`, `UI.Question` |
368
+ | Actions | `UI.Action`, `UI.CancelAction` |
369
+ | Dynamic | `UI.Value`, `UI.Map`, `UI.MapIndex`, `UI.Show`, `UI.Concat`, `UI.Zip` |
370
+
371
+ **Data binding** with `UI.Value` resolves paths from the tool response:
372
+
373
+ ```python
374
+ @resource(uri="ui://my-app/results", name="results")
375
+ async def get_results():
376
+ return UI.Document(
377
+ title=UI.Value(path="/title"),
378
+ body=UI.Map(
379
+ path="/items",
380
+ children=UI.Text(children=UI.Value(path="name")),
381
+ ),
382
+ )
383
+ ```
384
+
385
+ **Conditional rendering** with `UI.Show`:
386
+
387
+ ```python
388
+ UI.Show(
389
+ when={"!!": UI.Value(path="/error")},
390
+ children=UI.Text(children="An error occurred", color="fg.error"),
391
+ )
392
+ ```
393
+
394
+ The MIME type constant is available as `UI.MIME_TYPE` (`"application/vnd.opal.proteus+json"`).
395
+
210
396
  ## Type Definitions
211
397
 
212
398
  The SDK provides several TypedDict and dataclass definitions for better type safety:
@@ -1,9 +1,17 @@
1
- from .service import ToolsService
2
- from .decorators import tool, resource, interaction
3
1
  from .auth import requires_auth
2
+ from .decorators import interaction, resource, tool
4
3
  from .logging import register_logger_factory
5
- from .models import AuthData, AuthRequirement, Credentials, Environment, InteractionContext, IslandConfig, IslandResponse
4
+ from .models import (
5
+ AuthData,
6
+ AuthRequirement,
7
+ Credentials,
8
+ Environment,
9
+ InteractionContext,
10
+ IslandConfig,
11
+ IslandResponse,
12
+ )
6
13
  from .proteus import UI
14
+ from .service import ToolsService
7
15
 
8
16
  __version__ = "0.1.14-dev"
9
17
  __all__ = [
@@ -1,3 +1,5 @@
1
1
  """Internal registry for ToolsService instances."""
2
2
 
3
- services = []
3
+ from typing import Any
4
+
5
+ services: list[Any] = []
@@ -1,7 +1,9 @@
1
- from typing import Callable, Dict, Any, Optional, List
1
+ from collections.abc import Callable
2
2
  from functools import wraps
3
+
3
4
  from fastapi import Header, HTTPException
4
5
 
6
+
5
7
  def requires_auth(provider: str, scope_bundle: str, required: bool = True):
6
8
  """Decorator to indicate that a tool requires authentication.
7
9
 
@@ -13,9 +15,10 @@ def requires_auth(provider: str, scope_bundle: str, required: bool = True):
13
15
  Returns:
14
16
  Decorator function
15
17
  """
18
+
16
19
  def decorator(func: Callable):
17
20
  @wraps(func)
18
- async def wrapper(*args, authorization: Optional[str] = Header(None), **kwargs):
21
+ async def wrapper(*args, authorization: str | None = Header(None), **kwargs):
19
22
  if required and not authorization:
20
23
  raise HTTPException(status_code=401, detail="Authentication required")
21
24
 
@@ -24,18 +27,14 @@ def requires_auth(provider: str, scope_bundle: str, required: bool = True):
24
27
  return await func(*args, authorization=authorization, **kwargs)
25
28
 
26
29
  # Store auth requirements in function metadata
27
- auth_req = {
28
- "provider": provider,
29
- "scope_bundle": scope_bundle,
30
- "required": required
31
- }
30
+ auth_req = {"provider": provider, "scope_bundle": scope_bundle, "required": required}
32
31
 
33
32
  # Initialize the list if it doesn't exist
34
33
  if not hasattr(wrapper, "__auth_requirements__"):
35
- wrapper.__auth_requirements__ = []
34
+ wrapper.__auth_requirements__ = [] # type: ignore[attr-defined]
36
35
 
37
36
  # Add this auth requirement to the list
38
- wrapper.__auth_requirements__.append(auth_req)
37
+ wrapper.__auth_requirements__.append(auth_req) # type: ignore[attr-defined]
39
38
 
40
39
  return wrapper
41
40