optimizely-opal.opal-tools-sdk 0.1.0.dev0__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 optimizely-opal.opal-tools-sdk might be problematic. Click here for more details.
- opal_tools_sdk/__init__.py +6 -0
- opal_tools_sdk/_registry.py +3 -0
- opal_tools_sdk/auth.py +42 -0
- opal_tools_sdk/decorators.py +128 -0
- opal_tools_sdk/models.py +69 -0
- opal_tools_sdk/service.py +171 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/METADATA +24 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/RECORD +10 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/WHEEL +5 -0
- optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/top_level.txt +1 -0
opal_tools_sdk/auth.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Callable, Dict, Any, Optional, List
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from fastapi import Header, HTTPException
|
|
4
|
+
|
|
5
|
+
def requires_auth(provider: str, scope_bundle: str, required: bool = True):
|
|
6
|
+
"""Decorator to indicate that a tool requires authentication.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
provider: Authentication provider (e.g., "google", "microsoft")
|
|
10
|
+
scope_bundle: Scope bundle required (e.g., "calendar", "drive")
|
|
11
|
+
required: Whether authentication is mandatory (default: True)
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Decorator function
|
|
15
|
+
"""
|
|
16
|
+
def decorator(func: Callable):
|
|
17
|
+
@wraps(func)
|
|
18
|
+
async def wrapper(*args, authorization: Optional[str] = Header(None), **kwargs):
|
|
19
|
+
if required and not authorization:
|
|
20
|
+
raise HTTPException(status_code=401, detail="Authentication required")
|
|
21
|
+
|
|
22
|
+
# The Tools Management Service will provide the appropriate token
|
|
23
|
+
# in the Authorization header
|
|
24
|
+
return await func(*args, authorization=authorization, **kwargs)
|
|
25
|
+
|
|
26
|
+
# Store auth requirements in function metadata
|
|
27
|
+
auth_req = {
|
|
28
|
+
"provider": provider,
|
|
29
|
+
"scope_bundle": scope_bundle,
|
|
30
|
+
"required": required
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Initialize the list if it doesn't exist
|
|
34
|
+
if not hasattr(wrapper, "__auth_requirements__"):
|
|
35
|
+
wrapper.__auth_requirements__ = []
|
|
36
|
+
|
|
37
|
+
# Add this auth requirement to the list
|
|
38
|
+
wrapper.__auth_requirements__.append(auth_req)
|
|
39
|
+
|
|
40
|
+
return wrapper
|
|
41
|
+
|
|
42
|
+
return decorator
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import re
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Callable, Any, List, Dict, Type, get_type_hints, Optional, Union
|
|
5
|
+
from fastapi import APIRouter, Depends, Header, HTTPException
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from .models import Parameter, ParameterType, AuthRequirement
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("opal_tools_sdk")
|
|
11
|
+
|
|
12
|
+
def tool(name: str, description: str, auth_requirements: Optional[List[Dict[str, Any]]] = None):
|
|
13
|
+
"""Decorator to register a function as an Opal tool.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
name: Name of the tool
|
|
17
|
+
description: Description of the tool
|
|
18
|
+
auth_requirements: Authentication requirements (optional)
|
|
19
|
+
Format: [{"provider": "oauth_provider", "scope_bundle": "permissions_scope", "required": True}, ...]
|
|
20
|
+
Example: [{"provider": "google", "scope_bundle": "calendar", "required": True}]
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Decorator function
|
|
24
|
+
|
|
25
|
+
Note:
|
|
26
|
+
If your tool requires authentication, define your handler function with two parameters:
|
|
27
|
+
async def my_tool(parameters: ParametersModel, auth_data: Optional[Dict] = None):
|
|
28
|
+
...
|
|
29
|
+
"""
|
|
30
|
+
def decorator(func: Callable):
|
|
31
|
+
# Get the ToolsService instance from the global registry
|
|
32
|
+
from . import _registry
|
|
33
|
+
|
|
34
|
+
# Extract parameters from function signature
|
|
35
|
+
sig = inspect.signature(func)
|
|
36
|
+
type_hints = get_type_hints(func)
|
|
37
|
+
|
|
38
|
+
parameters: List[Parameter] = []
|
|
39
|
+
param_model = None
|
|
40
|
+
|
|
41
|
+
# Look for a parameter that is a pydantic model (for parameters)
|
|
42
|
+
for param_name, param in sig.parameters.items():
|
|
43
|
+
if param_name in type_hints:
|
|
44
|
+
param_type = type_hints[param_name]
|
|
45
|
+
if hasattr(param_type, '__fields__') or hasattr(param_type, 'model_fields'): # Pydantic v1 or v2
|
|
46
|
+
param_model = param_type
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
# If we found a pydantic model, extract parameters
|
|
50
|
+
if param_model:
|
|
51
|
+
model_fields = getattr(param_model, 'model_fields', getattr(param_model, '__fields__', {}))
|
|
52
|
+
for field_name, field in model_fields.items():
|
|
53
|
+
# Get field metadata
|
|
54
|
+
field_info = field.field_info if hasattr(field, 'field_info') else field
|
|
55
|
+
|
|
56
|
+
# Determine type
|
|
57
|
+
if hasattr(field, 'outer_type_'):
|
|
58
|
+
field_type = field.outer_type_
|
|
59
|
+
elif hasattr(field, 'annotation'):
|
|
60
|
+
field_type = field.annotation
|
|
61
|
+
else:
|
|
62
|
+
field_type = str
|
|
63
|
+
|
|
64
|
+
# Map Python type to Parameter type
|
|
65
|
+
param_type = ParameterType.string
|
|
66
|
+
if field_type == int:
|
|
67
|
+
param_type = ParameterType.integer
|
|
68
|
+
elif field_type == float:
|
|
69
|
+
param_type = ParameterType.number
|
|
70
|
+
elif field_type == bool:
|
|
71
|
+
param_type = ParameterType.boolean
|
|
72
|
+
elif field_type == list or field_type == List:
|
|
73
|
+
param_type = ParameterType.list
|
|
74
|
+
elif field_type == dict or field_type == Dict:
|
|
75
|
+
param_type = ParameterType.dictionary
|
|
76
|
+
|
|
77
|
+
# Determine if required
|
|
78
|
+
required = field_info.default is ... if hasattr(field_info, 'default') else True
|
|
79
|
+
|
|
80
|
+
# Get description
|
|
81
|
+
description_text = ""
|
|
82
|
+
if hasattr(field_info, 'description'):
|
|
83
|
+
description_text = field_info.description
|
|
84
|
+
elif hasattr(field, 'description'):
|
|
85
|
+
description_text = field.description
|
|
86
|
+
|
|
87
|
+
parameters.append(Parameter(
|
|
88
|
+
name=field_name,
|
|
89
|
+
param_type=param_type,
|
|
90
|
+
description=description_text,
|
|
91
|
+
required=required
|
|
92
|
+
))
|
|
93
|
+
|
|
94
|
+
print(f"Registered parameter: {field_name} of type {param_type.value}, required: {required}")
|
|
95
|
+
else:
|
|
96
|
+
print(f"Warning: No parameter model found for {name}")
|
|
97
|
+
|
|
98
|
+
endpoint = f"/tools/{name}"
|
|
99
|
+
|
|
100
|
+
# Register the tool with the service
|
|
101
|
+
auth_req_list = None
|
|
102
|
+
if auth_requirements:
|
|
103
|
+
auth_req_list = []
|
|
104
|
+
for auth_req in auth_requirements:
|
|
105
|
+
auth_req_list.append(AuthRequirement(
|
|
106
|
+
provider=auth_req.get("provider", ""),
|
|
107
|
+
scope_bundle=auth_req.get("scope_bundle", ""),
|
|
108
|
+
required=auth_req.get("required", True)
|
|
109
|
+
))
|
|
110
|
+
|
|
111
|
+
print(f"Registering tool {name} with endpoint {endpoint}")
|
|
112
|
+
|
|
113
|
+
if not _registry.services:
|
|
114
|
+
print("No services registered in registry! Make sure to create ToolsService before decorating functions.")
|
|
115
|
+
|
|
116
|
+
for service in _registry.services:
|
|
117
|
+
service.register_tool(
|
|
118
|
+
name=name,
|
|
119
|
+
description=description,
|
|
120
|
+
handler=func,
|
|
121
|
+
parameters=parameters,
|
|
122
|
+
endpoint=endpoint,
|
|
123
|
+
auth_requirements=auth_req_list
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return func
|
|
127
|
+
|
|
128
|
+
return decorator
|
opal_tools_sdk/models.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import List, Dict, Any, Optional
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
class ParameterType(str, Enum):
|
|
6
|
+
"""Types of parameters supported by Opal tools."""
|
|
7
|
+
string = "string"
|
|
8
|
+
integer = "integer"
|
|
9
|
+
number = "number"
|
|
10
|
+
boolean = "boolean"
|
|
11
|
+
list = "list" # Changed to match main service expectation
|
|
12
|
+
dictionary = "object" # Standard JSON schema type
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Parameter:
|
|
16
|
+
"""Parameter definition for an Opal tool."""
|
|
17
|
+
name: str
|
|
18
|
+
param_type: ParameterType
|
|
19
|
+
description: str
|
|
20
|
+
required: bool
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
23
|
+
"""Convert to dictionary for the discovery endpoint."""
|
|
24
|
+
return {
|
|
25
|
+
"name": self.name,
|
|
26
|
+
"type": self.param_type.value,
|
|
27
|
+
"description": self.description,
|
|
28
|
+
"required": self.required
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class AuthRequirement:
|
|
33
|
+
"""Authentication requirements for an Opal tool."""
|
|
34
|
+
provider: str # e.g., "google", "microsoft"
|
|
35
|
+
scope_bundle: str # e.g., "calendar", "drive"
|
|
36
|
+
required: bool = True
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
39
|
+
"""Convert to dictionary for the discovery endpoint."""
|
|
40
|
+
return {
|
|
41
|
+
"provider": self.provider,
|
|
42
|
+
"scope_bundle": self.scope_bundle,
|
|
43
|
+
"required": self.required
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Function:
|
|
48
|
+
"""Function definition for an Opal tool."""
|
|
49
|
+
name: str
|
|
50
|
+
description: str
|
|
51
|
+
parameters: List[Parameter]
|
|
52
|
+
endpoint: str
|
|
53
|
+
auth_requirements: Optional[List[AuthRequirement]] = None
|
|
54
|
+
http_method: str = "POST"
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
57
|
+
"""Convert to dictionary for the discovery endpoint."""
|
|
58
|
+
result = {
|
|
59
|
+
"name": self.name,
|
|
60
|
+
"description": self.description,
|
|
61
|
+
"parameters": [p.to_dict() for p in self.parameters],
|
|
62
|
+
"endpoint": self.endpoint,
|
|
63
|
+
"http_method": self.http_method
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if self.auth_requirements:
|
|
67
|
+
result["auth_requirements"] = [auth.to_dict() for auth in self.auth_requirements]
|
|
68
|
+
|
|
69
|
+
return result
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from typing import Dict, List, Any, Callable, Type, Optional, get_type_hints
|
|
2
|
+
import inspect
|
|
3
|
+
import logging
|
|
4
|
+
from fastapi import FastAPI, APIRouter, Depends, Header, HTTPException, Request
|
|
5
|
+
from fastapi.routing import APIRoute
|
|
6
|
+
from pydantic import BaseModel, create_model
|
|
7
|
+
|
|
8
|
+
from .models import Function, Parameter, ParameterType, AuthRequirement
|
|
9
|
+
from . import _registry
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("opal_tools_sdk")
|
|
12
|
+
|
|
13
|
+
class ToolsService:
|
|
14
|
+
"""Main class for managing Opal tools."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, app: FastAPI):
|
|
17
|
+
"""Initialize the tools service.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
app: FastAPI application to attach routes to
|
|
21
|
+
"""
|
|
22
|
+
self.app = app
|
|
23
|
+
self.router = APIRouter()
|
|
24
|
+
self.functions: List[Function] = []
|
|
25
|
+
self._init_routes()
|
|
26
|
+
|
|
27
|
+
# Register in the global registry
|
|
28
|
+
_registry.services.append(self)
|
|
29
|
+
|
|
30
|
+
# Debug existing routes
|
|
31
|
+
@app.get("/debug-routes")
|
|
32
|
+
async def debug_routes():
|
|
33
|
+
routes = []
|
|
34
|
+
for route in app.routes:
|
|
35
|
+
if isinstance(route, APIRoute):
|
|
36
|
+
routes.append({
|
|
37
|
+
"path": route.path,
|
|
38
|
+
"name": route.name,
|
|
39
|
+
"methods": route.methods
|
|
40
|
+
})
|
|
41
|
+
return {"routes": routes}
|
|
42
|
+
|
|
43
|
+
def _init_routes(self) -> None:
|
|
44
|
+
"""Initialize the discovery endpoint."""
|
|
45
|
+
@self.router.get("/discovery")
|
|
46
|
+
async def discovery() -> Dict[str, Any]:
|
|
47
|
+
"""Return the discovery information for this tools service."""
|
|
48
|
+
return {"functions": [f.to_dict() for f in self.functions]}
|
|
49
|
+
|
|
50
|
+
# Include router in app
|
|
51
|
+
self.app.include_router(self.router)
|
|
52
|
+
|
|
53
|
+
def _extract_auth_requirements(self, handler: Callable) -> List[AuthRequirement]:
|
|
54
|
+
"""Extract auth requirements from a handler function decorated with @requires_auth.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
handler: The handler function
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
List of AuthRequirement objects
|
|
61
|
+
"""
|
|
62
|
+
auth_requirements = []
|
|
63
|
+
|
|
64
|
+
# Check if the function is wrapped with @requires_auth
|
|
65
|
+
if hasattr(handler, "__auth_requirements__"):
|
|
66
|
+
# Auth requirements should always be a list
|
|
67
|
+
if isinstance(handler.__auth_requirements__, list):
|
|
68
|
+
for req in handler.__auth_requirements__:
|
|
69
|
+
auth_requirements.append(AuthRequirement(
|
|
70
|
+
provider=req.get("provider", ""),
|
|
71
|
+
scope_bundle=req.get("scope_bundle", ""),
|
|
72
|
+
required=req.get("required", True)
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
return auth_requirements
|
|
76
|
+
|
|
77
|
+
def register_tool(self,
|
|
78
|
+
name: str,
|
|
79
|
+
description: str,
|
|
80
|
+
handler: Callable,
|
|
81
|
+
parameters: List[Parameter],
|
|
82
|
+
endpoint: str,
|
|
83
|
+
auth_requirements: Optional[List[AuthRequirement]] = None) -> None:
|
|
84
|
+
"""Register a tool function.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
name: Name of the tool
|
|
88
|
+
description: Description of the tool
|
|
89
|
+
handler: Function that implements the tool
|
|
90
|
+
parameters: List of parameters for the tool
|
|
91
|
+
endpoint: API endpoint for the tool
|
|
92
|
+
auth_requirements: List of authentication requirements (optional)
|
|
93
|
+
"""
|
|
94
|
+
print(f"Registering tool: {name} with endpoint: {endpoint}")
|
|
95
|
+
|
|
96
|
+
# Extract auth requirements from handler if decorated with @requires_auth
|
|
97
|
+
handler_auth_requirements = self._extract_auth_requirements(handler)
|
|
98
|
+
|
|
99
|
+
# If auth_requirements is explicitly provided, it takes precedence
|
|
100
|
+
# Otherwise, use the requirements extracted from the handler
|
|
101
|
+
final_auth_requirements = auth_requirements if auth_requirements else handler_auth_requirements
|
|
102
|
+
|
|
103
|
+
function = Function(
|
|
104
|
+
name=name,
|
|
105
|
+
description=description,
|
|
106
|
+
parameters=parameters,
|
|
107
|
+
endpoint=endpoint,
|
|
108
|
+
auth_requirements=final_auth_requirements
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
self.functions.append(function)
|
|
112
|
+
|
|
113
|
+
# Create a direct route with the app for better control
|
|
114
|
+
@self.app.post(endpoint)
|
|
115
|
+
async def tool_endpoint(request: Request):
|
|
116
|
+
try:
|
|
117
|
+
# Parse JSON body
|
|
118
|
+
body = await request.json()
|
|
119
|
+
print(f"Received request for {endpoint} with body: {body}")
|
|
120
|
+
|
|
121
|
+
# Parameters should be in the "parameters" key according to the spec
|
|
122
|
+
# This matches how the tools-mgmt-service calls tools
|
|
123
|
+
if "parameters" in body:
|
|
124
|
+
params = body["parameters"]
|
|
125
|
+
else:
|
|
126
|
+
# For backward compatibility with direct test calls
|
|
127
|
+
print(f"Warning: 'parameters' key not found in request body. Using body directly.")
|
|
128
|
+
params = body
|
|
129
|
+
|
|
130
|
+
# Extract auth data if available
|
|
131
|
+
auth_data = body.get("auth")
|
|
132
|
+
if auth_data:
|
|
133
|
+
print(f"Auth data provided for provider: {auth_data.get('provider', 'unknown')}")
|
|
134
|
+
|
|
135
|
+
print(f"Extracted parameters: {params}")
|
|
136
|
+
|
|
137
|
+
# Get the parameter model from handler's signature
|
|
138
|
+
sig = inspect.signature(handler)
|
|
139
|
+
param_name = list(sig.parameters.keys())[0]
|
|
140
|
+
param_type = get_type_hints(handler).get(param_name)
|
|
141
|
+
|
|
142
|
+
# Check signature to see if it accepts auth data
|
|
143
|
+
accepts_auth = len(sig.parameters) > 1
|
|
144
|
+
|
|
145
|
+
if param_type:
|
|
146
|
+
# Create instance of param model
|
|
147
|
+
model_instance = param_type(**params)
|
|
148
|
+
if accepts_auth:
|
|
149
|
+
# Call with auth data if the handler accepts it
|
|
150
|
+
result = await handler(model_instance, auth_data)
|
|
151
|
+
else:
|
|
152
|
+
# Call without auth data
|
|
153
|
+
result = await handler(model_instance)
|
|
154
|
+
else:
|
|
155
|
+
# Fall back if type hints not available
|
|
156
|
+
if accepts_auth:
|
|
157
|
+
result = await handler(BaseModel(**params), auth_data)
|
|
158
|
+
else:
|
|
159
|
+
result = await handler(BaseModel(**params))
|
|
160
|
+
|
|
161
|
+
print(f"Tool {name} returned: {result}")
|
|
162
|
+
return result
|
|
163
|
+
except Exception as e:
|
|
164
|
+
import traceback
|
|
165
|
+
print(f"Error in tool {name}: {str(e)}")
|
|
166
|
+
print(traceback.format_exc())
|
|
167
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
168
|
+
|
|
169
|
+
# Update the route function name and docstring
|
|
170
|
+
tool_endpoint.__name__ = f"tool_{name}"
|
|
171
|
+
tool_endpoint.__doc__ = description
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: optimizely-opal.opal-tools-sdk
|
|
3
|
+
Version: 0.1.0.dev0
|
|
4
|
+
Summary: SDK for creating Opal-compatible tools services
|
|
5
|
+
Home-page: https://github.com/optimizely/opal-tools-sdk
|
|
6
|
+
Author: Optimizely
|
|
7
|
+
Author-email: opal-team@optimizely.com
|
|
8
|
+
Keywords: opal,tools,sdk,ai,llm
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: fastapi>=0.100.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Requires-Dist: httpx>=0.24.1
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: home-page
|
|
21
|
+
Dynamic: keywords
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
opal_tools_sdk/__init__.py,sha256=Eu141xM3QVq1O_BLGZS9m_asU1b_KaOApFtwvaYll6E,170
|
|
2
|
+
opal_tools_sdk/_registry.py,sha256=YE3eD4kcS09QDe4RccBYAzXPo9znEU7fblrsB-g3o-Y,67
|
|
3
|
+
opal_tools_sdk/auth.py,sha256=9aMiZv6n6_iu7hQA0sKg4hgNr5DzYFFuP0SWUoZf_Vw,1520
|
|
4
|
+
opal_tools_sdk/decorators.py,sha256=a_CJcA11RW8eAt2xRGXZane_HvTZqR7wfhn-6WFgjOY,5057
|
|
5
|
+
opal_tools_sdk/models.py,sha256=fa2hhnZ2GTz6xqhGTNyqyIQj_rBE1UFQxsJ4KfApCiU,2123
|
|
6
|
+
opal_tools_sdk/service.py,sha256=tVaQKFpTX778UUWoczlM2WHSiUXqdYtYSKgTdqTH1Z0,6756
|
|
7
|
+
optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/METADATA,sha256=EoIwGaRzuaWjMlPwej56DdeTW6IcYtJDjttLigMu9ts,748
|
|
8
|
+
optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
|
9
|
+
optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/top_level.txt,sha256=nCJ5PxF0rgoV6yNJvvuUaZPx4D3EWkl7gpu-6xafH1E,15
|
|
10
|
+
optimizely_opal_opal_tools_sdk-0.1.0.dev0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
opal_tools_sdk
|