diff --git a/Makefile b/Makefile index c1de38b6..15b16eb7 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,11 @@ clean: ## Removes all generated code (except _patch.py files) @printf "=== Cleaning src directory\n" @rm -rf src/pydo/resources @rm -rf src/pydo/types - @find src/pydo -type f ! -name "_patch.py" ! -name "custom_*.py" ! -name "exceptions.py" -exec rm -rf {} + + @find src/pydo -type f \ + ! -name "_patch.py" ! -name "custom_*.py" ! -name "exceptions.py" \ + ! -path "*/gateway/*" \ + ! -path "*/action_gateway/*" \ + -exec rm -rf {} + .PHONY: download-spec download-spec: ## Download Latest DO Spec diff --git a/examples/gateway/approval_flow.py b/examples/gateway/approval_flow.py new file mode 100644 index 00000000..51ea2583 --- /dev/null +++ b/examples/gateway/approval_flow.py @@ -0,0 +1,94 @@ +"""Approve Chat Completions tool calls through an Action Gateway session. + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + ACTOR_ID + MODEL + PROMPT +""" + +import json +import os + +from pydo.action_gateway import ActionGatewayClient + + +def find_approval_ids(value): + """Find approval IDs in gateway tool-result messages.""" + approval_ids = [] + if isinstance(value, dict): + for key in ("approval_id", "approvalId"): + if value.get(key): + approval_ids.append(value[key]) + for nested in value.values(): + approval_ids.extend(find_approval_ids(nested)) + elif isinstance(value, list): + for nested in value: + approval_ids.extend(find_approval_ids(nested)) + elif isinstance(value, str): + try: + approval_ids.extend(find_approval_ids(json.loads(value))) + except json.JSONDecodeError: + pass + return approval_ids + + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"], timeout=30) +print("Creating Action Gateway session...") +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + permissions={ + "default_action": "ask", + "rules": [{"tool": "action_search", "action": "allow"}], + }, +) + +model = os.environ.get("MODEL", "openai-gpt-4o") +messages = [ + { + "role": "user", + "content": os.environ.get( + "PROMPT", + "Search for the latest DigitalOcean news and summarize it.", + ), + } +] +tools = session.tools() +tool_choice = "required" + +while True: + print(f"Requesting next tool call from {model}...") + response = client.chat.completions.create( + model=model, + messages=messages, + tools=tools, + tool_choice=tool_choice, + parallel_tool_calls=False, + ) + message = response.choices[0].message + if not message.get("tool_calls"): + break + + print("Executing requested gateway tool...") + tool_messages = session.handle_tool_calls(response) + approval_ids = list(dict.fromkeys(find_approval_ids(tool_messages))) + for approval_id in approval_ids: + input(f"Approve {approval_id}? Press Enter to continue...") + session.approve(approval_id) + + if approval_ids: + print("Retrying approved gateway tool...") + tool_messages = session.handle_tool_calls(response) + + messages.append(dict(message)) + messages.extend(tool_messages) + if any( + tool_call["function"]["name"] != "action_search" + for tool_call in message["tool_calls"] + ): + tool_choice = "auto" + +print("\nFinal answer:\n") +print(message.get("content")) diff --git a/examples/gateway/async_invoke_tools.py b/examples/gateway/async_invoke_tools.py new file mode 100644 index 00000000..bdc2aff5 --- /dev/null +++ b/examples/gateway/async_invoke_tools.py @@ -0,0 +1,45 @@ +"""Async Action Gateway session: list, invoke, and execute code. + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run + ACTOR_ID +""" + +import asyncio +import os + +from pydo.action_gateway.aio import ActionGatewayClient + + +async def main() -> None: + client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) + session = await client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + permissions={ + "default_action": "ask", + "rules": [ + {"tool": "exa_web_search", "action": "allow"}, + {"tool": "execute_code", "action": "allow"}, + ], + }, + ) + + tools = await session.tools.list(include_all=True) + print("session tools:", [tool.name for tool in tools]) + print("MCP URL:", session.url) + + output = await session.tools.invoke_one( + "exa_web_search", {"query": "DigitalOcean Gradient", "max_results": 2} + ) + print("web_search output:", str(output)[:200]) + + result = await session.code.execute("print('hello from async')") + print("code stdout:", result.get("stdout")) + + await client.close() + + +asyncio.run(main()) diff --git a/examples/gateway/create_session.py b/examples/gateway/create_session.py new file mode 100644 index 00000000..1481917d --- /dev/null +++ b/examples/gateway/create_session.py @@ -0,0 +1,25 @@ +"""Create an Action Gateway session and print its MCP URL. + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + ACTOR_ID +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + tools=["exa_web_search@v1"], + config={"preloadTools": ["exa_web_search@v1"]}, + permissions={ + "default_action": "ask", + "rules": [{"tool": "exa_web_search", "action": "allow"}], + }, +) + +print(session.url) diff --git a/examples/gateway/create_toolbelt.py b/examples/gateway/create_toolbelt.py new file mode 100644 index 00000000..f9b08ca1 --- /dev/null +++ b/examples/gateway/create_toolbelt.py @@ -0,0 +1,36 @@ +"""Create a versioned Action Gateway toolbelt. + +Required env: + DIGITALOCEAN_TOKEN +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) + +toolbelt = client.create_toolbelt( + name="search-toolbelt", + tools=[ + "exa_web_search", + "exa_web_fetch", + ], +) + +print(toolbelt.ref) + +# Public Tool Registry APIs are generated from DigitalOcean's OpenAPI spec. +print(client.toolbelts.list(status="active")) +print(client.toolbelts.get("search-toolbelt", version="1")) +client.toolbelts.add_tools( + "search-toolbelt", + body={"tools": ["jira_create_issue"]}, +) +client.toolbelts.delete_tools( + "search-toolbelt", + body={"tools": ["exa_web_fetch"]}, +) + +# Delete the toolbelt when it is no longer needed. +# client.toolbelts.delete("search-toolbelt") diff --git a/examples/gateway/execute_code.py b/examples/gateway/execute_code.py new file mode 100644 index 00000000..886bb8a9 --- /dev/null +++ b/examples/gateway/execute_code.py @@ -0,0 +1,34 @@ +"""Run Python code in the Action Gateway sandbox (action_code). + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run + ACTOR_ID +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + permissions={ + "default_action": "ask", + "rules": [{"tool": "execute_code", "action": "allow"}], + }, +) + +result = session.code.execute( + "import sys\n" "print('hello from the sandbox')\n" "print(sys.version)\n", + thought="verify the sandbox works", +) + +print("exit_code:", result.get("exit_code")) +print("stdout:") +print(result.get("stdout")) +if result.get("stderr"): + print("stderr:") + print(result.get("stderr")) diff --git a/examples/gateway/function_calling_loop.py b/examples/gateway/function_calling_loop.py new file mode 100644 index 00000000..d4770909 --- /dev/null +++ b/examples/gateway/function_calling_loop.py @@ -0,0 +1,57 @@ +"""Agentic function-calling loop: chat completions + Action Gateway session. + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run + ACTOR_ID + MODEL + PROMPT +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + permissions={ + "default_action": "ask", + "rules": [ + {"tool": "exa_web_search", "action": "allow"}, + {"tool": "exa_web_fetch", "action": "allow"}, + ], + }, +) + +model = os.environ.get("MODEL", "openai-gpt-5.4") +prompt = os.environ.get("PROMPT", "Search the web for information on DigitalOcean.") + +tools = session.tools() +messages = [{"role": "user", "content": prompt}] +tool_choice = "required" + +while True: + response = client.chat.completions.create( + model=model, + messages=messages, + tools=tools, + tool_choice=tool_choice, + ) + message = response.choices[0].message + if not message.get("tool_calls"): + break + + messages.append(dict(message)) + tool_messages = session.handle_tool_calls(response) + if any( + tool_call["function"]["name"] != "action_search" + for tool_call in message["tool_calls"] + ): + tool_choice = "auto" + messages.extend(tool_messages) + +print("\nFinal answer:\n") +print(message.get("content")) diff --git a/examples/gateway/invoke_tools.py b/examples/gateway/invoke_tools.py new file mode 100644 index 00000000..8415bf5a --- /dev/null +++ b/examples/gateway/invoke_tools.py @@ -0,0 +1,54 @@ +"""Invoke Action Gateway tools in parallel (action_invoke). + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run + ACTOR_ID +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + permissions={ + "default_action": "ask", + "rules": [ + {"tool": "exa_web_search", "action": "allow"}, + {"tool": "exa_web_fetch", "action": "allow"}, + ], + }, +) + +envelope = session.tools.invoke( + [ + { + "tool": "exa_web_search", + "arguments": {"query": "DigitalOcean Gradient", "max_results": 3}, + }, + { + "tool": "exa_web_fetch", + "arguments": {"url": "https://www.digitalocean.com"}, + }, + ], + rationale="demonstrate parallel tool invocation", +) + +print(f"{envelope.success_count}/{envelope.total_count} succeeded\n") +for item in envelope.results: + result = item.result + print(f"[{item.index}] {item.tool}: {result.status}") + if result.status == "succeeded": + print(f" output: {str(result.get('output'))[:200]}") + else: + error = result.get("error", {}) + print(f" error ({error.get('class')}): {error.get('message')}") + +output = session.tools.invoke_one( + "exa_web_search", {"query": "MCP protocol", "max_results": 1} +) +print("\ninvoke_one output:", str(output)[:200]) diff --git a/examples/gateway/list_tools.py b/examples/gateway/list_tools.py new file mode 100644 index 00000000..0ac1b288 --- /dev/null +++ b/examples/gateway/list_tools.py @@ -0,0 +1,31 @@ +"""List Action Gateway tools for a session. + +By default the session exposes three meta-tools (action_search, +action_invoke, action_code). Pass include_all=True to include tools configured +through config.preloadTools. + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run + ACTOR_ID +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), +) + +print("MCP URL:", session.url) +print("\nMeta-tools (default):") +for tool in session.tools.list(): + print(f" {tool.name}: {tool.get('description', '')[:80]}") + +print("\nAll tools exposed by this session MCP endpoint:") +for tool in session.tools.list(include_all=True): + print(f" {tool.name}: {tool.get('description', '')[:80]}") diff --git a/examples/gateway/messages_tool_use.py b/examples/gateway/messages_tool_use.py new file mode 100644 index 00000000..2dec6211 --- /dev/null +++ b/examples/gateway/messages_tool_use.py @@ -0,0 +1,80 @@ +"""Tool use via the Messages API (Anthropic format) + Action Gateway session. + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run + ACTOR_ID + MODEL + PROMPT +""" + +import os + +from pydo.action_gateway import ActionGatewayClient, MessagesProvider + + +def _assistant_content(response) -> list: + """Return assistant content blocks, tolerating missing keys.""" + content = response.get("content") + return list(content) if content else [] + + +def _print_final_message(response) -> None: + """Print assistant text from a Messages API response.""" + if response.get("type") == "error": + error = response.get("error") or {} + raise RuntimeError(f"Messages API error: {error}") + + blocks = _assistant_content(response) + printed = False + for block in blocks: + if block.get("type") == "text" and block.get("text"): + print(block["text"]) + printed = True + + if not printed: + stop_reason = response.get("stop_reason") + if stop_reason: + print(f"(no text blocks; stop_reason={stop_reason!r})") + print(response) + + +client = ActionGatewayClient( + token=os.environ["DIGITALOCEAN_TOKEN"], + gateway_provider=MessagesProvider(), +) +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + permissions={ + "default_action": "ask", + "rules": [ + {"tool": "exa_web_search", "action": "allow"}, + {"tool": "exa_web_fetch", "action": "allow"}, + ], + }, +) + +model = os.environ.get("MODEL", "claude-opus-4-6") +prompt = os.environ.get( + "PROMPT", "Find the latest news about DigitalOcean and summarize it." +) + +tools = session.tools() +messages = [{"role": "user", "content": prompt}] + +while True: + response = client.messages.create( + model=model, + max_tokens=1024, + tools=tools, + messages=messages, + ) + if response.get("stop_reason") != "tool_use": + break + + messages.append({"role": "assistant", "content": _assistant_content(response)}) + messages.extend(session.handle_tool_calls(response)) + +_print_final_message(response) diff --git a/examples/gateway/public_api.py b/examples/gateway/public_api.py new file mode 100644 index 00000000..46b2bdea --- /dev/null +++ b/examples/gateway/public_api.py @@ -0,0 +1,80 @@ +"""Use every OpenAPI-generated Action Gateway resource. + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + ACTOR_ID + CONNECTION_ID enables get, update, and delete connection examples + SESSION_URN enables session deletion example +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) +actor_id = os.environ.get("ACTOR_ID", "example-user") + +# Tools are read-only catalog resources. +print("Tools:", client.tools.list(toolkit_id="exa")) +print("Toolkits:", client.tools.list_toolkits()) +print("Providers:", client.tools.list_providers()) +print( + "Definition:", + client.tools.get_definition("exa_web_search", version="v1"), +) + +# Toolbelts support create, list, get, membership changes, and delete. +toolbelt = client.toolbelts.create( + body={"name": "search-toolbelt", "tools": ["exa_web_search"]} +) +print("Created toolbelt:", toolbelt) +print("Toolbelts:", client.toolbelts.list(status="active")) +print("Toolbelt:", client.toolbelts.get("search-toolbelt")) +client.toolbelts.add_tools( + "search-toolbelt", + body={"tools": ["exa_web_fetch"]}, +) +client.toolbelts.delete_tools( + "search-toolbelt", + body={"tools": ["exa_web_fetch"]}, +) + +# Connections support create, list, get, parameter updates, and delete. +connection = client.connections.create( + body={"provider": "github", "user_id": actor_id, "scopes": ["repo"]} +) +print("Created connection:", connection) +print("Connections:", client.connections.list(user_id=actor_id)) + +connection_id = os.environ.get("CONNECTION_ID") +if connection_id: + print("Connection:", client.connections.get(connection_id)) + client.connections.update( + connection_id, + body={"connection_parameters": {"site_url": "https://github.com"}}, + ) + client.connections.delete(connection_id) + +# Users are derived from their sessions and connections. +print("Users:", client.users.list()) +print("User:", client.users.get(actor_id)) + +# Sessions are generated too. The convenience session API delegates creation +# to this same generated resource and returns a session bound to response.mcpUrl. +print("Sessions:", client.sessions_api.list(end_user_id=actor_id)) +session = client.session.create( + actor_id=actor_id, + tools=["exa_web_search@v1"], + config={"preloadTools": ["exa_web_search@v1"]}, + permissions={"default_action": "ask"}, +) +print("Session MCP URL:", session.url) + +session_urn = os.environ.get("SESSION_URN") +if session_urn: + client.sessions_api.delete(session_urn) + +# Uncomment when the example toolbelt is no longer needed. +# client.toolbelts.delete("search-toolbelt") diff --git a/examples/gateway/responses_tool_use.py b/examples/gateway/responses_tool_use.py new file mode 100644 index 00000000..1f7e084b --- /dev/null +++ b/examples/gateway/responses_tool_use.py @@ -0,0 +1,38 @@ +"""Tool use via the Responses API + Action Gateway session. + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + ACTOR_ID + MODEL + PROMPT +""" + +import os + +from pydo.action_gateway import ActionGatewayClient, ResponsesProvider + +client = ActionGatewayClient( + token=os.environ["DIGITALOCEAN_TOKEN"], + gateway_provider=ResponsesProvider(), +) +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + permissions={ + "default_action": "ask", + "rules": [{"tool": "exa_web_search", "action": "allow"}], + }, +) + +response = client.responses.create( + model=os.environ.get("MODEL", "openai-gpt-4o"), + input=os.environ.get( + "PROMPT", + "Find the latest DigitalOcean news and summarize it.", + ), + tools=session.tools(), +) + +for tool_output in session.handle_tool_calls(response): + print(tool_output) diff --git a/examples/gateway/search_tools.py b/examples/gateway/search_tools.py new file mode 100644 index 00000000..715866c6 --- /dev/null +++ b/examples/gateway/search_tools.py @@ -0,0 +1,31 @@ +"""Search the Action Gateway tool catalog by use case (action_search). + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + PYDO_GATEWAY_ENDPOINT preview: https://actions.do-ai-test.run + ACTOR_ID + USE_CASE +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), +) + +use_case = os.environ.get("USE_CASE", "search the public web for a topic") + +payload = session.tools.search(use_case, limit=3) + +for group in payload.get("results", []): + print(f"use case: {group.get('use_case')}") + for match in group.get("results", []): + print(f" {match.get('name')} (score {match.get('score')})") + print(f" {match.get('description', '')[:100]}") + if group.get("guidance"): + print(f" guidance: {group['guidance']}") diff --git a/examples/gateway/session_controls.py b/examples/gateway/session_controls.py new file mode 100644 index 00000000..ea110b6d --- /dev/null +++ b/examples/gateway/session_controls.py @@ -0,0 +1,41 @@ +"""Control Action Gateway discovery, direct tools, and invocation policy. + +The three session controls have separate roles: + tools catalog available to action_search/action_invoke + config.preloadTools concrete tools also exposed directly over MCP + permissions allow, ask, or deny each invocation + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + ACTOR_ID +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + tools=["exa_web_search@v1", "exa_web_fetch@v1"], + config={"preloadTools": ["exa_web_search@v1"]}, + permissions={ + "default_action": "deny", + "rules": [ + {"tool": "exa_web_search", "action": "allow"}, + {"tool": "exa_web_fetch", "action": "ask"}, + ], + }, +) + +print("MCP URL:", session.url) +print("Selected for search/invoke:", session.selected_tools) +print( + "Exposed directly:", + [tool.name for tool in session.tools.list(include_all=True)], +) + +results = session.tools.search("search or fetch a public web page") +print("Search results:", results) diff --git a/examples/gateway/toolbelt_policy.py b/examples/gateway/toolbelt_policy.py new file mode 100644 index 00000000..b1a2dc77 --- /dev/null +++ b/examples/gateway/toolbelt_policy.py @@ -0,0 +1,28 @@ +"""Create a session whose policy allows one pinned toolbelt. + +Required env: + DIGITALOCEAN_TOKEN + +Optional env: + ACTOR_ID +""" + +import os + +from pydo.action_gateway import ActionGatewayClient + +client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) + +session = client.session.create( + actor_id=os.environ.get("ACTOR_ID", "example-user"), + permissions={ + "default_action": "ask", + "rules": [ + {"tool": "toolbelt:search-toolbelt@1", "action": "allow"}, + {"tool": "exa_web_search", "action": "allow"}, + ], + }, +) + +print("MCP URL:", session.url) +print("tools:", [tool["function"]["name"] for tool in session.tools()]) diff --git a/openapi/README.md b/openapi/README.md new file mode 100644 index 00000000..a1366e7e --- /dev/null +++ b/openapi/README.md @@ -0,0 +1,19 @@ +# Action Gateway OpenAPI patch + +`action-gateway-toolbelts.patch` adds the public Toolbelts API used to generate +the synchronous and asynchronous `client.toolbelts` operations. + +The patch is based on the OpenAPI revision recorded in +`DO_OPENAPI_COMMIT_SHA.txt`. To regenerate the SDK: + +```shell +git -C /path/to/openapi checkout "$(cat DO_OPENAPI_COMMIT_SHA.txt)" +git -C /path/to/openapi apply "$PWD/openapi/action-gateway-toolbelts.patch" +make -C /path/to/openapi bundle \ + BUNDLE_PATH="$PWD/DigitalOcean-public.v2.yaml" +SPEC_FILE="$PWD/DigitalOcean-public.v2.yaml" make generate +``` + +Submit the same source changes to the DigitalOcean OpenAPI repository. Once +they are published and `DO_OPENAPI_COMMIT_SHA.txt` advances to include them, +remove this transitional patch. diff --git a/openapi/action-gateway-toolbelts.patch b/openapi/action-gateway-toolbelts.patch new file mode 100644 index 00000000..c89cfd96 --- /dev/null +++ b/openapi/action-gateway-toolbelts.patch @@ -0,0 +1,518 @@ +diff --git a/specification/DigitalOcean-public.v2.yaml b/specification/DigitalOcean-public.v2.yaml +index b8c29b4..6bbe488 100644 +--- a/specification/DigitalOcean-public.v2.yaml ++++ b/specification/DigitalOcean-public.v2.yaml +@@ -50,6 +50,9 @@ tags: + + - `Accept: application/vnd.digitalocean.reserveip+json` + ++ - name: Action Gateway ++ description: Manage versioned tool collections used by Action Gateway sessions. ++ + - name: Add-Ons + description: |- + Add-ons are third-party applications that can be added to your DigitalOcean account. +@@ -718,6 +721,26 @@ x-tagGroups: + - Serverless Inference + + paths: ++ /v2/toolbelts: ++ get: ++ $ref: "resources/action_gateway/toolbelts_list.yml" ++ post: ++ $ref: "resources/action_gateway/toolbelts_create.yml" ++ ++ /v2/toolbelts/{name}: ++ get: ++ $ref: "resources/action_gateway/toolbelts_get.yml" ++ delete: ++ $ref: "resources/action_gateway/toolbelts_delete.yml" ++ ++ /v2/toolbelts/{name}/tools/add: ++ post: ++ $ref: "resources/action_gateway/toolbelts_add_tools.yml" ++ ++ /v2/toolbelts/{name}/tools/remove: ++ post: ++ $ref: "resources/action_gateway/toolbelts_remove_tools.yml" ++ + /v2/1-clicks: + get: + $ref: "resources/1-clicks/oneClicks_list.yml" +diff --git a/specification/resources/action_gateway/models.yml b/specification/resources/action_gateway/models.yml +new file mode 100644 +index 0000000..c709cc0 +--- /dev/null ++++ b/specification/resources/action_gateway/models.yml +@@ -0,0 +1,193 @@ ++toolbelt: ++ type: object ++ required: ++ - name ++ - version ++ - tools ++ - status ++ - reference ++ - reference_latest ++ - tool_count ++ - created_at ++ - updated_at ++ properties: ++ name: ++ type: string ++ example: search-toolbelt ++ version: ++ type: string ++ pattern: '^[0-9]+$' ++ example: '1' ++ display_name: ++ type: string ++ maxLength: 128 ++ example: Search Tools ++ description: ++ type: string ++ maxLength: 255 ++ example: Tools for searching and fetching public web pages. ++ tools: ++ type: array ++ maxItems: 500 ++ items: ++ type: string ++ example: ++ - exa_web_search ++ - exa_web_fetch ++ status: ++ type: string ++ enum: ++ - active ++ - deprecated ++ example: active ++ reference: ++ type: string ++ description: A reference pinned to this immutable toolbelt version. ++ example: search-toolbelt@1 ++ reference_latest: ++ type: string ++ description: An unversioned reference to the latest active version. ++ example: search-toolbelt ++ tool_count: ++ type: integer ++ format: int32 ++ example: 2 ++ created_at: ++ type: string ++ format: date-time ++ example: '2026-06-11T12:00:00Z' ++ updated_at: ++ type: string ++ format: date-time ++ example: '2026-06-11T12:00:00Z' ++ ++toolbelt_summary: ++ type: object ++ required: ++ - name ++ - latest_version ++ - version_count ++ - tool_count ++ - status ++ - reference_latest ++ - updated_at ++ properties: ++ name: ++ type: string ++ example: search-toolbelt ++ display_name: ++ type: string ++ example: Search Tools ++ description: ++ type: string ++ example: Tools for searching and fetching public web pages. ++ latest_version: ++ type: string ++ example: '1' ++ version_count: ++ type: integer ++ format: int32 ++ example: 1 ++ tool_count: ++ type: integer ++ format: int32 ++ example: 2 ++ status: ++ type: string ++ enum: ++ - active ++ - deprecated ++ example: active ++ reference_latest: ++ type: string ++ example: search-toolbelt ++ updated_at: ++ type: string ++ format: date-time ++ example: '2026-06-11T12:00:00Z' ++ ++toolbelt_create: ++ type: object ++ required: ++ - name ++ - tools ++ properties: ++ name: ++ type: string ++ pattern: '^[a-z][a-z0-9_-]{0,63}$' ++ example: search-toolbelt ++ version: ++ type: string ++ pattern: '^[0-9]+$' ++ default: '1' ++ display_name: ++ type: string ++ maxLength: 128 ++ example: Search Tools ++ description: ++ type: string ++ maxLength: 255 ++ example: Tools for searching and fetching public web pages. ++ tools: ++ type: array ++ maxItems: 500 ++ items: ++ type: string ++ example: ++ - exa_web_search ++ - exa_web_fetch ++ ++toolbelt_tools: ++ type: object ++ required: ++ - tools ++ properties: ++ tools: ++ type: array ++ minItems: 1 ++ maxItems: 500 ++ items: ++ type: string ++ example: ++ - exa_web_search ++ ++toolbelt_response: ++ type: object ++ required: ++ - toolbelt ++ properties: ++ toolbelt: ++ $ref: '#/toolbelt' ++ ++toolbelts_response: ++ type: object ++ required: ++ - toolbelts ++ - pagination ++ properties: ++ toolbelts: ++ type: array ++ items: ++ $ref: '#/toolbelt_summary' ++ pagination: ++ $ref: '#/pagination' ++ ++pagination: ++ type: object ++ required: ++ - page ++ - per_page ++ - total ++ properties: ++ page: ++ type: integer ++ format: int32 ++ example: 1 ++ per_page: ++ type: integer ++ format: int32 ++ example: 20 ++ total: ++ type: integer ++ format: int32 ++ example: 1 +diff --git a/specification/resources/action_gateway/parameters.yml b/specification/resources/action_gateway/parameters.yml +new file mode 100644 +index 0000000..8f40fbf +--- /dev/null ++++ b/specification/resources/action_gateway/parameters.yml +@@ -0,0 +1,33 @@ ++toolbelt_name: ++ name: name ++ in: path ++ required: true ++ description: The natural key identifying the toolbelt. ++ schema: ++ type: string ++ pattern: '^[a-z][a-z0-9_-]{0,63}$' ++ example: search-toolbelt ++ ++toolbelt_version: ++ name: version ++ in: query ++ required: false ++ description: An immutable numeric toolbelt version. Omit to retrieve the latest active version. ++ schema: ++ type: string ++ pattern: '^[0-9]+$' ++ example: '1' ++ ++toolbelt_status: ++ name: status ++ in: query ++ required: false ++ description: Filter toolbelts by status. ++ schema: ++ type: string ++ enum: ++ - active ++ - deprecated ++ - all ++ default: active ++ example: active +diff --git a/specification/resources/action_gateway/response_headers.yml b/specification/resources/action_gateway/response_headers.yml +new file mode 100644 +index 0000000..c8ac026 +--- /dev/null ++++ b/specification/resources/action_gateway/response_headers.yml +@@ -0,0 +1,6 @@ ++ratelimit-limit: ++ $ref: '../../shared/headers.yml#/ratelimit-limit' ++ratelimit-remaining: ++ $ref: '../../shared/headers.yml#/ratelimit-remaining' ++ratelimit-reset: ++ $ref: '../../shared/headers.yml#/ratelimit-reset' +diff --git a/specification/resources/action_gateway/toolbelts_add_tools.yml b/specification/resources/action_gateway/toolbelts_add_tools.yml +new file mode 100644 +index 0000000..8253309 +--- /dev/null ++++ b/specification/resources/action_gateway/toolbelts_add_tools.yml +@@ -0,0 +1,36 @@ ++operationId: toolbelts_add_tools ++summary: Add Tools to a Toolbelt ++description: Adds provider-qualified tool names and creates a new immutable toolbelt version. ++tags: ++ - Action Gateway ++parameters: ++ - $ref: 'parameters.yml#/toolbelt_name' ++requestBody: ++ required: true ++ content: ++ application/json: ++ schema: ++ $ref: 'models.yml#/toolbelt_tools' ++responses: ++ '200': ++ description: The resulting toolbelt version. ++ headers: ++ $ref: 'response_headers.yml' ++ content: ++ application/json: ++ schema: ++ $ref: 'models.yml#/toolbelt_response' ++ '400': ++ $ref: '../../shared/responses/bad_request.yml' ++ '401': ++ $ref: '../../shared/responses/unauthorized.yml' ++ '404': ++ $ref: '../../shared/responses/not_found.yml' ++ '429': ++ $ref: '../../shared/responses/too_many_requests.yml' ++ '500': ++ $ref: '../../shared/responses/server_error.yml' ++ default: ++ $ref: '../../shared/responses/unexpected_error.yml' ++security: ++ - bearer_auth: [] +diff --git a/specification/resources/action_gateway/toolbelts_create.yml b/specification/resources/action_gateway/toolbelts_create.yml +new file mode 100644 +index 0000000..f496b03 +--- /dev/null ++++ b/specification/resources/action_gateway/toolbelts_create.yml +@@ -0,0 +1,34 @@ ++operationId: toolbelts_create ++summary: Create a Toolbelt ++description: Creates a versioned collection of provider-qualified Action Gateway tool names. ++tags: ++ - Action Gateway ++requestBody: ++ required: true ++ content: ++ application/json: ++ schema: ++ $ref: 'models.yml#/toolbelt_create' ++responses: ++ '200': ++ description: A toolbelt was created successfully. ++ headers: ++ $ref: 'response_headers.yml' ++ content: ++ application/json: ++ schema: ++ $ref: 'models.yml#/toolbelt_response' ++ '400': ++ $ref: '../../shared/responses/bad_request.yml' ++ '401': ++ $ref: '../../shared/responses/unauthorized.yml' ++ '409': ++ $ref: '../../shared/responses/conflict.yml' ++ '429': ++ $ref: '../../shared/responses/too_many_requests.yml' ++ '500': ++ $ref: '../../shared/responses/server_error.yml' ++ default: ++ $ref: '../../shared/responses/unexpected_error.yml' ++security: ++ - bearer_auth: [] +diff --git a/specification/resources/action_gateway/toolbelts_delete.yml b/specification/resources/action_gateway/toolbelts_delete.yml +new file mode 100644 +index 0000000..ea6a984 +--- /dev/null ++++ b/specification/resources/action_gateway/toolbelts_delete.yml +@@ -0,0 +1,28 @@ ++operationId: toolbelts_delete ++summary: Delete a Toolbelt ++description: Deprecates the latest active version of a toolbelt. ++tags: ++ - Action Gateway ++parameters: ++ - $ref: 'parameters.yml#/toolbelt_name' ++responses: ++ '200': ++ description: The toolbelt was deprecated successfully. ++ headers: ++ $ref: 'response_headers.yml' ++ content: ++ application/json: ++ schema: ++ $ref: 'models.yml#/toolbelt_response' ++ '401': ++ $ref: '../../shared/responses/unauthorized.yml' ++ '404': ++ $ref: '../../shared/responses/not_found.yml' ++ '429': ++ $ref: '../../shared/responses/too_many_requests.yml' ++ '500': ++ $ref: '../../shared/responses/server_error.yml' ++ default: ++ $ref: '../../shared/responses/unexpected_error.yml' ++security: ++ - bearer_auth: [] +diff --git a/specification/resources/action_gateway/toolbelts_get.yml b/specification/resources/action_gateway/toolbelts_get.yml +new file mode 100644 +index 0000000..b1e6992 +--- /dev/null ++++ b/specification/resources/action_gateway/toolbelts_get.yml +@@ -0,0 +1,29 @@ ++operationId: toolbelts_get ++summary: Retrieve a Toolbelt ++description: Retrieves the latest active version or a specified immutable version of a toolbelt. ++tags: ++ - Action Gateway ++parameters: ++ - $ref: 'parameters.yml#/toolbelt_name' ++ - $ref: 'parameters.yml#/toolbelt_version' ++responses: ++ '200': ++ description: The toolbelt was retrieved successfully. ++ headers: ++ $ref: 'response_headers.yml' ++ content: ++ application/json: ++ schema: ++ $ref: 'models.yml#/toolbelt_response' ++ '401': ++ $ref: '../../shared/responses/unauthorized.yml' ++ '404': ++ $ref: '../../shared/responses/not_found.yml' ++ '429': ++ $ref: '../../shared/responses/too_many_requests.yml' ++ '500': ++ $ref: '../../shared/responses/server_error.yml' ++ default: ++ $ref: '../../shared/responses/unexpected_error.yml' ++security: ++ - bearer_auth: [] +diff --git a/specification/resources/action_gateway/toolbelts_list.yml b/specification/resources/action_gateway/toolbelts_list.yml +new file mode 100644 +index 0000000..f5c4776 +--- /dev/null ++++ b/specification/resources/action_gateway/toolbelts_list.yml +@@ -0,0 +1,28 @@ ++operationId: toolbelts_list ++summary: List Toolbelts ++description: Lists the latest version of each toolbelt owned by the authenticated team. ++tags: ++ - Action Gateway ++parameters: ++ - $ref: 'parameters.yml#/toolbelt_status' ++ - $ref: '../../shared/parameters.yml#/page' ++ - $ref: '../../shared/parameters.yml#/per_page' ++responses: ++ '200': ++ description: Toolbelts were retrieved successfully. ++ headers: ++ $ref: 'response_headers.yml' ++ content: ++ application/json: ++ schema: ++ $ref: 'models.yml#/toolbelts_response' ++ '401': ++ $ref: '../../shared/responses/unauthorized.yml' ++ '429': ++ $ref: '../../shared/responses/too_many_requests.yml' ++ '500': ++ $ref: '../../shared/responses/server_error.yml' ++ default: ++ $ref: '../../shared/responses/unexpected_error.yml' ++security: ++ - bearer_auth: [] +diff --git a/specification/resources/action_gateway/toolbelts_remove_tools.yml b/specification/resources/action_gateway/toolbelts_remove_tools.yml +new file mode 100644 +index 0000000..f28dc3f +--- /dev/null ++++ b/specification/resources/action_gateway/toolbelts_remove_tools.yml +@@ -0,0 +1,36 @@ ++operationId: toolbelts_delete_tools ++summary: Remove Tools from a Toolbelt ++description: Removes tool names and creates a new immutable toolbelt version. ++tags: ++ - Action Gateway ++parameters: ++ - $ref: 'parameters.yml#/toolbelt_name' ++requestBody: ++ required: true ++ content: ++ application/json: ++ schema: ++ $ref: 'models.yml#/toolbelt_tools' ++responses: ++ '200': ++ description: The resulting toolbelt version. ++ headers: ++ $ref: 'response_headers.yml' ++ content: ++ application/json: ++ schema: ++ $ref: 'models.yml#/toolbelt_response' ++ '400': ++ $ref: '../../shared/responses/bad_request.yml' ++ '401': ++ $ref: '../../shared/responses/unauthorized.yml' ++ '404': ++ $ref: '../../shared/responses/not_found.yml' ++ '429': ++ $ref: '../../shared/responses/too_many_requests.yml' ++ '500': ++ $ref: '../../shared/responses/server_error.yml' ++ default: ++ $ref: '../../shared/responses/unexpected_error.yml' ++security: ++ - bearer_auth: [] diff --git a/src/pydo/_client.py b/src/pydo/_client.py index b190e6ab..da85c809 100644 --- a/src/pydo/_client.py +++ b/src/pydo/_client.py @@ -26,6 +26,7 @@ ByoipPrefixesOperations, CdnOperations, CertificatesOperations, + ConnectionsOperations, DatabasesOperations, DedicatedInferencesOperations, DomainsOperations, @@ -54,12 +55,16 @@ ReservedIPv6ActionsOperations, ReservedIPv6Operations, SecurityOperations, + SessionsOperations, SizesOperations, SnapshotsOperations, SpacesKeyOperations, SshKeysOperations, TagsOperations, + ToolbeltsOperations, + ToolsOperations, UptimeOperations, + UsersOperations, VectorDatabasesOperations, VolumeActionsOperations, VolumeSnapshotsOperations, @@ -77,6 +82,16 @@ class GeneratedClient: # pylint: disable=client-accepts-api-version-keyword,too-many-instance-attributes """GeneratedClient. + :ivar tools: ToolsOperations operations + :vartype tools: pydo.operations.ToolsOperations + :ivar toolbelts: ToolbeltsOperations operations + :vartype toolbelts: pydo.operations.ToolbeltsOperations + :ivar connections: ConnectionsOperations operations + :vartype connections: pydo.operations.ConnectionsOperations + :ivar users: UsersOperations operations + :vartype users: pydo.operations.UsersOperations + :ivar sessions: SessionsOperations operations + :vartype sessions: pydo.operations.SessionsOperations :ivar one_clicks: OneClicksOperations operations :vartype one_clicks: pydo.operations.OneClicksOperations :ivar account: AccountOperations operations @@ -211,11 +226,9 @@ def __init__( self._config.custom_hook_policy, self._config.logging_policy, policies.DistributedTracingPolicy(**kwargs), - ( - policies.SensitiveHeaderCleanupPolicy(**kwargs) - if self._config.redirect_policy - else None - ), + policies.SensitiveHeaderCleanupPolicy(**kwargs) + if self._config.redirect_policy + else None, self._config.http_logging_policy, ] self._client: PipelineClient = PipelineClient( @@ -225,6 +238,21 @@ def __init__( self._serialize = Serializer() self._deserialize = Deserializer() self._serialize.client_side_validation = False + self.tools = ToolsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.toolbelts = ToolbeltsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.connections = ConnectionsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.users = UsersOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.sessions = SessionsOperations( + self._client, self._config, self._serialize, self._deserialize + ) self.one_clicks = OneClicksOperations( self._client, self._config, self._serialize, self._deserialize ) diff --git a/src/pydo/_patch.py b/src/pydo/_patch.py index d979f13d..5125202f 100644 --- a/src/pydo/_patch.py +++ b/src/pydo/_patch.py @@ -6,6 +6,7 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ + from typing import Optional from azure.core.credentials import AccessToken @@ -58,6 +59,13 @@ class Client( # type: ignore subdomain (e.g. ``"https://.agents.do-ai.run"``). Required only when using agent inference endpoints. :paramtype agent_endpoint: str + :keyword gateway_endpoint: Action Gateway base URL (default + ``https://actions.do-ai.run``; preview is + ``https://actions.do-ai-test.run``; override via + ``PYDO_GATEWAY_ENDPOINT``). + :keyword gateway_provider: Provider that formats gateway tools for an + inference surface (default :class:`ChatCompletionsProvider`; also + ``MessagesProvider`` and ``ResponsesProvider`` in ``pydo.gateway``). """ def __init__( @@ -68,6 +76,8 @@ def __init__( timeout: int = 120, inference_endpoint: str = INFERENCE_BASE_URL, agent_endpoint: str = "", + gateway_endpoint: Optional[str] = None, + gateway_provider=None, **kwargs, ): if token is not None and api_key is not None: @@ -111,6 +121,17 @@ def __init__( self.images.generate = inference_images.generate self.images.generations = inference_images.generations + try: + from pydo.gateway import GatewayResources + except ImportError: + self.gateway = None + else: + self.gateway = GatewayResources( + self, + gateway_endpoint=gateway_endpoint, + provider=gateway_provider, + ) + def _setup_inference_routing( self, inference_endpoint: str, diff --git a/src/pydo/action_gateway/__init__.py b/src/pydo/action_gateway/__init__.py new file mode 100644 index 00000000..6821fc0b --- /dev/null +++ b/src/pydo/action_gateway/__init__.py @@ -0,0 +1,165 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Action Gateway entry point: ``from pydo.action_gateway import ActionGatewayClient``. + +Purpose-built client for the Action Gateway. Create a session first, then +use ``session.tools`` / ``session.code`` / ``session.handle_tool_calls``. +Inference surfaces inherited from :class:`pydo.Client` (``chat``, +``messages``, ``responses``, …) remain available for agentic loops. + +Example:: + + import os + from pydo.action_gateway import ActionGatewayClient + + client = ActionGatewayClient(token=os.environ["DIGITALOCEAN_TOKEN"]) + session = client.session.create(actor_id="user-123") + + tools = session.tools() + response = client.chat.completions.create( + model="openai-gpt-4o", + messages=[{"role": "user", "content": "Search for DigitalOcean news"}], + tools=tools, + ) + messages = session.handle_tool_calls(response) +""" + +from __future__ import annotations + +from typing import List, Optional + +from pydo import Client as _DigitalOceanClient +from pydo._patch import TokenCredentials +from pydo.gateway import ( + META_CODE, + META_INVOKE, + META_SEARCH, + META_TOOL_NAMES, + DEFAULT_GATEWAY_BASE_URL, + ChatCompletionsProvider, + GatewayProtocolError, + GatewayToolError, + MessagesProvider, + ResponsesProvider, + Session, + SessionsOperations, + Toolbelt, + ToolCall, + normalize_permissions, + resolve_gateway_base_url, + session_mcp_url, +) + +_GATEWAY_SURFACE: tuple = ( + "base_url", + "chat", + "create_toolbelt", + "messages", + "provider", + "responses", + "session", + "sessions", + "sessions_api", + "connections", + "tools", + "toolbelts", + "users", +) + + +class Client(_DigitalOceanClient): + """Action Gateway–focused DigitalOcean Python client. + + Primary surface: + + * ``client.session.create(actor_id=...)`` → :class:`Session` + * ``session.tools`` / ``session.tools()`` — discover and wrap tools + * ``session.code`` — sandboxed Python execution + * ``session.handle_tool_calls(response)`` — run model tool calls + * ``session.url`` — MCP URL for external clients + + Inherits the full :class:`pydo.Client` machinery (auth, transport, + inference routing), so agentic loops can call ``client.chat`` / + ``client.messages`` on the same instance. + """ + + def __init__( + self, + token: Optional[str] = None, + *, + api_key: Optional[str] = None, + timeout: int = 120, + gateway_endpoint: Optional[str] = None, + gateway_provider=None, + **kwargs, + ) -> None: + super().__init__( + token=token, + api_key=api_key, + timeout=timeout, + gateway_endpoint=gateway_endpoint, + gateway_provider=gateway_provider, + **kwargs, + ) + gateway = self.gateway + if gateway is None: + raise RuntimeError( + "Action Gateway package is unavailable; " + "ensure pydo.gateway is installed" + ) + self.sessions_api = self.sessions + self.sessions = gateway.sessions + self.session = self.sessions + self.provider = gateway.provider + + def create_toolbelt(self, name: str, tools, **kwargs) -> Toolbelt: + """Create a versioned collection of Action Gateway tools.""" + if isinstance(tools, (str, bytes)): + raise TypeError("tools must be an iterable of tool names") + body = {"name": name, "tools": list(tools), **kwargs} + response = self.toolbelts.create( + body=body, + cls=Toolbelt.validate_create_response, + ) + return Toolbelt.from_response(response) + + @property + def base_url(self) -> Optional[str]: + """Resolved Action Gateway base URL.""" + gateway = self.gateway + return gateway.base_url if gateway is not None else None + + def __dir__(self) -> List[str]: + return sorted(set(_GATEWAY_SURFACE)) + + def __repr__(self) -> str: + return "" + + +ActionGatewayClient = Client + + +__all__ = [ + "Client", + "ActionGatewayClient", + "TokenCredentials", + "Session", + "SessionsOperations", + "Toolbelt", + "ChatCompletionsProvider", + "MessagesProvider", + "ResponsesProvider", + "GatewayToolError", + "GatewayProtocolError", + "ToolCall", + "normalize_permissions", + "session_mcp_url", + "META_SEARCH", + "META_INVOKE", + "META_CODE", + "META_TOOL_NAMES", + "DEFAULT_GATEWAY_BASE_URL", + "resolve_gateway_base_url", +] diff --git a/src/pydo/action_gateway/aio/__init__.py b/src/pydo/action_gateway/aio/__init__.py new file mode 100644 index 00000000..bc657090 --- /dev/null +++ b/src/pydo/action_gateway/aio/__init__.py @@ -0,0 +1,143 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +# pylint: disable=duplicate-code +"""Async Action Gateway entry point: ``ActionGatewayClient``. + +Asynchronous twin of :class:`pydo.action_gateway.Client`. Same surface, +``await``-friendly. See :mod:`pydo.action_gateway` for usage details. +""" + +from __future__ import annotations + +from typing import List, Optional + +from pydo.aio import Client as _DigitalOceanClient +from pydo.aio._patch import TokenCredentials +from pydo.aio.gateway import ( + AsyncSession, + AsyncSessionsOperations, +) +from pydo.gateway import ( + META_CODE, + META_INVOKE, + META_SEARCH, + META_TOOL_NAMES, + DEFAULT_GATEWAY_BASE_URL, + ChatCompletionsProvider, + GatewayProtocolError, + GatewayToolError, + MessagesProvider, + ResponsesProvider, + Toolbelt, + ToolCall, + normalize_permissions, + resolve_gateway_base_url, + session_mcp_url, +) + +_GATEWAY_SURFACE: tuple = ( + "base_url", + "chat", + "create_toolbelt", + "messages", + "provider", + "responses", + "session", + "sessions", + "sessions_api", + "connections", + "tools", + "toolbelts", + "users", +) + + +class Client(_DigitalOceanClient): + """Action Gateway–focused DigitalOcean async client. + + Asynchronous counterpart to :class:`pydo.action_gateway.Client`. + Create a session with ``await client.session.create(actor_id=...)``, + then use ``session.tools`` / ``session.code`` / + ``await session.handle_tool_calls(...)``. + """ + + def __init__( + self, + token: Optional[str] = None, + *, + api_key: Optional[str] = None, + timeout: int = 120, + gateway_endpoint: Optional[str] = None, + gateway_provider=None, + **kwargs, + ) -> None: + super().__init__( + token=token, + api_key=api_key, + timeout=timeout, + gateway_endpoint=gateway_endpoint, + gateway_provider=gateway_provider, + **kwargs, + ) + gateway = self.gateway + if gateway is None: + raise RuntimeError( + "Action Gateway package is unavailable; " + "ensure pydo.aio.gateway is installed" + ) + self.sessions_api = self.sessions + self.sessions = gateway.sessions + self.session = self.sessions + self.provider = gateway.provider + + async def create_toolbelt(self, name: str, tools, **kwargs) -> Toolbelt: + """Create a versioned collection of Action Gateway tools.""" + if isinstance(tools, (str, bytes)): + raise TypeError("tools must be an iterable of tool names") + body = {"name": name, "tools": list(tools), **kwargs} + response = await self.toolbelts.create( + body=body, + cls=Toolbelt.validate_create_response, + ) + return Toolbelt.from_response(response) + + @property + def base_url(self) -> Optional[str]: + """Resolved Action Gateway base URL.""" + gateway = self.gateway + return gateway.base_url if gateway is not None else None + + def __dir__(self) -> List[str]: + return sorted(set(_GATEWAY_SURFACE)) + + def __repr__(self) -> str: + return "" + + +ActionGatewayClient = Client + + +__all__ = [ + "Client", + "ActionGatewayClient", + "TokenCredentials", + "AsyncSession", + "AsyncSessionsOperations", + "Toolbelt", + "ChatCompletionsProvider", + "MessagesProvider", + "ResponsesProvider", + "GatewayToolError", + "GatewayProtocolError", + "ToolCall", + "normalize_permissions", + "session_mcp_url", + "META_SEARCH", + "META_INVOKE", + "META_CODE", + "META_TOOL_NAMES", + "DEFAULT_GATEWAY_BASE_URL", + "resolve_gateway_base_url", +] diff --git a/src/pydo/aio/_client.py b/src/pydo/aio/_client.py index 6a8b62f4..04057e72 100644 --- a/src/pydo/aio/_client.py +++ b/src/pydo/aio/_client.py @@ -26,6 +26,7 @@ ByoipPrefixesOperations, CdnOperations, CertificatesOperations, + ConnectionsOperations, DatabasesOperations, DedicatedInferencesOperations, DomainsOperations, @@ -54,12 +55,16 @@ ReservedIPv6ActionsOperations, ReservedIPv6Operations, SecurityOperations, + SessionsOperations, SizesOperations, SnapshotsOperations, SpacesKeyOperations, SshKeysOperations, TagsOperations, + ToolbeltsOperations, + ToolsOperations, UptimeOperations, + UsersOperations, VectorDatabasesOperations, VolumeActionsOperations, VolumeSnapshotsOperations, @@ -77,6 +82,16 @@ class GeneratedClient: # pylint: disable=client-accepts-api-version-keyword,too-many-instance-attributes """GeneratedClient. + :ivar tools: ToolsOperations operations + :vartype tools: pydo.aio.operations.ToolsOperations + :ivar toolbelts: ToolbeltsOperations operations + :vartype toolbelts: pydo.aio.operations.ToolbeltsOperations + :ivar connections: ConnectionsOperations operations + :vartype connections: pydo.aio.operations.ConnectionsOperations + :ivar users: UsersOperations operations + :vartype users: pydo.aio.operations.UsersOperations + :ivar sessions: SessionsOperations operations + :vartype sessions: pydo.aio.operations.SessionsOperations :ivar one_clicks: OneClicksOperations operations :vartype one_clicks: pydo.aio.operations.OneClicksOperations :ivar account: AccountOperations operations @@ -211,11 +226,9 @@ def __init__( self._config.custom_hook_policy, self._config.logging_policy, policies.DistributedTracingPolicy(**kwargs), - ( - policies.SensitiveHeaderCleanupPolicy(**kwargs) - if self._config.redirect_policy - else None - ), + policies.SensitiveHeaderCleanupPolicy(**kwargs) + if self._config.redirect_policy + else None, self._config.http_logging_policy, ] self._client: AsyncPipelineClient = AsyncPipelineClient( @@ -225,6 +238,21 @@ def __init__( self._serialize = Serializer() self._deserialize = Deserializer() self._serialize.client_side_validation = False + self.tools = ToolsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.toolbelts = ToolbeltsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.connections = ConnectionsOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.users = UsersOperations( + self._client, self._config, self._serialize, self._deserialize + ) + self.sessions = SessionsOperations( + self._client, self._config, self._serialize, self._deserialize + ) self.one_clicks = OneClicksOperations( self._client, self._config, self._serialize, self._deserialize ) diff --git a/src/pydo/aio/_patch.py b/src/pydo/aio/_patch.py index 1d317f97..4299faf0 100644 --- a/src/pydo/aio/_patch.py +++ b/src/pydo/aio/_patch.py @@ -6,6 +6,7 @@ Follow our quickstart for examples: https://aka.ms/azsdk/python/dpcodegen/python/customize """ + from typing import TYPE_CHECKING, Optional from azure.core.credentials import AccessToken @@ -64,6 +65,13 @@ class Client( # type: ignore subdomain (e.g. ``"https://.agents.do-ai.run"``). Required only when using agent inference endpoints. :paramtype agent_endpoint: str + :keyword gateway_endpoint: Action Gateway base URL (default + ``https://actions.do-ai.run``; preview is + ``https://actions.do-ai-test.run``; override via + ``PYDO_GATEWAY_ENDPOINT``). + :keyword gateway_provider: Provider that formats gateway tools for an + inference surface (default :class:`ChatCompletionsProvider`; also + ``MessagesProvider`` and ``ResponsesProvider`` in ``pydo.gateway``). """ def __init__( @@ -74,6 +82,8 @@ def __init__( timeout: int = 120, inference_endpoint: str = INFERENCE_BASE_URL, agent_endpoint: str = "", + gateway_endpoint: Optional[str] = None, + gateway_provider=None, **kwargs, ): if token is not None and api_key is not None: @@ -117,6 +127,17 @@ def __init__( self.images.generate = inference_images.generate self.images.generations = inference_images.generations + try: + from pydo.aio.gateway import AsyncGatewayResources + except ImportError: + self.gateway = None + else: + self.gateway = AsyncGatewayResources( + self, + gateway_endpoint=gateway_endpoint, + provider=gateway_provider, + ) + def _setup_inference_routing( self, inference_endpoint: str, diff --git a/src/pydo/aio/gateway/__init__.py b/src/pydo/aio/gateway/__init__.py new file mode 100644 index 00000000..1bc0b53b --- /dev/null +++ b/src/pydo/aio/gateway/__init__.py @@ -0,0 +1,99 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +# pylint: disable=duplicate-code +"""Async Action Gateway API — hand-written; preserved across ``make generate``.""" + +from __future__ import annotations + +from typing import Any, List, Optional, Sequence + +from pydo.gateway.custom_models import ToolCall +from pydo.gateway.providers import BaseProvider, default_provider +from pydo.gateway import resolve_gateway_base_url + +from .custom_operations import ( + AsyncCodeOperations, + AsyncGatewayTransport, + AsyncMCPTransport, + AsyncRESTTransport, + AsyncToolsOperations, + async_execute_tool_calls, +) +from .session import AsyncSession, AsyncSessionsOperations + + +class AsyncGatewayResources: + """Async Action Gateway surface attached at ``client.gateway``.""" + + def __init__( + self, + parent_client: Any, + *, + gateway_endpoint: Optional[str] = None, + provider: Optional[BaseProvider] = None, + transport: Optional[AsyncGatewayTransport] = None, + ): + self._parent = parent_client + self._gateway_base_url = resolve_gateway_base_url(gateway_endpoint) + self.provider = provider or default_provider() + self.sessions = AsyncSessionsOperations( + parent_client, + gateway_endpoint=gateway_endpoint, + provider=self.provider, + ) + self._transport = transport + if transport is not None: + self.tools = AsyncToolsOperations(transport, self.provider) + self.code = AsyncCodeOperations(transport) + else: + self.tools = None + self.code = None + + @property + def base_url(self) -> str: + return self._gateway_base_url + + async def handle_tool_calls( + self, + response: Any, + *, + rationale: Optional[str] = None, + ) -> List[Any]: + if self.tools is None: + raise RuntimeError( + "create a session first: session = await client.sessions.create(" + "actor_id=...); then await session.handle_tool_calls(response)" + ) + calls = self.provider.extract_tool_calls(response) + if not calls: + return [] + results = await async_execute_tool_calls(calls, self.tools, rationale=rationale) + return self.provider.format_tool_results(calls, results) + + async def execute_tool_calls( + self, + calls: Sequence[ToolCall], + *, + rationale: Optional[str] = None, + ) -> List[Any]: + if self.tools is None: + raise RuntimeError( + "create a session first via await client.sessions.create(" + "actor_id=...)" + ) + return await async_execute_tool_calls(calls, self.tools, rationale=rationale) + + +__all__ = [ + "AsyncGatewayResources", + "AsyncSession", + "AsyncSessionsOperations", + "AsyncGatewayTransport", + "AsyncMCPTransport", + "AsyncRESTTransport", + "AsyncToolsOperations", + "AsyncCodeOperations", + "async_execute_tool_calls", +] diff --git a/src/pydo/aio/gateway/custom_operations.py b/src/pydo/aio/gateway/custom_operations.py new file mode 100644 index 00000000..aa7634a0 --- /dev/null +++ b/src/pydo/aio/gateway/custom_operations.py @@ -0,0 +1,430 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +# pylint: disable=duplicate-code +"""Async Action Gateway operations (mirror of :mod:`pydo.gateway`).""" + +from __future__ import annotations + +import itertools +from typing import Any, Dict, List, Optional, Sequence, Union +from urllib.parse import quote, urlsplit + +from azure.core.rest import HttpRequest + +from pydo.custom_extensions import _wrap +from pydo.gateway.custom_models import ( + META_CODE, + META_INVOKE, + META_SEARCH, + META_TOOL_NAMES, + GatewayToolError, +) +from pydo.gateway.custom_operations import ( + QueryInput, + ToolSpecInput, + _normalize_invoke_entry, + _normalize_queries, + _normalize_tool_specs, + _result_output_or_raise, + _flatten_search_results, + _tool_name, + normalize_invoke_arguments, +) +from pydo.gateway.providers import _error_payload, _get +from pydo.gateway.transport import ( + ACTOR_ID_HEADER, + SESSION_ID_HEADER, + _MCP_HEADERS, + _MCP_META_PATH, + _MCP_PATH, + _META_TOOL_DEFINITIONS, + _REST_CODE_PATH, + _REST_HEADERS, + _REST_INVOKE_PATH, + _REST_SEARCH_PATH, + _REST_TOOLS_PATH, + _external_session_id, + _parse_json_body, + _parse_jsonrpc, + _raise_gateway_http_error, + _unwrap_call_result, + _unwrap_tool_result, +) + + +class AsyncGatewayTransport: + """Async counterpart of :class:`pydo.gateway.transport.GatewayTransport`.""" + + async def list_tools(self, *, meta: bool) -> List[Any]: + raise NotImplementedError + + async def call_tool( + self, name: str, arguments: Dict[str, Any], *, meta: bool + ) -> Any: + raise NotImplementedError + + async def decide_approval(self, approval_id: str, decision: str) -> Any: + raise NotImplementedError + + async def approve(self, approval_id: str) -> Any: + return await self.decide_approval(approval_id, "approve") + + +class AsyncMCPTransport(AsyncGatewayTransport): + """Async JSON-RPC 2.0 over plain HTTP POST to ``/mcp`` and ``/mcp/meta``.""" + + def __init__( + self, + base_url_proxy: Any, + *, + session_id: Optional[str] = None, + actor_id: str, + endpoint_url: Optional[str] = None, + ): + if not actor_id or not str(actor_id).strip(): + raise ValueError("actor_id is required for AsyncMCPTransport") + self._client = base_url_proxy + self._ids = itertools.count(1) + self.session_id = _external_session_id(session_id) if session_id else None + self.actor_id = str(actor_id).strip() + self.endpoint_url = endpoint_url + + def _headers(self) -> Dict[str, str]: + headers = dict(_MCP_HEADERS) + if self.session_id: + headers[SESSION_ID_HEADER] = self.session_id + headers[ACTOR_ID_HEADER] = self.actor_id + return headers + + async def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]: + if self.endpoint_url: + path = self.endpoint_url + request = HttpRequest( + "POST", + path, + headers=self._headers(), + json=payload, + ) + request.url = self._client.format_url(request.url) + pipeline_response = await self._client._pipeline.run(request) + response = pipeline_response.http_response + body = await response.read() + if response.status_code != 200: + _raise_gateway_http_error(response) + return _parse_jsonrpc(body) + + async def _rpc( + self, + method: str, + params: Optional[Dict[str, Any]] = None, + *, + meta: bool, + ) -> Dict[str, Any]: + payload: Dict[str, Any] = { + "jsonrpc": "2.0", + "id": next(self._ids), + "method": method, + } + if params is not None: + payload["params"] = params + return await self._post(_MCP_META_PATH if meta else _MCP_PATH, payload) + + async def list_tools(self, *, meta: bool) -> List[Any]: + result = await self._rpc("tools/list", meta=meta) + return _wrap(result.get("tools") or []) + + async def call_tool( + self, name: str, arguments: Dict[str, Any], *, meta: bool + ) -> Any: + result = await self._rpc( + "tools/call", + {"name": name, "arguments": arguments or {}}, + meta=meta, + ) + return _unwrap_call_result(result) + + async def decide_approval(self, approval_id: str, decision: str) -> Any: + if not approval_id or not str(approval_id).strip(): + raise ValueError("approval_id is required") + if decision not in ("approve", "deny"): + raise ValueError("decision must be 'approve' or 'deny'") + endpoint = urlsplit(self.endpoint_url or self._client._base_url) + approval_id = quote(str(approval_id).strip(), safe="") + url = f"{endpoint.scheme}://{endpoint.netloc}/approvals/{approval_id}" + request = HttpRequest( + "POST", + url, + headers={**self._headers(), "Accept": "application/json"}, + json={"decision": decision}, + ) + pipeline_response = await self._client._pipeline.run(request) + response = pipeline_response.http_response + body = await response.read() + if response.status_code not in (200, 201, 202, 204): + _raise_gateway_http_error(response) + return _wrap(_parse_json_body(body)) if body else None + + +class AsyncRESTTransport(AsyncGatewayTransport): + """Async REST transport; requires ``session_id`` via ``X-Session-Id``.""" + + def __init__(self, base_url_proxy: Any, *, session_id: str, actor_id: str): + if not session_id: + raise ValueError("session_id is required for AsyncRESTTransport") + if not actor_id or not str(actor_id).strip(): + raise ValueError("actor_id is required for AsyncRESTTransport") + self._client = base_url_proxy + self.session_id = _external_session_id(session_id) + self.actor_id = str(actor_id).strip() + + def _headers(self) -> Dict[str, str]: + headers = dict(_REST_HEADERS) + headers[SESSION_ID_HEADER] = self.session_id + headers[ACTOR_ID_HEADER] = self.actor_id + return headers + + async def _request( + self, + method: str, + path: str, + payload: Optional[Dict[str, Any]] = None, + ) -> Any: + kwargs: Dict[str, Any] = {"headers": self._headers()} + if payload is not None: + kwargs["json"] = payload + request = HttpRequest(method, path, **kwargs) + request.url = self._client.format_url(request.url) + pipeline_response = await self._client._pipeline.run(request) + response = pipeline_response.http_response + body = await response.read() + if response.status_code != 200: + _raise_gateway_http_error(response) + return _parse_json_body(body) + + async def list_tools(self, *, meta: bool) -> List[Any]: + if meta: + return _wrap([dict(tool) for tool in _META_TOOL_DEFINITIONS]) + catalog = await self._request("GET", _REST_TOOLS_PATH) + if isinstance(catalog, dict): + return _wrap(catalog.get("tools") or []) + return _wrap(catalog or []) + + async def call_tool( + self, name: str, arguments: Dict[str, Any], *, meta: bool + ) -> Any: + arguments = arguments or {} + if name == META_SEARCH: + return _unwrap_tool_result( + await self._request("POST", _REST_SEARCH_PATH, arguments) + ) + if name == META_INVOKE: + return _wrap(await self._request("POST", _REST_INVOKE_PATH, arguments)) + if name == META_CODE: + return _unwrap_tool_result( + await self._request("POST", _REST_CODE_PATH, arguments) + ) + envelope = await self._request( + "POST", + _REST_INVOKE_PATH, + {"tools": [{"tool": name, "arguments": arguments}]}, + ) + results = (envelope or {}).get("results") or [] + if not results: + raise GatewayToolError(f"invoke of {name!r} returned no results") + item = results[0] + item_result = item.get("result") if isinstance(item, dict) else item + return _unwrap_tool_result(item_result) + + async def decide_approval(self, approval_id: str, decision: str) -> Any: + if not approval_id or not str(approval_id).strip(): + raise ValueError("approval_id is required") + if decision not in ("approve", "deny"): + raise ValueError("decision must be 'approve' or 'deny'") + approval_id = quote(str(approval_id).strip(), safe="") + return await self._request( + "POST", + f"/approvals/{approval_id}", + {"decision": decision}, + ) + + +class AsyncToolsOperations: + """Async Action Gateway tool discovery and invocation.""" + + def __init__(self, transport: AsyncGatewayTransport, provider: Any = None): + self._transport = transport + self._provider = provider + + async def list(self, *, include_all: bool = False) -> Any: + return await self._transport.list_tools(meta=not include_all) + + async def search( + self, + queries: Union[QueryInput, Sequence[QueryInput]], + *, + providers: Optional[Sequence[str]] = None, + tags: Optional[Sequence[str]] = None, + limit: Optional[int] = None, + ) -> Any: + arguments: Dict[str, Any] = {"queries": _normalize_queries(queries)} + if providers: + arguments["providers"] = list(providers) + if tags: + arguments["tags"] = list(tags) + if limit is not None: + arguments["limit"] = limit + return await self._transport.call_tool(META_SEARCH, arguments, meta=True) + + async def invoke( + self, + tools: Sequence[ToolSpecInput], + *, + rationale: Optional[str] = None, + ) -> Any: + arguments: Dict[str, Any] = {"tools": _normalize_tool_specs(tools)} + if rationale: + arguments["rationale"] = rationale + return await self._transport.call_tool(META_INVOKE, arguments, meta=True) + + async def invoke_one( + self, + name: str, + arguments: Optional[Dict[str, Any]] = None, + *, + rationale: Optional[str] = None, + ) -> Any: + envelope = await self.invoke( + [{"tool": name, "arguments": arguments or {}}], + rationale=rationale, + ) + get = getattr(envelope, "get", None) + results = (get("results") if get else None) or [] + if not results: + raise GatewayToolError(f"invoke of {name!r} returned no results") + first = results[0] + item_result = (getattr(first, "get", lambda *_: first)("result")) or first + return _result_output_or_raise(item_result, name) + + async def call(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Any: + return await self._transport.call_tool(name, arguments or {}, meta=False) + + async def __call__( + self, + *, + include_all: bool = False, + names: Optional[Sequence[str]] = None, + search: Optional[Union[QueryInput, Sequence[QueryInput]]] = None, + providers: Optional[Sequence[str]] = None, + tags: Optional[Sequence[str]] = None, + limit: Optional[int] = None, + ) -> List[Any]: + if self._provider is None: + raise RuntimeError( + "no gateway provider configured; pass gateway_provider= to " + "Client() or use tools.list()/tools.invoke() directly" + ) + if search is not None: + payload = await self.search( + search, providers=providers, tags=tags, limit=limit + ) + catalog: List[Any] = _flatten_search_results(payload) + else: + wants_concrete = include_all or bool(names) + tools = await self.list(include_all=wants_concrete) + if names: + wanted = set(names) + tools = [t for t in tools if _tool_name(t) in wanted] + missing = wanted - {_tool_name(t) for t in tools} + if missing: + raise LookupError(f"tools not found in catalog: {sorted(missing)}") + catalog = list(tools) + return self._provider.wrap_tools(catalog) + + +class AsyncCodeOperations: + """Async ephemeral Python sandbox execution (``action.code``).""" + + def __init__(self, transport: AsyncGatewayTransport): + self._transport = transport + + async def execute(self, code: str, *, thought: Optional[str] = None) -> Any: + if not code or not code.strip(): + raise ValueError("code is empty") + arguments: Dict[str, Any] = {"code": code} + if thought: + arguments["thought"] = thought + return await self._transport.call_tool(META_CODE, arguments, meta=True) + + +async def async_execute_tool_calls( + calls: Sequence[Any], + tools_operations: AsyncToolsOperations, + *, + rationale: Optional[str] = None, +) -> List[Any]: + """Async twin of :func:`pydo.gateway.providers.execute_tool_calls`.""" + results: List[Any] = [None] * len(calls) + concrete: List[int] = [] + + for index, call in enumerate(calls): + if call.name in META_TOOL_NAMES: + try: + arguments = call.arguments + if call.name == META_INVOKE: + arguments = normalize_invoke_arguments(arguments) + results[index] = await tools_operations._transport.call_tool( + call.name, arguments, meta=True + ) + except (GatewayToolError, TypeError, ValueError) as exc: + results[index] = _error_payload(exc) + else: + concrete.append(index) + + if concrete: + try: + batch = [ + _normalize_invoke_entry( + {"tool": calls[i].name, "arguments": calls[i].arguments} + ) + for i in concrete + ] + except (TypeError, ValueError) as exc: + error = _error_payload(exc) + for index in concrete: + results[index] = error + return results + envelope = await tools_operations.invoke(batch, rationale=rationale) + items = (_get(envelope, "results") or []) if envelope is not None else [] + for position, index in enumerate(concrete): + if position < len(items): + item = items[position] + item_result = _get(item, "result") or item + status = _get(item_result, "status") + if status and status != "succeeded": + error_result = { + "error": _get(item_result, "error") + or {"message": f"tool {calls[index].name!r} failed"} + } + meta = _get(item_result, "_meta") + if meta: + error_result["_meta"] = meta + results[index] = error_result + else: + results[index] = _get(item_result, "output") + else: + results[index] = { + "error": {"message": "no result returned for this tool call"} + } + return results + + +__all__ = [ + "AsyncGatewayTransport", + "AsyncMCPTransport", + "AsyncRESTTransport", + "AsyncToolsOperations", + "AsyncCodeOperations", + "async_execute_tool_calls", +] diff --git a/src/pydo/aio/gateway/session.py b/src/pydo/aio/gateway/session.py new file mode 100644 index 00000000..dbaee9fa --- /dev/null +++ b/src/pydo/aio/gateway/session.py @@ -0,0 +1,199 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +# pylint: disable=duplicate-code +"""Async Action Gateway sessions.""" + +from __future__ import annotations + +import uuid +from typing import Any, Dict, List, Optional, Sequence + +from pydo.custom_extensions import _BaseURLProxy +from pydo.gateway.custom_models import GatewayProtocolError +from pydo.gateway.providers import BaseProvider, default_provider +from pydo.gateway.session import normalize_permissions +from pydo.gateway.transport import ( + resolve_gateway_base_url, +) + +from .custom_operations import ( + AsyncCodeOperations, + AsyncMCPTransport, + AsyncToolsOperations, + async_execute_tool_calls, +) + + +def _pick(data: Dict[str, Any], *keys: str) -> Any: + for key in keys: + if key in data and data[key] is not None: + return data[key] + return None + + +class AsyncSession: + """Async twin of :class:`pydo.gateway.session.Session`.""" + + def __init__( + self, + *, + session_urn: str, + actor_id: str, + name: str, + policy: Dict[str, Any], + mcp_url: str, + tools: AsyncToolsOperations, + code: AsyncCodeOperations, + provider: BaseProvider, + selected_tools: Optional[Sequence[str]] = None, + raw: Optional[Dict[str, Any]] = None, + ): + self.session_urn = session_urn + self.id = session_urn + self.actor_id = actor_id + self.name = name + self.policy = policy + self._mcp_url = mcp_url + self.tools = tools + self.code = code + self._transport = tools._transport + self.provider = provider + self.selected_tools = list(selected_tools or []) + self.raw = raw or {} + + @property + def url(self) -> str: + return self._mcp_url + + async def handle_tool_calls( + self, + response: Any, + *, + rationale: Optional[str] = None, + ) -> List[Any]: + calls = self.provider.extract_tool_calls(response) + if not calls: + return [] + results = await async_execute_tool_calls(calls, self.tools, rationale=rationale) + return self.provider.format_tool_results(calls, results) + + async def execute_tool_calls( + self, + calls: Sequence[Any], + *, + rationale: Optional[str] = None, + ) -> List[Any]: + return await async_execute_tool_calls(calls, self.tools, rationale=rationale) + + async def approve(self, approval_id: str) -> Any: + """Approve a pending tool invocation for this session.""" + return await self._transport.decide_approval(approval_id, "approve") + + async def deny(self, approval_id: str) -> Any: + """Deny a pending tool invocation for this session.""" + return await self._transport.decide_approval(approval_id, "deny") + + def __repr__(self) -> str: # pragma: no cover + return f"" + + +class AsyncSessionsOperations: + """Create sessions through the generated async Action Gateway operation.""" + + def __init__( + self, + parent_client: Any, + *, + gateway_endpoint: Optional[str] = None, + provider: Optional[BaseProvider] = None, + ): + self._parent = parent_client + self._sessions_api = parent_client.sessions + self._gateway_base_url = resolve_gateway_base_url(gateway_endpoint) + self._provider = provider or default_provider() + + async def create( + self, + actor_id: str, + *, + name: Optional[str] = None, + permissions: Optional[Dict[str, Any]] = None, + tools: Optional[Sequence[str]] = None, + config: Optional[Dict[str, Any]] = None, + ) -> AsyncSession: + if not actor_id or not str(actor_id).strip(): + raise ValueError("actor_id is required") + + session_name = name or f"pydo-session-{uuid.uuid4().hex[:8]}" + policy = normalize_permissions(permissions) + body = { + "name": session_name, + "policy": policy, + "actor_id": str(actor_id).strip(), + } + if tools is not None: + if isinstance(tools, (str, bytes)): + raise TypeError("tools must be a sequence of tool references") + body["tools"] = list(tools) + if config is not None: + if not isinstance(config, dict): + raise TypeError("config must be a dict") + body["config"] = config + + raw_session = await self._post_create(body) + session_urn = _pick(raw_session, "sessionUrn", "session_urn") + if not session_urn: + raise GatewayProtocolError( + f"session create response missing sessionUrn: {raw_session!r}" + ) + + mcp_url = _pick(raw_session, "mcpUrl", "mcp_url") + if not mcp_url: + raise GatewayProtocolError( + f"session create response missing mcpUrl: {raw_session!r}" + ) + + transport = AsyncMCPTransport( + _BaseURLProxy(self._parent._client, self._gateway_base_url), + session_id=session_urn, + actor_id=actor_id, + endpoint_url=mcp_url, + ) + tools = AsyncToolsOperations(transport, self._provider) + code = AsyncCodeOperations(transport) + return AsyncSession( + session_urn=session_urn, + actor_id=str(actor_id).strip(), + name=_pick(raw_session, "name") or session_name, + policy=policy, + mcp_url=mcp_url, + tools=tools, + code=code, + provider=self._provider, + selected_tools=_pick(raw_session, "selectedTools") or [], + raw=raw_session, + ) + + async def _post_create(self, body: Dict[str, Any]) -> Dict[str, Any]: + payload = await self._sessions_api.create(body=body) + if not isinstance(payload, dict): + raise GatewayProtocolError( + f"unexpected session create response: {payload!r}" + ) + session = payload.get("session") + if not isinstance(session, dict): + raise GatewayProtocolError( + f"session create response missing session object: {payload!r}" + ) + result = dict(session) + mcp_url = _pick(payload, "mcpUrl", "mcp_url") + if mcp_url: + result["mcpUrl"] = mcp_url + if "tools" in payload: + result["selectedTools"] = payload["tools"] + return result + + +__all__ = ["AsyncSession", "AsyncSessionsOperations"] diff --git a/src/pydo/aio/operations/__init__.py b/src/pydo/aio/operations/__init__.py index 2de68fec..3960eadb 100644 --- a/src/pydo/aio/operations/__init__.py +++ b/src/pydo/aio/operations/__init__.py @@ -4,6 +4,11 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +from ._operations import ToolsOperations +from ._operations import ToolbeltsOperations +from ._operations import ConnectionsOperations +from ._operations import UsersOperations +from ._operations import SessionsOperations from ._operations import OneClicksOperations from ._operations import AccountOperations from ._operations import SshKeysOperations @@ -63,6 +68,11 @@ from ._patch import patch_sdk as _patch_sdk __all__ = [ + "ToolsOperations", + "ToolbeltsOperations", + "ConnectionsOperations", + "UsersOperations", + "SessionsOperations", "OneClicksOperations", "AccountOperations", "SshKeysOperations", diff --git a/src/pydo/aio/operations/_operations.py b/src/pydo/aio/operations/_operations.py index 120e722d..c9c03aff 100644 --- a/src/pydo/aio/operations/_operations.py +++ b/src/pydo/aio/operations/_operations.py @@ -113,6 +113,11 @@ build_certificates_delete_request, build_certificates_get_request, build_certificates_list_request, + build_connections_create_request, + build_connections_delete_request, + build_connections_get_request, + build_connections_list_request, + build_connections_update_request, build_databases_add_connection_pool_request, build_databases_add_request, build_databases_add_user_request, @@ -617,6 +622,9 @@ build_security_post_restore_secret_request, build_security_update_secret_request, build_security_update_settings_plan_request, + build_sessions_create_request, + build_sessions_delete_request, + build_sessions_list_request, build_sizes_list_request, build_snapshots_delete_request, build_snapshots_get_request, @@ -638,6 +646,16 @@ build_tags_get_request, build_tags_list_request, build_tags_unassign_resources_request, + build_toolbelts_add_tools_request, + build_toolbelts_create_request, + build_toolbelts_delete_request, + build_toolbelts_delete_tools_request, + build_toolbelts_get_request, + build_toolbelts_list_request, + build_tools_get_definition_request, + build_tools_list_providers_request, + build_tools_list_request, + build_tools_list_toolkits_request, build_uptime_create_alert_request, build_uptime_create_check_request, build_uptime_delete_alert_request, @@ -649,6 +667,8 @@ build_uptime_list_checks_request, build_uptime_update_alert_request, build_uptime_update_check_request, + build_users_get_request, + build_users_list_request, build_vector_databases_create_request, build_vector_databases_delete_request, build_vector_databases_get_credentials_request, @@ -706,6 +726,4615 @@ ] +class ToolsOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.aio.GeneratedClient`'s + :attr:`tools` attribute. + """ + + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace_async + async def list( + self, + *, + toolkit_id: Optional[str] = None, + page: int = 1, + per_page: int = 20, + **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """List Tools. + + Lists active Action Gateway tools visible to the authenticated team. + + :keyword toolkit_id: Filter tools by toolkit identifier. Default value is None. + :paramtype toolkit_id: str + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "definitions": [ + { + "annotations": { + "destructiveHint": bool, # Optional. + "idempotentHint": bool, # Optional. + "openWorldHint": bool, # Optional. + "readOnlyHint": bool, # Optional. + "title": "str" # Optional. + }, + "auth": { + "baseUrlResolution": { + "httpLookup": { + "baseUrlTemplate": "str", # + Optional. HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "caseInsensitive": bool, # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "extractField": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "matchField": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "matchValue": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "match_value_parameter": "str", # + Optional. HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "method": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "requiredScopes": [ + "str" # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access + token), selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When + both are empty, exactly one entry whose own "scopes" + array contains required_scopes must exist. Configuring + only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + ], + "trimTrailingSlash": bool, # + Optional. HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "url": "str" # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + } + }, + "credentialBinding": "str", # Optional. + "credentialRefSource": "str", # Optional. + "doManagedCredentialRef": "str", # Optional. + "injection": { + "location": "str", # Optional. + "name": "str", # Optional. + "scheme": "str" # Optional. + }, + "modes": [ + "str" # Optional. + ], + "provider": "str", # Optional. + "scopes": [ + "str" # Optional. + ] + }, + "classification": { + "dataClasses": [ + "str" # Optional. + ], + "operation": "str", # Optional. + "risk": "str" # Optional. + }, + "description": "str", # Optional. + "execution": { + "adapterVersion": "str", # Optional. + "configRef": "str", # Optional. + "http": { + "allowedHosts": [ + "str" # Optional. + ], + "baseUrl": "str", # Optional. + "method": "str", # Optional. + "path": "str", # Optional. + "requestEncoding": "str", # Optional. + "responseFormat": "str" # Optional. + }, + "mcp": { + "allowedHosts": [ + "str" # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote + MCP server (as opposed to a plain HTTP endpoint). endpoint is + the remote MCP server's URL, tool_name is the name the remote + server expects on tools/call (may differ from this tool's + registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and + server_ref is an opaque label identifying the remote server + for logging/metrics/allowlisting. + ], + "endpoint": "str", # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote MCP + server (as opposed to a plain HTTP endpoint). endpoint is the + remote MCP server's URL, tool_name is the name the remote server + expects on tools/call (may differ from this tool's registry + name), transport selects the wire protocol ("streamable_http" is + the only kind implemented today), and server_ref is an opaque + label identifying the remote server for + logging/metrics/allowlisting. + "serverRef": "str", # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote MCP + server (as opposed to a plain HTTP endpoint). endpoint is the + remote MCP server's URL, tool_name is the name the remote server + expects on tools/call (may differ from this tool's registry + name), transport selects the wire protocol ("streamable_http" is + the only kind implemented today), and server_ref is an opaque + label identifying the remote server for + logging/metrics/allowlisting. + "toolName": "str", # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote MCP + server (as opposed to a plain HTTP endpoint). endpoint is the + remote MCP server's URL, tool_name is the name the remote server + expects on tools/call (may differ from this tool's registry + name), transport selects the wire protocol ("streamable_http" is + the only kind implemented today), and server_ref is an opaque + label identifying the remote server for + logging/metrics/allowlisting. + "transport": "str" # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote MCP + server (as opposed to a plain HTTP endpoint). endpoint is the + remote MCP server's URL, tool_name is the name the remote server + expects on tools/call (may differ from this tool's registry + name), transport selects the wire protocol ("streamable_http" is + the only kind implemented today), and server_ref is an opaque + label identifying the remote server for + logging/metrics/allowlisting. + }, + "type": "str" # Optional. + }, + "flipperName": "str", # Optional. + "hooks": { + "usage": { + "billable": bool, # Optional. When usage + metadata is present, false prevents billing. Omitting usage + metadata leaves consumers' legacy billing classification + unchanged. + "meters": [ + { + "quantitySource": "str", # + Optional. + "sku": "str", # Optional. + "unit": "str" # Optional. + } + ] + } + }, + "inputSchema": {}, # Optional. Any object. + "name": "str", # Optional. + "outputSchema": {}, # Optional. Any object. + "parallelizable": bool, # Optional. + "policy": { + "permission": "str" # Optional. + }, + "reliability": { + "maxOutputBytes": "str", # Optional. + "retry": { + "backoff": "str", # Optional. + "maxAttempts": 0, # Optional. + "retryOn": [ + "str" # Optional. + ] + }, + "timeoutMs": 0 # Optional. + }, + "schemaVersion": "str", # Optional. + "status": "str", # Optional. + "streamingSafe": bool, # Optional. + "tags": [ + "str" # Optional. + ], + "title": "str", # Optional. + "toolId": "str", # Optional. + "toolSlug": "str", # Optional. tool_slug is the + provider-qualified, stable tool identifier + ":code:``_:code:``". Pass this value back verbatim to + the toolbelt add/remove endpoints; clients should treat it as opaque. + "toolkitId": "str", # Optional. + "transform": { + "input": {}, # Optional. Any object. + "language": "str", # Optional. + "output": {} # Optional. Any object. + }, + "version": "str" # Optional. + } + ], + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + }, + "tools": [ + { + "annotations": { + "destructiveHint": bool, # Optional. + "idempotentHint": bool, # Optional. + "openWorldHint": bool, # Optional. + "readOnlyHint": bool, # Optional. + "title": "str" # Optional. + }, + "description": "str", # Optional. + "inputSchema": {}, # Optional. Any object. + "name": "str", # Optional. + "outputSchema": {}, # Optional. Any object. + "parallelizable": bool, # Optional. + "streamingSafe": bool, # Optional. + "title": "str", # Optional. + "toolSlug": "str", # Optional. tool_slug is the + provider-qualified, stable tool identifier + ":code:``_:code:``". Pass this value back verbatim to + the toolbelt add/remove endpoints; clients should treat it as opaque + rather than reconstructing it from toolkit_id and name. + "toolkitId": "str", # Optional. + "version": "str" # Optional. + } + ], + "version": "str" # Optional. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_tools_list_request( + toolkit_id=toolkit_id, + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace_async + async def list_toolkits(self, **kwargs: Any) -> JSON: + """List Toolkits. + + Lists the toolkits that group Action Gateway tools. + + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolkits": [ + { + "description": "str", # Optional. + "id": "str", # Optional. + "name": "str" # Optional. + } + ], + "version": "str" # Optional. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_tools_list_toolkits_request( + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace_async + async def list_providers(self, **kwargs: Any) -> JSON: + """List Tool Providers. + + Lists Action Gateway providers and their connection requirements. + + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "providers": [ + { + "auth_type": "str", # Optional. + "connection_parameters": [ + { + "allowed_host_suffixes": [ + "str" # Optional. + ], + "allowed_values": [ + "str" # Optional. + ], + "description": "str", # Optional. + "input_kind": "str", # Optional. + "key": "str", # Optional. + "label": "str", # Optional. + "max_length": 0, # Optional. + "normalization": "str", # Optional. + "required": bool # Optional. + } + ], + "description": "str", # Optional. + "display_name": "str", # Optional. + "name": "str", # Optional. + "scopes": [ + "str" # Optional. + ] + } + ] + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_tools_list_providers_request( + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace_async + async def get_definition( + self, + name: str, + *, + version: Optional[str] = None, + toolkit_id: Optional[str] = None, + **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Retrieve a Tool Definition. + + Retrieves the executable definition for an active Action Gateway tool. + + :param name: The provider-qualified tool name. Required. + :type name: str + :keyword version: The tool version. Omit to retrieve the current version. Default value is + None. + :paramtype version: str + :keyword toolkit_id: The toolkit identifier used to disambiguate a bare tool name. Default + value is None. + :paramtype toolkit_id: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "annotations": { + "destructiveHint": bool, # Optional. + "idempotentHint": bool, # Optional. + "openWorldHint": bool, # Optional. + "readOnlyHint": bool, # Optional. + "title": "str" # Optional. + }, + "auth": { + "baseUrlResolution": { + "httpLookup": { + "baseUrlTemplate": "str", # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "caseInsensitive": bool, # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "extractField": "str", # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "matchField": "str", # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "matchValue": "str", # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "match_value_parameter": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting extract_field from + that entry, and substituting it for "{value}" in base_url_template. + When match_field and match_value are both set, they select the entry. + When both are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one match field + is invalid. Resolution fails fast on zero or multiple compatible + entries. + "method": "str", # Optional. HTTPLookupSpec resolves + a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "requiredScopes": [ + "str" # Optional. HTTPLookupSpec resolves a + base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON + array, extracting extract_field from that entry, and substituting + it for "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both are + empty, exactly one entry whose own "scopes" array contains + required_scopes must exist. Configuring only one match field is + invalid. Resolution fails fast on zero or multiple compatible + entries. + ], + "trimTrailingSlash": bool, # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting extract_field from + that entry, and substituting it for "{value}" in base_url_template. + When match_field and match_value are both set, they select the entry. + When both are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one match field + is invalid. Resolution fails fast on zero or multiple compatible + entries. + "url": "str" # Optional. HTTPLookupSpec resolves a + base_url by calling url (bearer-authenticated with the just-exchanged + access token), selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for "{value}" in + base_url_template. When match_field and match_value are both set, + they select the entry. When both are empty, exactly one entry whose + own "scopes" array contains required_scopes must exist. Configuring + only one match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + } + }, + "credentialBinding": "str", # Optional. + "credentialRefSource": "str", # Optional. + "doManagedCredentialRef": "str", # Optional. + "injection": { + "location": "str", # Optional. + "name": "str", # Optional. + "scheme": "str" # Optional. + }, + "modes": [ + "str" # Optional. + ], + "provider": "str", # Optional. + "scopes": [ + "str" # Optional. + ] + }, + "classification": { + "dataClasses": [ + "str" # Optional. + ], + "operation": "str", # Optional. + "risk": "str" # Optional. + }, + "description": "str", # Optional. + "execution": { + "adapterVersion": "str", # Optional. + "configRef": "str", # Optional. + "http": { + "allowedHosts": [ + "str" # Optional. + ], + "baseUrl": "str", # Optional. + "method": "str", # Optional. + "path": "str", # Optional. + "requestEncoding": "str", # Optional. + "responseFormat": "str" # Optional. + }, + "mcp": { + "allowedHosts": [ + "str" # Optional. MCPExecution describes how to + invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, + tool_name is the name the remote server expects on tools/call (may + differ from this tool's registry name), transport selects the wire + protocol ("streamable_http" is the only kind implemented today), and + server_ref is an opaque label identifying the remote server for + logging/metrics/allowlisting. + ], + "endpoint": "str", # Optional. MCPExecution describes how to + invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, tool_name + is the name the remote server expects on tools/call (may differ from this + tool's registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and server_ref is + an opaque label identifying the remote server for + logging/metrics/allowlisting. + "serverRef": "str", # Optional. MCPExecution describes how + to invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, tool_name + is the name the remote server expects on tools/call (may differ from this + tool's registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and server_ref is + an opaque label identifying the remote server for + logging/metrics/allowlisting. + "toolName": "str", # Optional. MCPExecution describes how to + invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, tool_name + is the name the remote server expects on tools/call (may differ from this + tool's registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and server_ref is + an opaque label identifying the remote server for + logging/metrics/allowlisting. + "transport": "str" # Optional. MCPExecution describes how to + invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, tool_name + is the name the remote server expects on tools/call (may differ from this + tool's registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and server_ref is + an opaque label identifying the remote server for + logging/metrics/allowlisting. + }, + "type": "str" # Optional. + }, + "flipperName": "str", # Optional. + "hooks": { + "usage": { + "billable": bool, # Optional. When usage metadata is + present, false prevents billing. Omitting usage metadata leaves + consumers' legacy billing classification unchanged. + "meters": [ + { + "quantitySource": "str", # Optional. + "sku": "str", # Optional. + "unit": "str" # Optional. + } + ] + } + }, + "inputSchema": {}, # Optional. Any object. + "name": "str", # Optional. + "outputSchema": {}, # Optional. Any object. + "parallelizable": bool, # Optional. + "policy": { + "permission": "str" # Optional. + }, + "reliability": { + "maxOutputBytes": "str", # Optional. + "retry": { + "backoff": "str", # Optional. + "maxAttempts": 0, # Optional. + "retryOn": [ + "str" # Optional. + ] + }, + "timeoutMs": 0 # Optional. + }, + "schemaVersion": "str", # Optional. + "status": "str", # Optional. + "streamingSafe": bool, # Optional. + "tags": [ + "str" # Optional. + ], + "title": "str", # Optional. + "toolId": "str", # Optional. + "toolSlug": "str", # Optional. tool_slug is the provider-qualified, stable + tool identifier ":code:``_:code:``". Pass this value back + verbatim to the toolbelt add/remove endpoints; clients should treat it as opaque. + "toolkitId": "str", # Optional. + "transform": { + "input": {}, # Optional. Any object. + "language": "str", # Optional. + "output": {} # Optional. Any object. + }, + "version": "str" # Optional. + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_tools_get_definition_request( + name=name, + version=version, + toolkit_id=toolkit_id, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + +class ToolbeltsOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.aio.GeneratedClient`'s + :attr:`toolbelts` attribute. + """ + + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace_async + async def list( + self, + *, + status: str = "active", + page: int = 1, + per_page: int = 20, + **kwargs: Any + ) -> JSON: + """List Toolbelts. + + Lists the latest version of each toolbelt owned by the authenticated team. + + :keyword status: Filter toolbelts by status. Known values are: "active", "deprecated", and + "all". Default value is "active". + :paramtype status: str + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + }, + "toolbelts": [ + { + "latest_version": "str", # Required. + "name": "str", # Required. + "reference_latest": "str", # Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "updated_at": "2020-02-20 00:00:00", # Required. + "version_count": 0, # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + ] + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_toolbelts_list_request( + status=status, + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + async def create( + self, body: JSON, *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a Toolbelt. + + Creates a versioned collection of provider-qualified Action Gateway tool names. + + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "name": "str", # Required. + "tools": [ + "str" # Required. + ], + "description": "str", # Optional. + "display_name": "str", # Optional. + "version": "1" # Optional. Default value is "1". + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + async def create( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a Toolbelt. + + Creates a versioned collection of provider-qualified Action Gateway tool names. + + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace_async + async def create(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Create a Toolbelt. + + Creates a versioned collection of provider-qualified Action Gateway tool names. + + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "name": "str", # Required. + "tools": [ + "str" # Required. + ], + "description": "str", # Optional. + "display_name": "str", # Optional. + "version": "1" # Optional. Default value is "1". + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_toolbelts_create_request( + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 409]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 409: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace_async + async def get( + self, name: str, *, version: Optional[str] = None, **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Retrieve a Toolbelt. + + Retrieves the latest active version or a specified immutable version of a toolbelt. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :keyword version: An immutable numeric toolbelt version. Omit to retrieve the latest active + version. Default value is None. + :paramtype version: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_toolbelts_get_request( + name=name, + version=version, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace_async + async def delete(self, name: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Delete a Toolbelt. + + Deprecates the latest active version of a toolbelt. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_toolbelts_delete_request( + name=name, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + async def add_tools( + self, + name: str, + body: JSON, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Add Tools to a Toolbelt. + + Adds provider-qualified tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "tools": [ + "str" # Required. + ] + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + async def add_tools( + self, + name: str, + body: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Add Tools to a Toolbelt. + + Adds provider-qualified tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace_async + async def add_tools( + self, name: str, body: Union[JSON, IO[bytes]], **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Add Tools to a Toolbelt. + + Adds provider-qualified tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "tools": [ + "str" # Required. + ] + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_toolbelts_add_tools_request( + name=name, + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + async def delete_tools( + self, + name: str, + body: JSON, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Remove Tools from a Toolbelt. + + Removes tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "tools": [ + "str" # Required. + ] + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + async def delete_tools( + self, + name: str, + body: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Remove Tools from a Toolbelt. + + Removes tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace_async + async def delete_tools( + self, name: str, body: Union[JSON, IO[bytes]], **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Remove Tools from a Toolbelt. + + Removes tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "tools": [ + "str" # Required. + ] + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_toolbelts_delete_tools_request( + name=name, + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + +class ConnectionsOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.aio.GeneratedClient`'s + :attr:`connections` attribute. + """ + + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace_async + async def list( + self, + *, + provider: Optional[str] = None, + user_id: Optional[str] = None, + status: Optional[str] = None, + sort: Optional[str] = None, + sort_direction: Optional[str] = None, + page: int = 1, + per_page: int = 20, + **kwargs: Any + ) -> JSON: + """List Connections. + + Lists OAuth connections owned by the authenticated team. + + :keyword provider: Filter by provider name. Default value is None. + :paramtype provider: str + :keyword user_id: Filter by end-user identifier. Default value is None. + :paramtype user_id: str + :keyword status: Filter by connection status. Default value is None. + :paramtype status: str + :keyword sort: Field used to sort results. Default value is None. + :paramtype sort: str + :keyword sort_direction: Sort direction. Default value is None. + :paramtype sort_direction: str + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "connections": [ + { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. + "granted_at": "2020-02-20 00:00:00", # Optional. + "id": "str", # Optional. + "provider": "str", # Optional. + "provider_display_name": "str", # Optional. + "revoked_at": "2020-02-20 00:00:00", # Optional. + "scopes": [ + "str" # Optional. + ], + "status": "str", # Optional. + "updated_at": "2020-02-20 00:00:00", # Optional. + "user_id": "str" # Optional. + } + ], + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + } + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_connections_list_request( + provider=provider, + user_id=user_id, + status=status, + sort=sort, + sort_direction=sort_direction, + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + async def create( + self, body: JSON, *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a Connection. + + Creates or begins authorization for an OAuth connection to an Action Gateway provider. + + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "connection_parameters": {}, # Optional. Any object. + "provider": "str", # Optional. + "scopes": [ + "str" # Optional. + ], + "user_id": "str" # Optional. + } + + # response body for status code(s): 200 + response == { + "authorization": { + "connect_url": "str", # Optional. ConnectionAuthorization is present + only while a connection is pending. The UI sends the user to connect_url and + polls GetConnection until the connection becomes active or expires. The + Secrets Manager poll URL is never exposed. + "expires_at": "2020-02-20 00:00:00", # Optional. + ConnectionAuthorization is present only while a connection is pending. The UI + sends the user to connect_url and polls GetConnection until the connection + becomes active or expires. The Secrets Manager poll URL is never exposed. + "status": "str", # Optional. ConnectionAuthorization is present only + while a connection is pending. The UI sends the user to connect_url and polls + GetConnection until the connection becomes active or expires. The Secrets + Manager poll URL is never exposed. + "verification_code": "str" # Optional. ConnectionAuthorization is + present only while a connection is pending. The UI sends the user to + connect_url and polls GetConnection until the connection becomes active or + expires. The Secrets Manager poll URL is never exposed. + }, + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + async def create( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a Connection. + + Creates or begins authorization for an OAuth connection to an Action Gateway provider. + + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "authorization": { + "connect_url": "str", # Optional. ConnectionAuthorization is present + only while a connection is pending. The UI sends the user to connect_url and + polls GetConnection until the connection becomes active or expires. The + Secrets Manager poll URL is never exposed. + "expires_at": "2020-02-20 00:00:00", # Optional. + ConnectionAuthorization is present only while a connection is pending. The UI + sends the user to connect_url and polls GetConnection until the connection + becomes active or expires. The Secrets Manager poll URL is never exposed. + "status": "str", # Optional. ConnectionAuthorization is present only + while a connection is pending. The UI sends the user to connect_url and polls + GetConnection until the connection becomes active or expires. The Secrets + Manager poll URL is never exposed. + "verification_code": "str" # Optional. ConnectionAuthorization is + present only while a connection is pending. The UI sends the user to + connect_url and polls GetConnection until the connection becomes active or + expires. The Secrets Manager poll URL is never exposed. + }, + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace_async + async def create(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Create a Connection. + + Creates or begins authorization for an OAuth connection to an Action Gateway provider. + + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "connection_parameters": {}, # Optional. Any object. + "provider": "str", # Optional. + "scopes": [ + "str" # Optional. + ], + "user_id": "str" # Optional. + } + + # response body for status code(s): 200 + response == { + "authorization": { + "connect_url": "str", # Optional. ConnectionAuthorization is present + only while a connection is pending. The UI sends the user to connect_url and + polls GetConnection until the connection becomes active or expires. The + Secrets Manager poll URL is never exposed. + "expires_at": "2020-02-20 00:00:00", # Optional. + ConnectionAuthorization is present only while a connection is pending. The UI + sends the user to connect_url and polls GetConnection until the connection + becomes active or expires. The Secrets Manager poll URL is never exposed. + "status": "str", # Optional. ConnectionAuthorization is present only + while a connection is pending. The UI sends the user to connect_url and polls + GetConnection until the connection becomes active or expires. The Secrets + Manager poll URL is never exposed. + "verification_code": "str" # Optional. ConnectionAuthorization is + present only while a connection is pending. The UI sends the user to + connect_url and polls GetConnection until the connection becomes active or + expires. The Secrets Manager poll URL is never exposed. + }, + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_connections_create_request( + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 409]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 409: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace_async + async def get(self, id: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Retrieve a Connection. + + Retrieves an OAuth connection owned by the authenticated team. + + :param id: The connection UUID. Required. + :type id: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "authorization": { + "connect_url": "str", # Optional. ConnectionAuthorization is present + only while a connection is pending. The UI sends the user to connect_url and + polls GetConnection until the connection becomes active or expires. The + Secrets Manager poll URL is never exposed. + "expires_at": "2020-02-20 00:00:00", # Optional. + ConnectionAuthorization is present only while a connection is pending. The UI + sends the user to connect_url and polls GetConnection until the connection + becomes active or expires. The Secrets Manager poll URL is never exposed. + "status": "str", # Optional. ConnectionAuthorization is present only + while a connection is pending. The UI sends the user to connect_url and polls + GetConnection until the connection becomes active or expires. The Secrets + Manager poll URL is never exposed. + "verification_code": "str" # Optional. ConnectionAuthorization is + present only while a connection is pending. The UI sends the user to + connect_url and polls GetConnection until the connection becomes active or + expires. The Secrets Manager poll URL is never exposed. + }, + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_connections_get_request( + id=id, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + async def update( + self, + id: str, + body: JSON, + *, + content_type: str = "application/json", + **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Update Connection Parameters. + + Updates non-sensitive connection parameters for an OAuth connection. + + :param id: The connection UUID. Required. + :type id: str + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "connection_parameters": {}, # Optional. Any object. + "id": "str" # Optional. + } + + # response body for status code(s): 200 + response == { + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + async def update( + self, + id: str, + body: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Update Connection Parameters. + + Updates non-sensitive connection parameters for an OAuth connection. + + :param id: The connection UUID. Required. + :type id: str + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace_async + async def update( + self, id: str, body: Union[JSON, IO[bytes]], **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Update Connection Parameters. + + Updates non-sensitive connection parameters for an OAuth connection. + + :param id: The connection UUID. Required. + :type id: str + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "connection_parameters": {}, # Optional. Any object. + "id": "str" # Optional. + } + + # response body for status code(s): 200 + response == { + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_connections_update_request( + id=id, + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace_async + async def delete(self, id: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Delete a Connection. + + Revokes and deletes an OAuth connection owned by the authenticated team. + + :param id: The connection UUID. Required. + :type id: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_connections_delete_request( + id=id, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + +class UsersOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.aio.GeneratedClient`'s + :attr:`users` attribute. + """ + + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace_async + async def list(self, *, page: int = 1, per_page: int = 20, **kwargs: Any) -> JSON: + """List Action Gateway Users. + + Lists end-user identifiers derived from sessions and OAuth connections for the authenticated + team. + + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + }, + "user_ids": [ + "str" # Optional. + ] + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_users_list_request( + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace_async + async def get(self, user_id: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Retrieve an Action Gateway User. + + Retrieves a derived end-user view containing its sessions and OAuth connections. + + :param user_id: The end-user identifier. Required. + :type user_id: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "user": { + "connections": [ + { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "granted_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "id": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "provider": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "provider_display_name": "str", # Optional. User is + a derived, team-scoped view across sessions and OAuth connections. + "revoked_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "scopes": [ + "str" # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + ], + "status": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "updated_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "user_id": "str" # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + } + ], + "sessions": [ + { + "created_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "name": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "session_urn": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "updated_at": "2020-02-20 00:00:00" # Optional. User + is a derived, team-scoped view across sessions and OAuth connections. + } + ], + "user_id": "str" # Optional. User is a derived, team-scoped view + across sessions and OAuth connections. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_users_get_request( + user_id=user_id, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + +class SessionsOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.aio.GeneratedClient`'s + :attr:`sessions` attribute. + """ + + def __init__(self, *args, **kwargs) -> None: + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace_async + async def list( + self, + *, + end_user_id: Optional[str] = None, + page: int = 1, + per_page: int = 20, + **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """List Action Gateway Sessions. + + Lists Action Gateway sessions owned by the authenticated team. + + :keyword end_user_id: Filter sessions by actor identifier. Default value is None. + :paramtype end_user_id: str + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + }, + "sessions": [ + { + "actorId": "str", # Optional. actor_id is empty when the + session is not bound to an actor. + "config": {}, # Optional. Preserved as an opaque object. + Gateway currently interprets config.preloadTools to add selected direct + tools to the session MCP. + "createdAt": "2020-02-20 00:00:00", # Optional. + "name": "str", # Optional. name is the required + human-readable session name. + "policy": { + "defaultAction": "ask", # Optional. Default value is + "ask". SessionPolicyAction is the disposition applied to a tool call. + Lowercase values are canonical so ProtoJSON matches the public REST + vocabulary; the prefixed aliases preserve compatibility for existing + protobuf clients. Known values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default + value is "ask". SessionPolicyAction is the disposition + applied to a tool call. Lowercase values are canonical so + ProtoJSON matches the public REST vocabulary; the prefixed + aliases preserve compatibility for existing protobuf clients. + Known values are: "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. + Dictionary of :code:``. + }, + "tool": "str" # Optional. + SessionPolicySpec is the Gateway-relevant subset of a + session's permission policy. Filesystem and network policy + remain enforced by the sandbox. + } + ] + }, + "sessionUrn": "str", # Optional. + "tools": { + "references": [ + { + "kind": "str", # Optional. Known + values are: "SESSION_TOOL_REFERENCE_KIND_TOOL" and + "SESSION_TOOL_REFERENCE_KIND_TOOLBELT". + "name": "str", # Optional. Omitted + when the request omitted tools (all tools). A present + selection with no references represents tools: []. + "version": "str" # Optional. Omitted + when the request omitted tools (all tools). A present + selection with no references represents tools: []. + } + ] + }, + "updatedAt": "2020-02-20 00:00:00" # Optional. + } + ] + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_sessions_list_request( + end_user_id=end_user_id, + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + async def create( + self, body: JSON, *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create an Action Gateway Session. + + Creates a session with a tool selection, invocation policy, and optional direct-tool preload + configuration. + + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "actor_id": "str", # Required. + "name": "str", # Required. + "config": { + "preloadTools": [ + "str" # Optional. Concrete tools or pinned toolbelts to + expose directly beside the session meta-tools. + ] + }, + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. Known + values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. + Lowercase values are canonical so ProtoJSON matches the public REST + vocabulary; the prefixed aliases preserve compatibility for existing + protobuf clients. Known values are: "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary of + :code:``. + }, + "tool": "str" # Optional. Invocation policy. Omit to + use a default action of ask. + } + ] + }, + "tools": [ + "str" # Optional. Omitted enables every tool. An explicit empty + array enables no tools. Direct tools may be :code:`` or + @:code:``; toolbelt references must be version-pinned as + toolbelt::code:``@:code:``. + ] + } + + # response body for status code(s): 200 + response == { + "mcpUrl": "str", # Public session-pinned MCP URL. Required. + "session": { + "actorId": "str", # Optional. actor_id is empty when the session is + not bound to an actor. + "config": {}, # Optional. Preserved as an opaque object. Gateway + currently interprets config.preloadTools to add selected direct tools to the + session MCP. + "createdAt": "2020-02-20 00:00:00", # Optional. A session and the + tool-permission policy bound to it. Required. + "name": "str", # Optional. name is the required human-readable + session name. + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. + Known values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value + is "ask". SessionPolicyAction is the disposition applied to a + tool call. Lowercase values are canonical so ProtoJSON matches + the public REST vocabulary; the prefixed aliases preserve + compatibility for existing protobuf clients. Known values are: + "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary + of :code:``. + }, + "tool": "str" # Optional. SessionPolicySpec + is the Gateway-relevant subset of a session's permission policy. + Filesystem and network policy remain enforced by the sandbox. + } + ] + }, + "sessionUrn": "str", # Optional. A session and the tool-permission + policy bound to it. Required. + "tools": { + "references": [ + { + "kind": "str", # Optional. Known values are: + "SESSION_TOOL_REFERENCE_KIND_TOOL" and + "SESSION_TOOL_REFERENCE_KIND_TOOLBELT". + "name": "str", # Optional. Omitted when the + request omitted tools (all tools). A present selection with no + references represents tools: []. + "version": "str" # Optional. Omitted when + the request omitted tools (all tools). A present selection with + no references represents tools: []. + } + ] + }, + "updatedAt": "2020-02-20 00:00:00" # Optional. A session and the + tool-permission policy bound to it. Required. + }, + "tools": [ + "str" # Canonical, version-pinned selected tool references. + Required. + ] + } + # response body for status code(s): 400 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + async def create( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create an Action Gateway Session. + + Creates a session with a tool selection, invocation policy, and optional direct-tool preload + configuration. + + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "mcpUrl": "str", # Public session-pinned MCP URL. Required. + "session": { + "actorId": "str", # Optional. actor_id is empty when the session is + not bound to an actor. + "config": {}, # Optional. Preserved as an opaque object. Gateway + currently interprets config.preloadTools to add selected direct tools to the + session MCP. + "createdAt": "2020-02-20 00:00:00", # Optional. A session and the + tool-permission policy bound to it. Required. + "name": "str", # Optional. name is the required human-readable + session name. + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. + Known values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value + is "ask". SessionPolicyAction is the disposition applied to a + tool call. Lowercase values are canonical so ProtoJSON matches + the public REST vocabulary; the prefixed aliases preserve + compatibility for existing protobuf clients. Known values are: + "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary + of :code:``. + }, + "tool": "str" # Optional. SessionPolicySpec + is the Gateway-relevant subset of a session's permission policy. + Filesystem and network policy remain enforced by the sandbox. + } + ] + }, + "sessionUrn": "str", # Optional. A session and the tool-permission + policy bound to it. Required. + "tools": { + "references": [ + { + "kind": "str", # Optional. Known values are: + "SESSION_TOOL_REFERENCE_KIND_TOOL" and + "SESSION_TOOL_REFERENCE_KIND_TOOLBELT". + "name": "str", # Optional. Omitted when the + request omitted tools (all tools). A present selection with no + references represents tools: []. + "version": "str" # Optional. Omitted when + the request omitted tools (all tools). A present selection with + no references represents tools: []. + } + ] + }, + "updatedAt": "2020-02-20 00:00:00" # Optional. A session and the + tool-permission policy bound to it. Required. + }, + "tools": [ + "str" # Canonical, version-pinned selected tool references. + Required. + ] + } + # response body for status code(s): 400 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace_async + async def create(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Create an Action Gateway Session. + + Creates a session with a tool selection, invocation policy, and optional direct-tool preload + configuration. + + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "actor_id": "str", # Required. + "name": "str", # Required. + "config": { + "preloadTools": [ + "str" # Optional. Concrete tools or pinned toolbelts to + expose directly beside the session meta-tools. + ] + }, + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. Known + values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. + Lowercase values are canonical so ProtoJSON matches the public REST + vocabulary; the prefixed aliases preserve compatibility for existing + protobuf clients. Known values are: "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary of + :code:``. + }, + "tool": "str" # Optional. Invocation policy. Omit to + use a default action of ask. + } + ] + }, + "tools": [ + "str" # Optional. Omitted enables every tool. An explicit empty + array enables no tools. Direct tools may be :code:`` or + @:code:``; toolbelt references must be version-pinned as + toolbelt::code:``@:code:``. + ] + } + + # response body for status code(s): 200 + response == { + "mcpUrl": "str", # Public session-pinned MCP URL. Required. + "session": { + "actorId": "str", # Optional. actor_id is empty when the session is + not bound to an actor. + "config": {}, # Optional. Preserved as an opaque object. Gateway + currently interprets config.preloadTools to add selected direct tools to the + session MCP. + "createdAt": "2020-02-20 00:00:00", # Optional. A session and the + tool-permission policy bound to it. Required. + "name": "str", # Optional. name is the required human-readable + session name. + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. + Known values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value + is "ask". SessionPolicyAction is the disposition applied to a + tool call. Lowercase values are canonical so ProtoJSON matches + the public REST vocabulary; the prefixed aliases preserve + compatibility for existing protobuf clients. Known values are: + "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary + of :code:``. + }, + "tool": "str" # Optional. SessionPolicySpec + is the Gateway-relevant subset of a session's permission policy. + Filesystem and network policy remain enforced by the sandbox. + } + ] + }, + "sessionUrn": "str", # Optional. A session and the tool-permission + policy bound to it. Required. + "tools": { + "references": [ + { + "kind": "str", # Optional. Known values are: + "SESSION_TOOL_REFERENCE_KIND_TOOL" and + "SESSION_TOOL_REFERENCE_KIND_TOOLBELT". + "name": "str", # Optional. Omitted when the + request omitted tools (all tools). A present selection with no + references represents tools: []. + "version": "str" # Optional. Omitted when + the request omitted tools (all tools). A present selection with + no references represents tools: []. + } + ] + }, + "updatedAt": "2020-02-20 00:00:00" # Optional. A session and the + tool-permission policy bound to it. Required. + }, + "tools": [ + "str" # Canonical, version-pinned selected tool references. + Required. + ] + } + # response body for status code(s): 400 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_sessions_create_request( + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace_async + async def delete(self, session_urn: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Delete an Action Gateway Session. + + Deletes an Action Gateway session owned by the authenticated team. + + :param session_urn: The URL-encoded managed agents session URN. Required. + :type session_urn: str + :return: JSON or JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_sessions_delete_request( + session_urn=session_urn, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + await response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + class OneClicksOperations: """ .. warning:: @@ -13446,7 +18075,7 @@ async def create( }, "project_id": "str" # Optional. The ID of the project the app should be assigned to. If omitted, it will be assigned to your default project. - :code:`
`:code:`
`Requires ``project:update`` scope. + :code:`
`:code:`
`Requires ``project:assign_resource`` scope. } # response body for status code(s): 200 @@ -30169,7 +34798,7 @@ async def create(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: }, "project_id": "str" # Optional. The ID of the project the app should be assigned to. If omitted, it will be assigned to your default project. - :code:`
`:code:`
`Requires ``project:update`` scope. + :code:`
`:code:`
`Requires ``project:assign_resource`` scope. } # response body for status code(s): 200 @@ -111450,8 +116079,10 @@ async def list_clusters( } ], "pg_allow_replication": bool # - Optional. For Postgres clusters, set to ``true`` for a user - with replication rights. This option is not currently + Optional. For PostgreSQL clusters, set to ``true`` to grant + the user replication privileges. When omitted on create or + update, the value defaults to ``false`` and replication + privileges are not granted. This option is not currently supported for other database engines. } } @@ -111727,7 +116358,7 @@ async def create_cluster( "project_id": "str", # Optional. The ID of the project that the database cluster is assigned to. If excluded when creating a new database cluster, it will be assigned to your default project.:code:`
`:code:`
`Requires - ``project:update`` scope. + ``project:assign_resource`` scope. "rules": [ { "type": "str", # The type of resource that the firewall rule @@ -111904,9 +116535,10 @@ async def create_cluster( } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user replication + privileges. When omitted on create or update, the value defaults to + ``false`` and replication privileges are not granted. This option is + not currently supported for other database engines. } } ], @@ -112208,9 +116840,11 @@ async def create_cluster( } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user + replication privileges. When omitted on create or update, the + value defaults to ``false`` and replication privileges are not + granted. This option is not currently supported for other + database engines. } } ], @@ -112581,9 +117215,11 @@ async def create_cluster( } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user + replication privileges. When omitted on create or update, the + value defaults to ``false`` and replication privileges are not + granted. This option is not currently supported for other + database engines. } } ], @@ -112775,7 +117411,7 @@ async def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> J "project_id": "str", # Optional. The ID of the project that the database cluster is assigned to. If excluded when creating a new database cluster, it will be assigned to your default project.:code:`
`:code:`
`Requires - ``project:update`` scope. + ``project:assign_resource`` scope. "rules": [ { "type": "str", # The type of resource that the firewall rule @@ -112952,9 +117588,10 @@ async def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> J } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user replication + privileges. When omitted on create or update, the value defaults to + ``false`` and replication privileges are not granted. This option is + not currently supported for other database engines. } } ], @@ -113256,9 +117893,11 @@ async def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> J } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user + replication privileges. When omitted on create or update, the + value defaults to ``false`` and replication privileges are not + granted. This option is not currently supported for other + database engines. } } ], @@ -113692,9 +118331,11 @@ async def get_cluster(self, database_cluster_uuid: str, **kwargs: Any) -> JSON: } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user + replication privileges. When omitted on create or update, the + value defaults to ``false`` and replication privileges are not + granted. This option is not currently supported for other + database engines. } } ], @@ -115506,10 +120147,11 @@ async def update_firewall_rules( resources should be able to open connections to the database. You may limit connections to specific Droplets, Kubernetes clusters, or IP addresses. When a tag is provided, any Droplet or Kubernetes node with that tag applied to it will have access. The firewall is limited to 100 - rules (or trusted sources). When possible, we recommend `placing your databases into a VPC - network `_ to limit access to them - instead of using a firewall. - A successful. + rules (or trusted sources). You cannot add IPv6 addresses as trusted sources. For additional + limits, see your database engine's limits page. When possible, we recommend `placing your + databases into a VPC network `_ to + limit access to them instead of using a firewall. + A successful request returns a 204 status code with no content. :param database_cluster_uuid: A unique identifier for a database cluster. Required. :type database_cluster_uuid: str @@ -115578,10 +120220,11 @@ async def update_firewall_rules( resources should be able to open connections to the database. You may limit connections to specific Droplets, Kubernetes clusters, or IP addresses. When a tag is provided, any Droplet or Kubernetes node with that tag applied to it will have access. The firewall is limited to 100 - rules (or trusted sources). When possible, we recommend `placing your databases into a VPC - network `_ to limit access to them - instead of using a firewall. - A successful. + rules (or trusted sources). You cannot add IPv6 addresses as trusted sources. For additional + limits, see your database engine's limits page. When possible, we recommend `placing your + databases into a VPC network `_ to + limit access to them instead of using a firewall. + A successful request returns a 204 status code with no content. :param database_cluster_uuid: A unique identifier for a database cluster. Required. :type database_cluster_uuid: str @@ -115622,10 +120265,11 @@ async def update_firewall_rules( resources should be able to open connections to the database. You may limit connections to specific Droplets, Kubernetes clusters, or IP addresses. When a tag is provided, any Droplet or Kubernetes node with that tag applied to it will have access. The firewall is limited to 100 - rules (or trusted sources). When possible, we recommend `placing your databases into a VPC - network `_ to limit access to them - instead of using a firewall. - A successful. + rules (or trusted sources). You cannot add IPv6 addresses as trusted sources. For additional + limits, see your database engine's limits page. When possible, we recommend `placing your + databases into a VPC network `_ to + limit access to them instead of using a firewall. + A successful request returns a 204 status code with no content. :param database_cluster_uuid: A unique identifier for a database cluster. Required. :type database_cluster_uuid: str @@ -118038,6 +122682,11 @@ async def list_users(self, database_cluster_uuid: str, **kwargs: Any) -> JSON: For MySQL clusters, additional options will be contained in the mysql_settings object. + For PostgreSQL clusters, additional options will be contained in the ``settings`` + object (for example, ``pg_allow_replication``\\ ). + + For Kafka clusters, additional options will be contained in the ``settings`` object. + For MongoDB clusters, additional information will be contained in the mongo_user_settings object. @@ -118128,9 +122777,10 @@ async def list_users(self, database_cluster_uuid: str, **kwargs: Any) -> JSON: } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user replication + privileges. When omitted on create or update, the value defaults to + ``false`` and replication privileges are not granted. This option is + not currently supported for other database engines. } } ] @@ -118245,10 +122895,14 @@ async def add_user( When adding a user to a MySQL cluster, additional options can be configured in the ``mysql_settings`` object. + When adding a user to a PostgreSQL cluster, additional options can be configured in + the ``settings`` object (for example, ``pg_allow_replication``\\ ). When + ``pg_allow_replication`` is omitted, it defaults to ``false``. + When adding a user to a Kafka cluster, additional options can be configured in the ``settings`` object. - When adding a user to a MongoDB cluster, additional options can be configured in + When adding a user to a MongoDB cluster, additional options can be configured in the ``settings.mongo_user_settings`` object. The response will be a JSON object with a key called ``user``. The value of this will be an @@ -118340,9 +122994,11 @@ async def add_user( values are: "deny", "admin", "read", "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres clusters, set - to ``true`` for a user with replication rights. This option is not currently - supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL clusters, + set to ``true`` to grant the user replication privileges. When omitted on + create or update, the value defaults to ``false`` and replication privileges + are not granted. This option is not currently supported for other database + engines. } } @@ -118417,9 +123073,11 @@ async def add_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -118456,10 +123114,14 @@ async def add_user( When adding a user to a MySQL cluster, additional options can be configured in the ``mysql_settings`` object. + When adding a user to a PostgreSQL cluster, additional options can be configured in + the ``settings`` object (for example, ``pg_allow_replication``\\ ). When + ``pg_allow_replication`` is omitted, it defaults to ``false``. + When adding a user to a Kafka cluster, additional options can be configured in the ``settings`` object. - When adding a user to a MongoDB cluster, additional options can be configured in + When adding a user to a MongoDB cluster, additional options can be configured in the ``settings.mongo_user_settings`` object. The response will be a JSON object with a key called ``user``. The value of this will be an @@ -118551,9 +123213,11 @@ async def add_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -118585,10 +123249,14 @@ async def add_user( When adding a user to a MySQL cluster, additional options can be configured in the ``mysql_settings`` object. + When adding a user to a PostgreSQL cluster, additional options can be configured in + the ``settings`` object (for example, ``pg_allow_replication``\\ ). When + ``pg_allow_replication`` is omitted, it defaults to ``false``. + When adding a user to a Kafka cluster, additional options can be configured in the ``settings`` object. - When adding a user to a MongoDB cluster, additional options can be configured in + When adding a user to a MongoDB cluster, additional options can be configured in the ``settings.mongo_user_settings`` object. The response will be a JSON object with a key called ``user``. The value of this will be an @@ -118677,9 +123345,11 @@ async def add_user( values are: "deny", "admin", "read", "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres clusters, set - to ``true`` for a user with replication rights. This option is not currently - supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL clusters, + set to ``true`` to grant the user replication privileges. When omitted on + create or update, the value defaults to ``false`` and replication privileges + are not granted. This option is not currently supported for other database + engines. } } @@ -118754,9 +123424,11 @@ async def add_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -118883,6 +123555,9 @@ async def get_user( For MySQL clusters, additional options will be contained in the ``mysql_settings`` object. + For PostgreSQL clusters, additional options will be contained in the ``settings`` + object (for example, ``pg_allow_replication``\\ ). + For Kafka clusters, additional options will be contained in the ``settings`` object. For MongoDB clusters, additional information will be contained in the mongo_user_settings @@ -118970,9 +123645,11 @@ async def get_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -119201,8 +123878,14 @@ async def update_user( the name of a user, you must recreate a new user. + For PostgreSQL clusters, you can update ``settings.pg_allow_replication`` to enable or + disable replication privileges for the user. When omitted, the value defaults to ``false``. + + For Kafka and OpenSearch clusters, additional options can be configured in the + ``settings`` object (for example, topic or index ACLs). + The response will be a JSON object with a key called ``user``. The value of this will be an - object that contains the name of the update database user, along with the ``settings`` object + object that contains the name of the updated database user, along with the ``settings`` object that has been updated. @@ -119271,9 +123954,11 @@ async def update_user( values are: "deny", "admin", "read", "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres clusters, set - to ``true`` for a user with replication rights. This option is not currently - supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL clusters, + set to ``true`` to grant the user replication privileges. When omitted on + create or update, the value defaults to ``false`` and replication privileges + are not granted. This option is not currently supported for other database + engines. } } @@ -119348,9 +124033,11 @@ async def update_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -119388,8 +124075,14 @@ async def update_user( the name of a user, you must recreate a new user. + For PostgreSQL clusters, you can update ``settings.pg_allow_replication`` to enable or + disable replication privileges for the user. When omitted, the value defaults to ``false``. + + For Kafka and OpenSearch clusters, additional options can be configured in the + ``settings`` object (for example, topic or index ACLs). + The response will be a JSON object with a key called ``user``. The value of this will be an - object that contains the name of the update database user, along with the ``settings`` object + object that contains the name of the updated database user, along with the ``settings`` object that has been updated. @@ -119480,9 +124173,11 @@ async def update_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -119518,8 +124213,14 @@ async def update_user( the name of a user, you must recreate a new user. + For PostgreSQL clusters, you can update ``settings.pg_allow_replication`` to enable or + disable replication privileges for the user. When omitted, the value defaults to ``false``. + + For Kafka and OpenSearch clusters, additional options can be configured in the + ``settings`` object (for example, topic or index ACLs). + The response will be a JSON object with a key called ``user``. The value of this will be an - object that contains the name of the update database user, along with the ``settings`` object + object that contains the name of the updated database user, along with the ``settings`` object that has been updated. @@ -119585,9 +124286,11 @@ async def update_user( values are: "deny", "admin", "read", "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres clusters, set - to ``true`` for a user with replication rights. This option is not currently - supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL clusters, + set to ``true`` to grant the user replication privileges. When omitted on + create or update, the value defaults to ``false`` and replication privileges + are not granted. This option is not currently supported for other database + engines. } } @@ -119662,9 +124365,11 @@ async def update_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -119895,9 +124600,11 @@ async def reset_auth( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -120024,9 +124731,11 @@ async def reset_auth( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -120160,9 +124869,11 @@ async def reset_auth( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -133724,9 +138435,10 @@ class of Droplets created from this size. For example: Basic, General The unit of measure for the disk size. }, "type": "str" # Optional. The type - of disk. All Droplets contain a ``local`` disk. Additionally, - GPU Droplets can also have a ``scratch`` disk for - non-persistent data. Known values are: "local" and "scratch". + of disk. All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk + for non-persistent data. Known values are: "local", "remote", + and "scratch". } ], "gpu_info": { @@ -133771,9 +138483,10 @@ class of Droplets created from this size. For example: Basic, General of measure for the disk size. }, "type": "str" # Optional. The type of disk. - All Droplets contain a ``local`` disk. Additionally, GPU Droplets - can also have a ``scratch`` disk for non-persistent data. Known - values are: "local" and "scratch". + All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk for + non-persistent data. Known values are: "local", "remote", and + "scratch". } ], "gpu_info": { @@ -134397,9 +139110,10 @@ async def get(self, droplet_id: int, **kwargs: Any) -> JSON: of measure for the disk size. }, "type": "str" # Optional. The type of disk. - All Droplets contain a ``local`` disk. Additionally, GPU Droplets - can also have a ``scratch`` disk for non-persistent data. Known - values are: "local" and "scratch". + All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk for + non-persistent data. Known values are: "local", "remote", and + "scratch". } ], "gpu_info": { @@ -134442,9 +139156,9 @@ async def get(self, droplet_id: int, **kwargs: Any) -> JSON: measure for the disk size. }, "type": "str" # Optional. The type of disk. All - Droplets contain a ``local`` disk. Additionally, GPU Droplets can - also have a ``scratch`` disk for non-persistent data. Known values - are: "local" and "scratch". + Droplets contain a ``local`` or ``remote`` disk. Additionally, GPU + Droplets can also have a ``scratch`` disk for non-persistent data. + Known values are: "local", "remote", and "scratch". } ], "gpu_info": { @@ -135970,9 +140684,10 @@ class of Droplets created from this size. For example: Basic, General The unit of measure for the disk size. }, "type": "str" # Optional. The type - of disk. All Droplets contain a ``local`` disk. Additionally, - GPU Droplets can also have a ``scratch`` disk for - non-persistent data. Known values are: "local" and "scratch". + of disk. All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk + for non-persistent data. Known values are: "local", "remote", + and "scratch". } ], "gpu_info": { @@ -136017,9 +140732,10 @@ class of Droplets created from this size. For example: Basic, General of measure for the disk size. }, "type": "str" # Optional. The type of disk. - All Droplets contain a ``local`` disk. Additionally, GPU Droplets - can also have a ``scratch`` disk for non-persistent data. Known - values are: "local" and "scratch". + All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk for + non-persistent data. Known values are: "local", "remote", and + "scratch". } ], "gpu_info": { @@ -150188,6 +154904,10 @@ async def list_clusters( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -150500,6 +155220,10 @@ async def create_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the Peer-to-peer OCI + registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -150719,257 +155443,265 @@ async def create_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, - "rdma_shared_dev_plugin": { - "enabled": bool # Optional. Indicates whether the RDMA - shared device plugin is enabled. - }, - "registry_enabled": bool, # Optional. A read-only boolean value - indicating if a container registry is integrated with the cluster. - "routing_agent": { + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, + "rdma_shared_dev_plugin": { + "enabled": bool # Optional. Indicates whether the RDMA + shared device plugin is enabled. + }, + "registry_enabled": bool, # Optional. A read-only boolean value + indicating if a container registry is integrated with the cluster. + "routing_agent": { + "enabled": bool # Optional. Indicates whether the + routing-agent component is enabled. + }, + "service_subnet": "str", # Optional. The range of assignable IP + addresses for services running in the Kubernetes cluster in CIDR notation. + "sso": { + "client_id": "str", # Optional. The OIDC client ID + registered with the identity provider. Required when ``enabled`` is + ``true``. + "enabled": False, # Optional. Default value is False. + Indicates whether SSO authentication is enabled for the cluster. + "issuer_url": "str", # Optional. The OIDC issuer URL for the + identity provider. Required when ``enabled`` is ``true``. + "required": False # Optional. Default value is False. + Indicates whether any non-SSO forms of authentication are disallowed. Can + only be set to ``true`` when ``enabled`` is ``true``. + }, + "status": { + "message": "str", # Optional. An optional message providing + additional information about the current cluster state. + "state": "str" # Optional. A string indicating the current + status of the cluster. Known values are: "running", "provisioning", + "degraded", "error", "deleted", "upgrading", and "deleting". + }, + "surge_upgrade": False, # Optional. Default value is False. A + boolean value indicating whether surge upgrade is enabled/disabled for the + cluster. Surge upgrade makes cluster upgrades fast and reliable by bringing + up new nodes before destroying the outdated nodes. + "tags": [ + "str" # Optional. An array of tags to apply to the + Kubernetes cluster. All clusters are automatically tagged ``k8s`` and + ``k8s:$K8S_CLUSTER_ID``. :code:`
`:code:`
`Requires ``tag:read`` + and ``tag:create`` scope, as well as ``tag:delete`` if existing tags are + getting removed. + ], + "updated_at": "2020-02-20 00:00:00", # Optional. A time value given + in ISO8601 combined date and time format that represents when the Kubernetes + cluster was last updated. + "vpc_uuid": "str", # Optional. A string specifying the UUID of the + VPC to which the Kubernetes cluster is + assigned.:code:`
`:code:`
`Requires ``vpc:read`` scope. + "worker_subnet_uuid": "str" # Optional. The UUID of the VPC subnet + to attach worker nodes to. When omitted on create, the default subnet for the + VPC is used. This value cannot be changed after the cluster is created. + ``vpc_uuid`` must also be set. :code:`
`:code:`
`Requires ``vpc:read`` + scope. + } + } + """ + + @overload + async def create_cluster( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a New Kubernetes Cluster. + + To create a new Kubernetes cluster, send a POST request to + ``/v2/kubernetes/clusters``. The request must contain at least one node pool + with at least one worker. + + The request may contain a maintenance window policy describing a time period + when disruptive maintenance tasks may be carried out. Omitting the policy + implies that a window will be chosen automatically. See + `here `_ + for details. + + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 201 + response == { + "kubernetes_cluster": { + "name": "str", # A human-readable name for a Kubernetes cluster. + Required. + "node_pools": [ + { + "auto_scale": bool, # Optional. A boolean value + indicating whether auto-scaling is enabled for this node pool. + "count": 0, # Optional. The number of Droplet + instances in the node pool. + "id": "str", # Optional. A unique ID that can be + used to identify and reference a specific node pool. + "labels": {}, # Optional. An object of key/value + mappings specifying labels to apply to all nodes in a pool. Labels + will automatically be applied to all existing nodes and any + subsequent nodes added to the pool. Note that when a label is + removed, it is not deleted from the nodes in the pool. + "max_nodes": 0, # Optional. The maximum number of + nodes that this node pool can be auto-scaled to. The value will be + ``0`` if ``auto_scale`` is set to ``false``. + "min_nodes": 0, # Optional. The minimum number of + nodes that this node pool can be auto-scaled to. The value will be + ``0`` if ``auto_scale`` is set to ``false``. + "name": "str", # Optional. A human-readable name for + the node pool. + "nodes": [ + { + "created_at": "2020-02-20 00:00:00", + # Optional. A time value given in ISO8601 combined date and + time format that represents when the node was created. + "droplet_id": "str", # Optional. The + ID of the Droplet used for the worker node. + "id": "str", # Optional. A unique ID + that can be used to identify and reference the node. + "name": "str", # Optional. An + automatically generated, human-readable name for the node. + "status": { + "state": "str" # Optional. A + string indicating the current status of the node. Known + values are: "provisioning", "running", "draining", and + "deleting". + }, + "updated_at": "2020-02-20 00:00:00" + # Optional. A time value given in ISO8601 combined date and + time format that represents when the node was last updated. + } + ], + "size": "str", # Optional. The slug identifier for + the type of Droplet used as workers in the node pool. + "tags": [ + "str" # Optional. An array containing the + tags applied to the node pool. All node pools are automatically + tagged ``k8s``"" , ``k8s-worker``"" , and + ``k8s:$K8S_CLUSTER_ID``. :code:`
`:code:`
`Requires + ``tag:read`` scope. + ], + "taints": [ + { + "effect": "str", # Optional. How the + node reacts to pods that it won't tolerate. Available effect + values are ``NoSchedule``"" , ``PreferNoSchedule``"" , and + ``NoExecute``. Known values are: "NoSchedule", + "PreferNoSchedule", and "NoExecute". + "key": "str", # Optional. An + arbitrary string. The ``key`` and ``value`` fields of the + ``taint`` object form a key-value pair. For example, if the + value of the ``key`` field is "special" and the value of the + ``value`` field is "gpu", the key value pair would be + ``special=gpu``. + "value": "str" # Optional. An + arbitrary string. The ``key`` and ``value`` fields of the + ``taint`` object form a key-value pair. For example, if the + value of the ``key`` field is "special" and the value of the + ``value`` field is "gpu", the key value pair would be + ``special=gpu``. + } + ] + } + ], + "region": "str", # The slug identifier for the region where the + Kubernetes cluster is located. Required. + "version": "str", # The slug identifier for the version of + Kubernetes used for the cluster. If set to a minor version (e.g. "1.14"), the + latest version within it will be used (e.g. "1.14.6-do.1"); if set to + "latest", the latest published version will be used. See the + ``/v2/kubernetes/options`` endpoint to find all currently available versions. + Required. + "amd_gpu_device_metrics_exporter_plugin": { + "enabled": bool # Optional. Indicates whether the AMD Device + Metrics Exporter is enabled. + }, + "amd_gpu_device_plugin": { + "enabled": bool # Optional. Indicates whether the AMD GPU + Device Plugin is enabled. + }, + "auto_upgrade": False, # Optional. Default value is False. A boolean + value indicating whether the cluster will be automatically upgraded to new + patch releases during its maintenance window. + "cluster_autoscaler_configuration": { + "expanders": [ + "str" # Optional. Customizes expanders used by + cluster-autoscaler. The autoscaler will apply each expander from the + provided list to narrow down the selection of node types created to + scale up, until either a single node type is left, or the list of + expanders is exhausted. If this flag is unset, autoscaler will use + its default expander ``random``. Passing an empty list ("" *not* + ``null``"" ) will unset any previous expander customizations. + Available expanders: * ``random``"" : Randomly selects a node group + to scale. * `priority`: Selects the node group with the highest + priority as per [user-provided + configuration](https://docs.digitalocean.com/products/kubernetes/how-to/autoscale/#configuring-priority-expander) + * ``least_waste``"" : Selects the node group that will result in the + least amount of idle resources. + ], + "scale_down_unneeded_time": "str", # Optional. Used to + customize how long a node is unneeded before being scaled down. + "scale_down_utilization_threshold": 0.0 # Optional. Used to + customize when cluster autoscaler scales down non-empty nodes by setting + the node utilization threshold. + }, + "cluster_subnet": "str", # Optional. The range of IP addresses for + the overlay network of the Kubernetes cluster in CIDR notation. + "control_plane_firewall": { + "allowed_addresses": [ + "str" # Optional. An array of public addresses (IPv4 + or CIDR) allowed to access the control plane. + ], + "enabled": bool # Optional. Indicates whether the control + plane firewall is enabled. + }, + "coredns_autoscaler": { + "enabled": bool # Optional. Indicates whether the CoreDNS + Cluster Proportional Autoscaler add-on is enabled. + }, + "created_at": "2020-02-20 00:00:00", # Optional. A time value given + in ISO8601 combined date and time format that represents when the Kubernetes + cluster was created. + "endpoint": "str", # Optional. The base URL of the API server on the + Kubernetes master node. + "ha": bool, # Optional. A boolean value indicating whether the + control plane is run in a highly available configuration in the cluster. + Highly available control planes incur less downtime. The property cannot be + disabled. When omitted on create, the default is version-dependent; for DOKS + 1.36.0 and later, the default is true; for earlier versions, the default is + false. + "id": "str", # Optional. A unique ID that can be used to identify + and reference a Kubernetes cluster. + "ipv4": "str", # Optional. The public IPv4 address of the Kubernetes + master node. This will not be set if high availability is configured on the + cluster (v1.21+). + "maintenance_policy": { + "day": "str", # Optional. The day of the maintenance window + policy. May be one of ``monday`` through ``sunday``"" , or ``any`` to + indicate an arbitrary week day. Known values are: "any", "monday", + "tuesday", "wednesday", "thursday", "friday", "saturday", and "sunday". + "duration": "str", # Optional. The duration of the + maintenance window policy in human-readable format. + "start_time": "str" # Optional. The start time in UTC of the + maintenance window policy in 24-hour clock format / HH:MM notation (e.g., + ``15:00``"" ). + }, + "nvidia_gpu_device_plugin": { + "enabled": bool # Optional. Indicates whether the Nvidia GPU + Device Plugin is enabled. + }, + "p2p_oci_registry_plugin": { "enabled": bool # Optional. Indicates whether the - routing-agent component is enabled. - }, - "service_subnet": "str", # Optional. The range of assignable IP - addresses for services running in the Kubernetes cluster in CIDR notation. - "sso": { - "client_id": "str", # Optional. The OIDC client ID - registered with the identity provider. Required when ``enabled`` is - ``true``. - "enabled": False, # Optional. Default value is False. - Indicates whether SSO authentication is enabled for the cluster. - "issuer_url": "str", # Optional. The OIDC issuer URL for the - identity provider. Required when ``enabled`` is ``true``. - "required": False # Optional. Default value is False. - Indicates whether any non-SSO forms of authentication are disallowed. Can - only be set to ``true`` when ``enabled`` is ``true``. - }, - "status": { - "message": "str", # Optional. An optional message providing - additional information about the current cluster state. - "state": "str" # Optional. A string indicating the current - status of the cluster. Known values are: "running", "provisioning", - "degraded", "error", "deleted", "upgrading", and "deleting". - }, - "surge_upgrade": False, # Optional. Default value is False. A - boolean value indicating whether surge upgrade is enabled/disabled for the - cluster. Surge upgrade makes cluster upgrades fast and reliable by bringing - up new nodes before destroying the outdated nodes. - "tags": [ - "str" # Optional. An array of tags to apply to the - Kubernetes cluster. All clusters are automatically tagged ``k8s`` and - ``k8s:$K8S_CLUSTER_ID``. :code:`
`:code:`
`Requires ``tag:read`` - and ``tag:create`` scope, as well as ``tag:delete`` if existing tags are - getting removed. - ], - "updated_at": "2020-02-20 00:00:00", # Optional. A time value given - in ISO8601 combined date and time format that represents when the Kubernetes - cluster was last updated. - "vpc_uuid": "str", # Optional. A string specifying the UUID of the - VPC to which the Kubernetes cluster is - assigned.:code:`
`:code:`
`Requires ``vpc:read`` scope. - "worker_subnet_uuid": "str" # Optional. The UUID of the VPC subnet - to attach worker nodes to. When omitted on create, the default subnet for the - VPC is used. This value cannot be changed after the cluster is created. - ``vpc_uuid`` must also be set. :code:`
`:code:`
`Requires ``vpc:read`` - scope. - } - } - """ - - @overload - async def create_cluster( - self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any - ) -> JSON: - # pylint: disable=line-too-long - """Create a New Kubernetes Cluster. - - To create a new Kubernetes cluster, send a POST request to - ``/v2/kubernetes/clusters``. The request must contain at least one node pool - with at least one worker. - - The request may contain a maintenance window policy describing a time period - when disruptive maintenance tasks may be carried out. Omitting the policy - implies that a window will be chosen automatically. See - `here `_ - for details. - - :param body: Required. - :type body: IO[bytes] - :keyword content_type: Body Parameter content-type. Content type parameter for binary body. - Default value is "application/json". - :paramtype content_type: str - :return: JSON object - :rtype: JSON - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. code-block:: python - - # response body for status code(s): 201 - response == { - "kubernetes_cluster": { - "name": "str", # A human-readable name for a Kubernetes cluster. - Required. - "node_pools": [ - { - "auto_scale": bool, # Optional. A boolean value - indicating whether auto-scaling is enabled for this node pool. - "count": 0, # Optional. The number of Droplet - instances in the node pool. - "id": "str", # Optional. A unique ID that can be - used to identify and reference a specific node pool. - "labels": {}, # Optional. An object of key/value - mappings specifying labels to apply to all nodes in a pool. Labels - will automatically be applied to all existing nodes and any - subsequent nodes added to the pool. Note that when a label is - removed, it is not deleted from the nodes in the pool. - "max_nodes": 0, # Optional. The maximum number of - nodes that this node pool can be auto-scaled to. The value will be - ``0`` if ``auto_scale`` is set to ``false``. - "min_nodes": 0, # Optional. The minimum number of - nodes that this node pool can be auto-scaled to. The value will be - ``0`` if ``auto_scale`` is set to ``false``. - "name": "str", # Optional. A human-readable name for - the node pool. - "nodes": [ - { - "created_at": "2020-02-20 00:00:00", - # Optional. A time value given in ISO8601 combined date and - time format that represents when the node was created. - "droplet_id": "str", # Optional. The - ID of the Droplet used for the worker node. - "id": "str", # Optional. A unique ID - that can be used to identify and reference the node. - "name": "str", # Optional. An - automatically generated, human-readable name for the node. - "status": { - "state": "str" # Optional. A - string indicating the current status of the node. Known - values are: "provisioning", "running", "draining", and - "deleting". - }, - "updated_at": "2020-02-20 00:00:00" - # Optional. A time value given in ISO8601 combined date and - time format that represents when the node was last updated. - } - ], - "size": "str", # Optional. The slug identifier for - the type of Droplet used as workers in the node pool. - "tags": [ - "str" # Optional. An array containing the - tags applied to the node pool. All node pools are automatically - tagged ``k8s``"" , ``k8s-worker``"" , and - ``k8s:$K8S_CLUSTER_ID``. :code:`
`:code:`
`Requires - ``tag:read`` scope. - ], - "taints": [ - { - "effect": "str", # Optional. How the - node reacts to pods that it won't tolerate. Available effect - values are ``NoSchedule``"" , ``PreferNoSchedule``"" , and - ``NoExecute``. Known values are: "NoSchedule", - "PreferNoSchedule", and "NoExecute". - "key": "str", # Optional. An - arbitrary string. The ``key`` and ``value`` fields of the - ``taint`` object form a key-value pair. For example, if the - value of the ``key`` field is "special" and the value of the - ``value`` field is "gpu", the key value pair would be - ``special=gpu``. - "value": "str" # Optional. An - arbitrary string. The ``key`` and ``value`` fields of the - ``taint`` object form a key-value pair. For example, if the - value of the ``key`` field is "special" and the value of the - ``value`` field is "gpu", the key value pair would be - ``special=gpu``. - } - ] - } - ], - "region": "str", # The slug identifier for the region where the - Kubernetes cluster is located. Required. - "version": "str", # The slug identifier for the version of - Kubernetes used for the cluster. If set to a minor version (e.g. "1.14"), the - latest version within it will be used (e.g. "1.14.6-do.1"); if set to - "latest", the latest published version will be used. See the - ``/v2/kubernetes/options`` endpoint to find all currently available versions. - Required. - "amd_gpu_device_metrics_exporter_plugin": { - "enabled": bool # Optional. Indicates whether the AMD Device - Metrics Exporter is enabled. - }, - "amd_gpu_device_plugin": { - "enabled": bool # Optional. Indicates whether the AMD GPU - Device Plugin is enabled. - }, - "auto_upgrade": False, # Optional. Default value is False. A boolean - value indicating whether the cluster will be automatically upgraded to new - patch releases during its maintenance window. - "cluster_autoscaler_configuration": { - "expanders": [ - "str" # Optional. Customizes expanders used by - cluster-autoscaler. The autoscaler will apply each expander from the - provided list to narrow down the selection of node types created to - scale up, until either a single node type is left, or the list of - expanders is exhausted. If this flag is unset, autoscaler will use - its default expander ``random``. Passing an empty list ("" *not* - ``null``"" ) will unset any previous expander customizations. - Available expanders: * ``random``"" : Randomly selects a node group - to scale. * `priority`: Selects the node group with the highest - priority as per [user-provided - configuration](https://docs.digitalocean.com/products/kubernetes/how-to/autoscale/#configuring-priority-expander) - * ``least_waste``"" : Selects the node group that will result in the - least amount of idle resources. - ], - "scale_down_unneeded_time": "str", # Optional. Used to - customize how long a node is unneeded before being scaled down. - "scale_down_utilization_threshold": 0.0 # Optional. Used to - customize when cluster autoscaler scales down non-empty nodes by setting - the node utilization threshold. - }, - "cluster_subnet": "str", # Optional. The range of IP addresses for - the overlay network of the Kubernetes cluster in CIDR notation. - "control_plane_firewall": { - "allowed_addresses": [ - "str" # Optional. An array of public addresses (IPv4 - or CIDR) allowed to access the control plane. - ], - "enabled": bool # Optional. Indicates whether the control - plane firewall is enabled. - }, - "coredns_autoscaler": { - "enabled": bool # Optional. Indicates whether the CoreDNS - Cluster Proportional Autoscaler add-on is enabled. - }, - "created_at": "2020-02-20 00:00:00", # Optional. A time value given - in ISO8601 combined date and time format that represents when the Kubernetes - cluster was created. - "endpoint": "str", # Optional. The base URL of the API server on the - Kubernetes master node. - "ha": bool, # Optional. A boolean value indicating whether the - control plane is run in a highly available configuration in the cluster. - Highly available control planes incur less downtime. The property cannot be - disabled. When omitted on create, the default is version-dependent; for DOKS - 1.36.0 and later, the default is true; for earlier versions, the default is - false. - "id": "str", # Optional. A unique ID that can be used to identify - and reference a Kubernetes cluster. - "ipv4": "str", # Optional. The public IPv4 address of the Kubernetes - master node. This will not be set if high availability is configured on the - cluster (v1.21+). - "maintenance_policy": { - "day": "str", # Optional. The day of the maintenance window - policy. May be one of ``monday`` through ``sunday``"" , or ``any`` to - indicate an arbitrary week day. Known values are: "any", "monday", - "tuesday", "wednesday", "thursday", "friday", "saturday", and "sunday". - "duration": "str", # Optional. The duration of the - maintenance window policy in human-readable format. - "start_time": "str" # Optional. The start time in UTC of the - maintenance window policy in 24-hour clock format / HH:MM notation (e.g., - ``15:00``"" ). - }, - "nvidia_gpu_device_plugin": { - "enabled": bool # Optional. Indicates whether the Nvidia GPU - Device Plugin is enabled. + Peer-to-peer OCI registry component is enabled. }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA @@ -151209,6 +155941,10 @@ async def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> J "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the Peer-to-peer OCI + registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -151428,6 +156164,10 @@ async def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> J "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -151741,6 +156481,10 @@ async def get_cluster(self, cluster_id: str, **kwargs: Any) -> JSON: "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -151983,6 +156727,10 @@ async def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the Peer-to-peer OCI + registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -152179,6 +156927,10 @@ async def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -152443,6 +157195,10 @@ async def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -152600,6 +157356,10 @@ async def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the Peer-to-peer OCI + registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -152796,6 +157556,10 @@ async def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -178398,9 +183162,10 @@ async def assign_resources( To assign resources to a project, send a POST request to ``/v2/projects/$PROJECT_ID/resources``. - You must have both ``project:update`` and ``:read`` scopes to assign new resources. - For example, to assign a Droplet to a project, include both the ``project:update`` and - ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to a project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param project_id: A unique identifier for a project. Required. :type project_id: str @@ -178472,9 +183237,10 @@ async def assign_resources( To assign resources to a project, send a POST request to ``/v2/projects/$PROJECT_ID/resources``. - You must have both ``project:update`` and ``:read`` scopes to assign new resources. - For example, to assign a Droplet to a project, include both the ``project:update`` and - ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to a project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param project_id: A unique identifier for a project. Required. :type project_id: str @@ -178532,9 +183298,10 @@ async def assign_resources( To assign resources to a project, send a POST request to ``/v2/projects/$PROJECT_ID/resources``. - You must have both ``project:update`` and ``:read`` scopes to assign new resources. - For example, to assign a Droplet to a project, include both the ``project:update`` and - ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to a project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param project_id: A unique identifier for a project. Required. :type project_id: str @@ -178821,9 +183588,10 @@ async def assign_resources_default( To assign resources to your default project, send a POST request to ``/v2/projects/default/resources``. - You must have both project:update and :code:``:read scopes to assign new resources. - For example, to assign a Droplet to the default project, include both the ``project:update`` - and ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to the default project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param body: Required. :type body: JSON @@ -178888,9 +183656,10 @@ async def assign_resources_default( To assign resources to your default project, send a POST request to ``/v2/projects/default/resources``. - You must have both project:update and :code:``:read scopes to assign new resources. - For example, to assign a Droplet to the default project, include both the ``project:update`` - and ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to the default project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param body: Required. :type body: IO[bytes] @@ -178946,9 +183715,10 @@ async def assign_resources_default( To assign resources to your default project, send a POST request to ``/v2/projects/default/resources``. - You must have both project:update and :code:``:read scopes to assign new resources. - For example, to assign a Droplet to the default project, include both the ``project:update`` - and ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to the default project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param body: Is either a JSON type or a IO[bytes] type. Required. :type body: JSON or IO[bytes] @@ -192237,9 +197007,10 @@ async def list(self, *, per_page: int = 20, page: int = 1, **kwargs: Any) -> JSO of measure for the disk size. }, "type": "str" # Optional. The type of disk. - All Droplets contain a ``local`` disk. Additionally, GPU Droplets - can also have a ``scratch`` disk for non-persistent data. Known - values are: "local" and "scratch". + All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk for + non-persistent data. Known values are: "local", "remote", and + "scratch". } ], "gpu_info": { diff --git a/src/pydo/gateway/__init__.py b/src/pydo/gateway/__init__.py new file mode 100644 index 00000000..e137bc2b --- /dev/null +++ b/src/pydo/gateway/__init__.py @@ -0,0 +1,170 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Action Gateway API — hand-written; preserved across ``make generate``. + +Session-first surface: create a session on the DigitalOcean API +(generated ``POST /v2/action-gateway/sessions``), then discover/invoke tools and run code +through the API-returned MCP endpoint. Composio-style providers make session +tools plug into pydo inference surfaces (chat completions, messages, responses). +""" + +from __future__ import annotations + +from typing import Any, List, Optional, Sequence + +from .custom_models import ( + META_CODE, + META_INVOKE, + META_SEARCH, + META_TOOL_NAMES, + GatewayProtocolError, + GatewayToolError, + RecoveryHint, + ToolCall, + Toolbelt, + ToolErrorClass, + ToolResultStatus, +) +from .custom_operations import ( + CodeOperations, + ToolsOperations, + normalize_invoke_arguments, +) +from .providers import ( + BaseProvider, + ChatCompletionsProvider, + MessagesProvider, + ResponsesProvider, + default_provider, + execute_tool_calls, + simplify_inference_tool_schema, + simplify_messages_input_schema, +) +from .session import ( + Session, + SessionsOperations, + normalize_permissions, +) +from .transport import ( + ACTOR_ID_HEADER, + MCP_PROTOCOL_VERSION, + SESSION_ID_HEADER, + DEFAULT_GATEWAY_BASE_URL, + GatewayTransport, + MCPTransport, + RESTTransport, + resolve_gateway_base_url, + session_mcp_url, +) + +_ENV_VAR = "PYDO_GATEWAY_ENDPOINT" # kept for docs / discoverability + + +class GatewayResources: + """Action Gateway surface attached at ``client.gateway``. + + Primary entry point is :attr:`sessions` — create a :class:`Session` before + invoking tools. Legacy ``tools`` / ``code`` attributes require an explicit + session-bound transport and are not usable until a session exists. + """ + + def __init__( + self, + parent_client: Any, + *, + gateway_endpoint: Optional[str] = None, + provider: Optional[BaseProvider] = None, + transport: Optional[GatewayTransport] = None, + ): + self._parent = parent_client + self._gateway_endpoint = gateway_endpoint + self._gateway_base_url = resolve_gateway_base_url(gateway_endpoint) + self.provider = provider or default_provider() + self.sessions = SessionsOperations( + parent_client, + gateway_endpoint=gateway_endpoint, + provider=self.provider, + ) + # Optional pre-bound transport (tests). Production callers use sessions. + self._transport = transport + if transport is not None: + self.tools = ToolsOperations(transport, self.provider) + self.code = CodeOperations(transport) + else: + self.tools = None + self.code = None + + @property + def base_url(self) -> str: + return self._gateway_base_url + + def handle_tool_calls( + self, + response: Any, + *, + rationale: Optional[str] = None, + ) -> List[Any]: + """Deprecated path — prefer ``session.handle_tool_calls(response)``.""" + if self.tools is None: + raise RuntimeError( + "create a session first: session = client.sessions.create(" + "actor_id=...); then session.handle_tool_calls(response)" + ) + calls = self.provider.extract_tool_calls(response) + if not calls: + return [] + results = execute_tool_calls(calls, self.tools, rationale=rationale) + return self.provider.format_tool_results(calls, results) + + def execute_tool_calls( + self, + calls: Sequence[ToolCall], + *, + rationale: Optional[str] = None, + ) -> List[Any]: + if self.tools is None: + raise RuntimeError( + "create a session first via client.sessions.create(actor_id=...)" + ) + return execute_tool_calls(calls, self.tools, rationale=rationale) + + +__all__ = [ + "GatewayResources", + "Session", + "SessionsOperations", + "normalize_permissions", + "ToolsOperations", + "CodeOperations", + "normalize_invoke_arguments", + "GatewayTransport", + "RESTTransport", + "MCPTransport", + "MCP_PROTOCOL_VERSION", + "ACTOR_ID_HEADER", + "SESSION_ID_HEADER", + "session_mcp_url", + "BaseProvider", + "ChatCompletionsProvider", + "MessagesProvider", + "ResponsesProvider", + "default_provider", + "execute_tool_calls", + "simplify_inference_tool_schema", + "simplify_messages_input_schema", + "ToolCall", + "Toolbelt", + "GatewayToolError", + "GatewayProtocolError", + "ToolErrorClass", + "ToolResultStatus", + "RecoveryHint", + "META_SEARCH", + "META_INVOKE", + "META_CODE", + "META_TOOL_NAMES", + "DEFAULT_GATEWAY_BASE_URL", + "resolve_gateway_base_url", +] diff --git a/src/pydo/gateway/custom_models.py b/src/pydo/gateway/custom_models.py new file mode 100644 index 00000000..65b731f6 --- /dev/null +++ b/src/pydo/gateway/custom_models.py @@ -0,0 +1,188 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Action Gateway constants, errors, and shared value types.""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from azure.core.exceptions import HttpResponseError, ResourceExistsError + +# Meta-tool names exposed on the gateway's ``/mcp/meta`` endpoint. +META_SEARCH = "action_search" +META_INVOKE = "action_invoke" +META_CODE = "action_code" +META_TOOL_NAMES = frozenset({META_SEARCH, META_INVOKE, META_CODE}) + + +class ToolResultStatus: + """Status of a single tool invocation envelope.""" + + SUCCEEDED = "succeeded" + FAILED = "failed" + + +class ToolErrorClass: + """Closed error taxonomy for tool invocation failures.""" + + INVALID_ARGUMENT = "invalid_argument" + UNAUTHORIZED = "unauthorized" + FORBIDDEN = "forbidden" + RATE_LIMITED = "rate_limited" + NOT_FOUND = "not_found" + TIMEOUT = "timeout" + UPSTREAM_ERROR = "upstream_error" + OUTPUT_TOO_LARGE = "output_too_large" + EXECUTION_FAILED = "execution_failed" + UNAVAILABLE = "unavailable" + CANCELED = "canceled" + + +class RecoveryHint: + """Machine-routable hint on how a caller should recover from a failure.""" + + FIX_ARGS = "fix_args" + REFRESH_AUTH = "refresh_auth" + RETRY_LATER = "retry_later" + NARROW_OUTPUT = "narrow_output" + CONTACT_SUPPORT = "contact_support" + + +class GatewayToolError(RuntimeError): + """A tool invocation failed (gateway ``ToolResult`` error envelope). + + Raised when a single-tool operation (``invoke_one``, ``code.execute``, + ``tools.call``) fails, or when the MCP result reports ``isError``. + Batch ``invoke`` calls do NOT raise per-item failures; inspect the + envelope instead. + """ + + def __init__( + self, + message: str, + *, + error_class: Optional[str] = None, + retriable: Optional[bool] = None, + recovery_hint: Optional[str] = None, + invocation_id: Optional[str] = None, + details: Optional[Any] = None, + meta: Optional[Dict[str, Any]] = None, + ): + super().__init__(message) + self.message = message + self.error_class = error_class + self.retriable = retriable + self.recovery_hint = recovery_hint + self.invocation_id = invocation_id + self.details = details + self.meta = meta + + @classmethod + def from_error_payload( + cls, + error: Dict[str, Any], + *, + invocation_id: Optional[str] = None, + meta: Optional[Dict[str, Any]] = None, + ) -> "GatewayToolError": + return cls( + error.get("message") or "tool invocation failed", + error_class=error.get("class"), + retriable=error.get("retriable"), + recovery_hint=error.get("recovery_hint"), + invocation_id=invocation_id, + details=error, + meta=meta, + ) + + +class GatewayProtocolError(RuntimeError): + """A JSON-RPC protocol-level error from the gateway MCP endpoint.""" + + def __init__( + self, + message: str, + *, + code: Optional[int] = None, + data: Optional[Any] = None, + ): + super().__init__(message) + self.message = message + self.code = code + self.data = data + + +class ToolCall: + """A normalized tool call extracted from an inference response. + + ``arguments`` is always a decoded ``dict`` (providers JSON-decode the + vendor's string encoding when needed). + """ + + __slots__ = ("call_id", "name", "arguments") + + def __init__(self, call_id: str, name: str, arguments: Dict[str, Any]): + self.call_id = call_id + self.name = name + self.arguments = arguments + + def __repr__(self) -> str: # pragma: no cover - debug aid + return ( + f"ToolCall(call_id={self.call_id!r}, name={self.name!r}, " + f"arguments={self.arguments!r})" + ) + + +class Toolbelt(dict): + """A generated toolbelt response with a concise ``ref`` alias.""" + + @classmethod + def from_response(cls, response: Any) -> "Toolbelt": + """Accept the documented envelope and legacy flat API response.""" + if not isinstance(response, dict): + raise GatewayProtocolError( + f"unexpected toolbelt create response: {response!r}" + ) + data = response.get("toolbelt", response) + if not isinstance(data, dict) or not data.get("reference"): + raise GatewayProtocolError( + f"toolbelt create response missing toolbelt reference: {response!r}" + ) + return cls(data) + + @staticmethod + def validate_create_response( + pipeline_response: Any, response: Any, _headers: Any + ) -> Any: + """Raise for generated error responses before returning the body.""" + http_response = pipeline_response.http_response + if http_response.status_code == 409: + raise ResourceExistsError(response=http_response) + if http_response.status_code != 200: + raise HttpResponseError(response=http_response) + return response + + def __getattr__(self, name: str) -> Any: + if name == "ref": + return self.get("reference") + try: + return self[name] + except KeyError: + raise AttributeError(name) from None + + +__all__: List[str] = [ + "META_SEARCH", + "META_INVOKE", + "META_CODE", + "META_TOOL_NAMES", + "ToolResultStatus", + "ToolErrorClass", + "RecoveryHint", + "GatewayToolError", + "GatewayProtocolError", + "ToolCall", + "Toolbelt", +] diff --git a/src/pydo/gateway/custom_operations.py b/src/pydo/gateway/custom_operations.py new file mode 100644 index 00000000..511eed4e --- /dev/null +++ b/src/pydo/gateway/custom_operations.py @@ -0,0 +1,356 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Action Gateway operations (tools + sandboxed code execution). + +Every method delegates to a :class:`~pydo.gateway.transport.GatewayTransport`, +so the public return shapes hold regardless of the underlying wire protocol +(MCP JSON-RPC today, REST later). +""" + +from __future__ import annotations + +import json as _json +from typing import Any, Dict, List, Optional, Sequence, Union + +from .custom_models import ( + META_CODE, + META_INVOKE, + META_SEARCH, + GatewayToolError, + ToolResultStatus, +) +from .transport import GatewayTransport + +_MAX_SEARCH_QUERIES = 5 +_MAX_INVOKE_TOOLS = 10 + +QueryInput = Union[str, Dict[str, Any]] +ToolSpecInput = Dict[str, Any] + + +def _normalize_queries( + queries: Union[QueryInput, Sequence[QueryInput]], +) -> List[Dict[str, Any]]: + """Accept a single use-case str, a list of strs, or dicts with ``use_case``.""" + if isinstance(queries, (str, dict)): + queries = [queries] + normalized: List[Dict[str, Any]] = [] + for query in queries: + if isinstance(query, str): + entry: Dict[str, Any] = {"use_case": query} + elif isinstance(query, dict): + if not query.get("use_case"): + raise ValueError("each search query dict requires a 'use_case'") + entry = {"use_case": query["use_case"]} + if query.get("known_fields"): + entry["known_fields"] = query["known_fields"] + else: + raise TypeError("queries must be str or dict entries") + normalized.append(entry) + if not 1 <= len(normalized) <= _MAX_SEARCH_QUERIES: + raise ValueError(f"search accepts between 1 and {_MAX_SEARCH_QUERIES} queries") + return normalized + + +def _decode_json_object(value: Any) -> Dict[str, Any]: + if isinstance(value, str): + if not value.strip(): + return {} + return _json.loads(value) + if isinstance(value, dict): + return dict(value) + return {} + + +_INVOKE_ENTRY_RESERVED_KEYS = frozenset( + {"tool", "tool_slug", "name", "function", "type", "id"} +) + + +def _normalize_invoke_entry(spec: Any) -> Dict[str, Any]: + """Normalize one ``action.invoke`` tool entry to ``{tool, arguments}``. + + Models often emit chat-style ``{"function": {"name", "arguments"}}`` blobs + inside ``action.invoke`` even though the gateway expects ``tool`` / + ``tool_slug``. This helper accepts both shapes (plus a flat ``name`` key + and hoisted argument fields). + """ + if not isinstance(spec, dict): + raise TypeError( + "each invoke entry must be a dict like " + "{'tool': name, 'arguments': {...}}" + ) + + function = spec.get("function") + if isinstance(function, dict): + name = ( + function.get("name") + or spec.get("tool") + or spec.get("tool_slug") + or spec.get("name") + ) + if not name: + raise ValueError("each invoke entry requires a tool name") + if function.get("arguments") is not None: + arguments = _decode_json_object(function.get("arguments")) + else: + arguments = _decode_json_object(spec.get("arguments")) + return {"tool": name, "arguments": arguments} + + name = spec.get("tool") or spec.get("tool_slug") or spec.get("name") + if not name: + raise ValueError("each invoke entry requires a 'tool' name") + + arguments = spec.get("arguments") + if arguments is None: + hoisted = { + k: v for k, v in spec.items() if k not in _INVOKE_ENTRY_RESERVED_KEYS + } + arguments = hoisted if hoisted else {} + else: + arguments = _decode_json_object(arguments) + return {"tool": name, "arguments": arguments} + + +def normalize_invoke_arguments(arguments: Any) -> Dict[str, Any]: + """Normalize an ``action.invoke`` arguments object before calling the gateway.""" + if not isinstance(arguments, dict): + return {"tools": []} + normalized = dict(arguments) + tools = normalized.get("tools") + if tools is None: + return normalized + if isinstance(tools, dict): + tools = [tools] + elif not isinstance(tools, list): + tools = [tools] + normalized["tools"] = [_normalize_invoke_entry(entry) for entry in tools] + return normalized + + +def _normalize_tool_specs(tools: Sequence[ToolSpecInput]) -> List[Dict[str, Any]]: + """Normalize invoke entries; ``tool_slug`` is accepted as alias for ``tool``.""" + normalized = [_normalize_invoke_entry(spec) for spec in tools] + if not 1 <= len(normalized) <= _MAX_INVOKE_TOOLS: + raise ValueError(f"invoke accepts between 1 and {_MAX_INVOKE_TOOLS} tools") + return normalized + + +def _result_output_or_raise(item_result: Any, tool_name: str) -> Any: + """Unwrap one invoke ``ToolResult`` envelope; raise on failure.""" + get = getattr(item_result, "get", None) + if get is None: + return item_result + status = get("status") + if status and status != ToolResultStatus.SUCCEEDED: + error = get("error") or {} + raise GatewayToolError.from_error_payload( + dict(error) if error else {"message": f"tool {tool_name!r} failed"}, + invocation_id=get("invocation_id"), + ) + return get("output") + + +class ToolsOperations: + """Action Gateway tool discovery and invocation. + + Calling the instance itself (``client.gateway.tools()``) returns + provider-formatted tool definitions ready for an inference ``tools=`` + parameter — see :mod:`pydo.gateway.providers`. + """ + + def __init__(self, transport: GatewayTransport, provider: Any = None): + self._transport = transport + self._provider = provider + + # -- discovery --------------------------------------------------------- + + def list(self, *, include_all: bool = False) -> Any: + """List available tools. + + By default returns the three meta-tools (``action.search``, + ``action.invoke``, ``action.code``) — the intended agent workflow. + Pass ``include_all=True`` for every tool exposed on the session MCP + endpoint, including configured ``preloadTools``. + """ + return self._transport.list_tools(meta=not include_all) + + def search( + self, + queries: Union[QueryInput, Sequence[QueryInput]], + *, + providers: Optional[Sequence[str]] = None, + tags: Optional[Sequence[str]] = None, + limit: Optional[int] = None, + ) -> Any: + """Search the tool catalog by use case (``action.search``). + + :param queries: A use-case string, a list of strings, or dicts with + ``use_case`` (and optional ``known_fields``). 1–5 queries. + :param providers: Optional provider filters (e.g. ``["exa"]``). + :param tags: Optional tag filters. + :param limit: Per-query result cap. + """ + arguments: Dict[str, Any] = {"queries": _normalize_queries(queries)} + if providers: + arguments["providers"] = list(providers) + if tags: + arguments["tags"] = list(tags) + if limit is not None: + arguments["limit"] = limit + return self._transport.call_tool(META_SEARCH, arguments, meta=True) + + # -- execution --------------------------------------------------------- + + def invoke( + self, + tools: Sequence[ToolSpecInput], + *, + rationale: Optional[str] = None, + ) -> Any: + """Invoke 1–10 tools in parallel (``action.invoke``). + + Returns the full envelope (``total_count`` / ``success_count`` / + ``error_count`` / ``results[]``). Per-tool failures are reported + inside the envelope and do NOT raise. + """ + arguments: Dict[str, Any] = {"tools": _normalize_tool_specs(tools)} + if rationale: + arguments["rationale"] = rationale + return self._transport.call_tool(META_INVOKE, arguments, meta=True) + + def invoke_one( + self, + name: str, + arguments: Optional[Dict[str, Any]] = None, + *, + rationale: Optional[str] = None, + ) -> Any: + """Invoke a single tool and return its output directly. + + Raises :class:`GatewayToolError` if the tool failed. + """ + envelope = self.invoke( + [{"tool": name, "arguments": arguments or {}}], + rationale=rationale, + ) + get = getattr(envelope, "get", None) + results = (get("results") if get else None) or [] + if not results: + raise GatewayToolError(f"invoke of {name!r} returned no results") + first = results[0] + item_result = (getattr(first, "get", lambda *_: first)("result")) or first + return _result_output_or_raise(item_result, name) + + def call(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Any: + """Call one concrete catalog tool directly (``tools/call`` on ``/mcp``). + + Unlike :meth:`invoke`, the result is the tool's output payload with + no invoke envelope; failures raise :class:`GatewayToolError`. + """ + return self._transport.call_tool(name, arguments or {}, meta=False) + + # -- inference integration (Composio-style) ----------------------------- + + def __call__( + self, + *, + include_all: bool = False, + names: Optional[Sequence[str]] = None, + search: Optional[Union[QueryInput, Sequence[QueryInput]]] = None, + providers: Optional[Sequence[str]] = None, + tags: Optional[Sequence[str]] = None, + limit: Optional[int] = None, + ) -> List[Any]: + """Return provider-formatted tool definitions for ``tools=``. + + By default wraps the three meta-tools so the model drives the + search → invoke → code workflow itself. Pass ``include_all=True``, + ``names=``, or ``search=`` to wrap selected tools instead. + """ + if self._provider is None: + raise RuntimeError( + "no gateway provider configured; pass gateway_provider= to " + "Client() or use tools.list()/tools.invoke() directly" + ) + catalog = self._fetch_catalog( + include_all=include_all, + names=names, + search=search, + providers=providers, + tags=tags, + limit=limit, + ) + return self._provider.wrap_tools(catalog) + + def _fetch_catalog( + self, + *, + include_all: bool, + names: Optional[Sequence[str]], + search: Optional[Union[QueryInput, Sequence[QueryInput]]], + providers: Optional[Sequence[str]], + tags: Optional[Sequence[str]], + limit: Optional[int], + ) -> List[Any]: + if search is not None: + payload = self.search(search, providers=providers, tags=tags, limit=limit) + return _flatten_search_results(payload) + wants_concrete = include_all or bool(names) + tools = self.list(include_all=wants_concrete) + if names: + wanted = set(names) + tools = [t for t in tools if _tool_name(t) in wanted] + missing = wanted - {_tool_name(t) for t in tools} + if missing: + raise LookupError(f"tools not found in catalog: {sorted(missing)}") + return list(tools) + + +def _tool_name(tool: Any) -> Optional[str]: + get = getattr(tool, "get", None) + return get("name") if get else getattr(tool, "name", None) + + +def _flatten_search_results(payload: Any) -> List[Any]: + """Flatten an ``action.search`` payload into a deduplicated tool list.""" + get = getattr(payload, "get", None) + groups = (get("results") if get else None) or [] + seen: Dict[str, Any] = {} + for group in groups: + group_get = getattr(group, "get", None) + matches = (group_get("results") if group_get else None) or [] + for match in matches: + name = _tool_name(match) + if name and name not in seen: + seen[name] = match + return list(seen.values()) + + +class CodeOperations: + """Ephemeral Python sandbox execution (``action.code``).""" + + def __init__(self, transport: GatewayTransport): + self._transport = transport + + def execute(self, code: str, *, thought: Optional[str] = None) -> Any: + """Run Python code in the gateway sandbox. + + Returns the execution output (``stdout`` / ``stderr`` / + ``exit_code``). Raises :class:`GatewayToolError` on sandbox failure. + """ + if not code or not code.strip(): + raise ValueError("code is empty") + arguments: Dict[str, Any] = {"code": code} + if thought: + arguments["thought"] = thought + return self._transport.call_tool(META_CODE, arguments, meta=True) + + +__all__ = [ + "ToolsOperations", + "CodeOperations", + "normalize_invoke_arguments", +] diff --git a/src/pydo/gateway/providers.py b/src/pydo/gateway/providers.py new file mode 100644 index 00000000..4a0f1edf --- /dev/null +++ b/src/pydo/gateway/providers.py @@ -0,0 +1,363 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Inference providers — translate gateway tools to/from vendor formats. + +pydo exposes three inference surfaces with three different tool wire +formats. A provider owns both directions of translation for one of them +(the Composio pattern): + +* :class:`ChatCompletionsProvider` — ``client.chat.completions.create`` + (OpenAI chat completions format). The default. +* :class:`MessagesProvider` — ``client.messages.create`` (Anthropic + Messages format). +* :class:`ResponsesProvider` — ``client.responses.create`` (OpenAI + Responses format). + +Usage:: + + client = Client(token=..., gateway_provider=MessagesProvider()) + tools = client.gateway.tools() # vendor tools= format + resp = client.messages.create(..., tools=tools, messages=messages) + messages += client.gateway.handle_tool_calls(resp) +""" + +from __future__ import annotations + +import copy +import json as _json +from typing import Any, Dict, List, Optional, Sequence + +from .custom_models import ToolCall +from .custom_operations import _normalize_invoke_entry, normalize_invoke_arguments + + +def simplify_inference_tool_schema(schema: Any) -> Dict[str, Any]: + """Normalize a gateway tool JSON Schema for inference ``tools=`` parameters. + + DO inference endpoints (chat completions, messages, responses) require a + plain top-level ``object`` schema. Gateway meta-tools such as + ``action.code`` use top-level combinators (``anyOf`` for Composio alias + args). We drop those keywords and keep ``properties``; the gateway still + validates aliases server-side. + """ + if not isinstance(schema, dict): + return {"type": "object", "properties": {}} + simplified = copy.deepcopy(schema) + for key in ( + "oneOf", + "allOf", + "anyOf", + "enum", + "const", + "not", + ): + simplified.pop(key, None) + if "type" not in simplified: + simplified["type"] = "object" + if simplified.get("type") == "object" and "properties" not in simplified: + simplified["properties"] = {} + return simplified + + +# Backward-compatible alias. +simplify_messages_input_schema = simplify_inference_tool_schema + + +def _get(obj: Any, key: str, default: Any = None) -> Any: + """Uniform field access over dicts/DotDicts and attribute objects.""" + getter = getattr(obj, "get", None) + if getter is not None: + return getter(key, default) + return getattr(obj, key, default) + + +def _decode_arguments(arguments: Any) -> Dict[str, Any]: + if isinstance(arguments, str): + if not arguments.strip(): + return {} + return _json.loads(arguments) + if isinstance(arguments, dict): + return dict(arguments) + return {} + + +def _tool_fields(tool: Any) -> Dict[str, Any]: + """Extract canonical fields from a gateway catalog/meta tool definition.""" + return { + "name": _get(tool, "name"), + "description": _get(tool, "description") or _get(tool, "title") or "", + "parameters": simplify_inference_tool_schema( + _get(tool, "inputSchema") or {"type": "object"} + ), + } + + +def _result_to_content(result: Any) -> str: + """Serialize one tool result (output or error payload) for the model.""" + if isinstance(result, str): + return result + try: + return _json.dumps(result, default=str) + except (TypeError, ValueError): + return str(result) + + +class BaseProvider: + """Translation contract between gateway tools and one inference surface.""" + + name = "base" + + def wrap_tools(self, catalog_tools: Sequence[Any]) -> List[Dict[str, Any]]: + """Convert canonical gateway tool defs to the vendor ``tools=`` format.""" + raise NotImplementedError + + def extract_tool_calls(self, response: Any) -> List[ToolCall]: + """Extract normalized tool calls from a vendor response object.""" + raise NotImplementedError + + def format_tool_results( + self, + calls: Sequence[ToolCall], + results: Sequence[Any], + ) -> List[Dict[str, Any]]: + """Convert invocation outputs into vendor-shaped result messages/items.""" + raise NotImplementedError + + +class ChatCompletionsProvider(BaseProvider): + """OpenAI chat-completions format (``client.chat.completions.create``).""" + + name = "chat.completions" + + def wrap_tools(self, catalog_tools: Sequence[Any]) -> List[Dict[str, Any]]: + return [ + {"type": "function", "function": _tool_fields(tool)} + for tool in catalog_tools + ] + + def extract_tool_calls(self, response: Any) -> List[ToolCall]: + choices = _get(response, "choices") or [] + if not choices: + return [] + message = _get(choices[0], "message") or {} + calls = [] + for tool_call in _get(message, "tool_calls") or []: + function = _get(tool_call, "function") or {} + calls.append( + ToolCall( + call_id=_get(tool_call, "id") or "", + name=_get(function, "name") or "", + arguments=_decode_arguments(_get(function, "arguments")), + ) + ) + return calls + + def format_tool_results( + self, + calls: Sequence[ToolCall], + results: Sequence[Any], + ) -> List[Dict[str, Any]]: + return [ + { + "role": "tool", + "tool_call_id": call.call_id, + "content": _result_to_content(result), + } + for call, result in zip(calls, results) + ] + + +class MessagesProvider(BaseProvider): + """Anthropic Messages format (``client.messages.create``).""" + + name = "messages" + + def wrap_tools(self, catalog_tools: Sequence[Any]) -> List[Dict[str, Any]]: + wrapped = [] + for tool in catalog_tools: + fields = _tool_fields(tool) + wrapped.append( + { + "name": fields["name"], + "description": fields["description"], + "input_schema": fields["parameters"], + } + ) + return wrapped + + def extract_tool_calls(self, response: Any) -> List[ToolCall]: + calls = [] + for block in _get(response, "content") or []: + if _get(block, "type") != "tool_use": + continue + calls.append( + ToolCall( + call_id=_get(block, "id") or "", + name=_get(block, "name") or "", + arguments=_decode_arguments(_get(block, "input")), + ) + ) + return calls + + def format_tool_results( + self, + calls: Sequence[ToolCall], + results: Sequence[Any], + ) -> List[Dict[str, Any]]: + if not calls: + return [] + content = [ + { + "type": "tool_result", + "tool_use_id": call.call_id, + "content": _result_to_content(result), + } + for call, result in zip(calls, results) + ] + # Anthropic expects all tool results in a single user turn. + return [{"role": "user", "content": content}] + + +class ResponsesProvider(BaseProvider): + """OpenAI Responses format (``client.responses.create``).""" + + name = "responses" + + def wrap_tools(self, catalog_tools: Sequence[Any]) -> List[Dict[str, Any]]: + return [{"type": "function", **_tool_fields(tool)} for tool in catalog_tools] + + def extract_tool_calls(self, response: Any) -> List[ToolCall]: + calls = [] + for item in _get(response, "output") or []: + if _get(item, "type") != "function_call": + continue + calls.append( + ToolCall( + call_id=_get(item, "call_id") or _get(item, "id") or "", + name=_get(item, "name") or "", + arguments=_decode_arguments(_get(item, "arguments")), + ) + ) + return calls + + def format_tool_results( + self, + calls: Sequence[ToolCall], + results: Sequence[Any], + ) -> List[Dict[str, Any]]: + return [ + { + "type": "function_call_output", + "call_id": call.call_id, + "output": _result_to_content(result), + } + for call, result in zip(calls, results) + ] + + +def default_provider() -> BaseProvider: + return ChatCompletionsProvider() + + +def execute_tool_calls( + calls: Sequence[ToolCall], + tools_operations: Any, + *, + rationale: Optional[str] = None, +) -> List[Any]: + """Execute normalized tool calls against the gateway. + + Meta-tool calls (``action.search`` / ``action.invoke`` / ``action.code``) + go straight through; concrete tool names are batched through one + ``action.invoke``. Per-tool failures become structured error payloads + rather than raising, so the model can observe and recover. + """ + from .custom_models import META_INVOKE, META_TOOL_NAMES, GatewayToolError + + results: List[Any] = [None] * len(calls) + concrete: List[int] = [] + + for index, call in enumerate(calls): + if call.name in META_TOOL_NAMES: + try: + arguments = call.arguments + if call.name == META_INVOKE: + arguments = normalize_invoke_arguments(arguments) + results[index] = tools_operations._transport.call_tool( + call.name, arguments, meta=True + ) + except ( + GatewayToolError, + TypeError, + ValueError, + _json.JSONDecodeError, + ) as exc: + results[index] = _error_payload(exc) + else: + concrete.append(index) + + if concrete: + try: + batch = [ + _normalize_invoke_entry( + {"tool": calls[i].name, "arguments": calls[i].arguments} + ) + for i in concrete + ] + except (TypeError, ValueError, _json.JSONDecodeError) as exc: + error = _error_payload(exc) + for index in concrete: + results[index] = error + return results + envelope = tools_operations.invoke(batch, rationale=rationale) + items = (_get(envelope, "results") or []) if envelope is not None else [] + for position, index in enumerate(concrete): + if position < len(items): + item = items[position] + item_result = _get(item, "result") or item + status = _get(item_result, "status") + if status and status != "succeeded": + error_result = { + "error": _get(item_result, "error") + or {"message": f"tool {calls[index].name!r} failed"} + } + meta = _get(item_result, "_meta") + if meta: + error_result["_meta"] = meta + results[index] = error_result + else: + results[index] = _get(item_result, "output") + else: + results[index] = { + "error": {"message": "no result returned for this tool call"} + } + return results + + +def _error_payload(exc: Any) -> Dict[str, Any]: + payload = { + "error": { + "message": str(exc), + "class": getattr(exc, "error_class", None), + "retriable": getattr(exc, "retriable", None), + "recovery_hint": getattr(exc, "recovery_hint", None), + } + } + meta = getattr(exc, "meta", None) + if isinstance(meta, dict) and meta: + payload["_meta"] = meta + return payload + + +__all__ = [ + "BaseProvider", + "ChatCompletionsProvider", + "MessagesProvider", + "ResponsesProvider", + "default_provider", + "execute_tool_calls", + "simplify_inference_tool_schema", + "simplify_messages_input_schema", +] diff --git a/src/pydo/gateway/session.py b/src/pydo/gateway/session.py new file mode 100644 index 00000000..103e361e --- /dev/null +++ b/src/pydo/gateway/session.py @@ -0,0 +1,254 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +# pylint: disable=duplicate-code +"""Action Gateway sessions — create on the DO API, execute on the gateway.""" + +from __future__ import annotations + +import uuid +from typing import Any, Dict, List, Optional, Sequence + +from pydo.custom_extensions import _BaseURLProxy + +from .custom_models import GatewayProtocolError +from .custom_operations import CodeOperations, ToolsOperations +from .providers import BaseProvider, default_provider, execute_tool_calls +from .transport import ( + MCPTransport, + resolve_gateway_base_url, +) + +_DEFAULT_POLICY: Dict[str, Any] = {"defaultAction": "ask"} + + +def _pick(data: Dict[str, Any], *keys: str) -> Any: + for key in keys: + if key in data and data[key] is not None: + return data[key] + return None + + +def normalize_permissions(permissions: Optional[Dict[str, Any]]) -> Dict[str, Any]: + """Normalize SDK permissions into the wire policy object. + + Accepts snake_case ``default_action`` or wire ``defaultAction``. When + omitted, returns ``{"defaultAction": "ask"}``. + """ + if permissions is None: + return dict(_DEFAULT_POLICY) + + default_action = ( + permissions.get("default_action") + if "default_action" in permissions + else permissions.get("defaultAction", "ask") + ) + rules_in = permissions.get("rules") or [] + rules: List[Dict[str, Any]] = [] + for rule in rules_in: + if not isinstance(rule, dict): + raise TypeError("each permissions rule must be a dict") + if "toolbelt" in rule: + raise ValueError( + "toolbelt permissions are no longer supported; " + "use a tool value such as 'toolbelt:my-belt@1'" + ) + entry: Dict[str, Any] = {"action": rule.get("action") or "allow"} + if rule.get("tool"): + entry["tool"] = rule["tool"] + if rule.get("match"): + entry["match"] = rule["match"] + if "tool" not in entry: + raise ValueError("each permissions rule requires tool") + rules.append(entry) + return {"defaultAction": default_action, "rules": rules} + + +class Session: + """A gateway session bound to an ``actor_id`` and tool policy. + + Create via :meth:`SessionsOperations.create`. Use ``url`` for external + MCP clients, ``tools()`` for inference ``tools=``, and + ``handle_tool_calls`` to execute model tool calls over MCP. + """ + + def __init__( + self, + *, + session_urn: str, + actor_id: str, + name: str, + policy: Dict[str, Any], + mcp_url: str, + tools: ToolsOperations, + code: CodeOperations, + provider: BaseProvider, + selected_tools: Optional[Sequence[str]] = None, + raw: Optional[Dict[str, Any]] = None, + ): + self.session_urn = session_urn + self.id = session_urn + self.actor_id = actor_id + self.name = name + self.policy = policy + self._mcp_url = mcp_url + self.tools = tools + self.code = code + self._transport = tools._transport + self.provider = provider + self.selected_tools = list(selected_tools or []) + self.raw = raw or {} + + @property + def url(self) -> str: + """Session-pinned MCP URL for external MCP clients.""" + return self._mcp_url + + def handle_tool_calls( + self, + response: Any, + *, + rationale: Optional[str] = None, + ) -> List[Any]: + """Execute tool calls from an inference response against this session.""" + calls = self.provider.extract_tool_calls(response) + if not calls: + return [] + results = execute_tool_calls(calls, self.tools, rationale=rationale) + return self.provider.format_tool_results(calls, results) + + def execute_tool_calls( + self, + calls: Sequence[Any], + *, + rationale: Optional[str] = None, + ) -> List[Any]: + """Execute pre-extracted tool calls; return raw outputs.""" + return execute_tool_calls(calls, self.tools, rationale=rationale) + + def approve(self, approval_id: str) -> Any: + """Approve a pending tool invocation for this session.""" + return self._transport.decide_approval(approval_id, "approve") + + def deny(self, approval_id: str) -> Any: + """Deny a pending tool invocation for this session.""" + return self._transport.decide_approval(approval_id, "deny") + + def __repr__(self) -> str: # pragma: no cover - debug aid + return f"" + + +class SessionsOperations: + """Create sessions through the generated Action Gateway operation.""" + + def __init__( + self, + parent_client: Any, + *, + gateway_endpoint: Optional[str] = None, + provider: Optional[BaseProvider] = None, + ): + self._parent = parent_client + self._sessions_api = parent_client.sessions + self._gateway_base_url = resolve_gateway_base_url(gateway_endpoint) + self._provider = provider or default_provider() + + def create( + self, + actor_id: str, + *, + name: Optional[str] = None, + permissions: Optional[Dict[str, Any]] = None, + tools: Optional[Sequence[str]] = None, + config: Optional[Dict[str, Any]] = None, + ) -> Session: + """Create a session. + + :param actor_id: Required actor identifier used to evaluate the policy. + :param name: Optional display name (auto-generated when omitted). + :param permissions: Optional policy. When omitted, defaults to + ``{"defaultAction": "ask"}``. + :param tools: Optional tool or version-pinned toolbelt references. + Omit for all tools; pass an empty sequence for no tools. + :param config: Optional session configuration, including + ``preloadTools``. + """ + if not actor_id or not str(actor_id).strip(): + raise ValueError("actor_id is required") + + session_name = name or f"pydo-session-{uuid.uuid4().hex[:8]}" + policy = normalize_permissions(permissions) + body = { + "name": session_name, + "policy": policy, + "actor_id": str(actor_id).strip(), + } + if tools is not None: + if isinstance(tools, (str, bytes)): + raise TypeError("tools must be a sequence of tool references") + body["tools"] = list(tools) + if config is not None: + if not isinstance(config, dict): + raise TypeError("config must be a dict") + body["config"] = config + + raw_session = self._post_create(body) + session_urn = _pick(raw_session, "sessionUrn", "session_urn") + if not session_urn: + raise GatewayProtocolError( + f"session create response missing sessionUrn: {raw_session!r}" + ) + + mcp_url = _pick(raw_session, "mcpUrl", "mcp_url") + if not mcp_url: + raise GatewayProtocolError( + f"session create response missing mcpUrl: {raw_session!r}" + ) + + transport = MCPTransport( + _BaseURLProxy(self._parent._client, self._gateway_base_url), + session_id=session_urn, + actor_id=actor_id, + endpoint_url=mcp_url, + ) + tools = ToolsOperations(transport, self._provider) + code = CodeOperations(transport) + return Session( + session_urn=session_urn, + actor_id=str(actor_id).strip(), + name=_pick(raw_session, "name") or session_name, + policy=policy, + mcp_url=mcp_url, + tools=tools, + code=code, + provider=self._provider, + selected_tools=_pick(raw_session, "selectedTools") or [], + raw=raw_session, + ) + + def _post_create(self, body: Dict[str, Any]) -> Dict[str, Any]: + payload = self._sessions_api.create(body=body) + if not isinstance(payload, dict): + raise GatewayProtocolError( + f"unexpected session create response: {payload!r}" + ) + session = payload.get("session") + if not isinstance(session, dict): + raise GatewayProtocolError( + f"session create response missing session object: {payload!r}" + ) + result = dict(session) + mcp_url = _pick(payload, "mcpUrl", "mcp_url") + if mcp_url: + result["mcpUrl"] = mcp_url + if "tools" in payload: + result["selectedTools"] = payload["tools"] + return result + + +__all__ = [ + "Session", + "SessionsOperations", + "normalize_permissions", +] diff --git a/src/pydo/gateway/transport.py b/src/pydo/gateway/transport.py new file mode 100644 index 00000000..30851c31 --- /dev/null +++ b/src/pydo/gateway/transport.py @@ -0,0 +1,543 @@ +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Action Gateway wire layer. + +The public SDK surface (``ToolsOperations`` / ``CodeOperations``) only talks +to the small :class:`GatewayTransport` interface. Sessions use MCP JSON-RPC at +the endpoint returned by the create API. REST transports remain available for +the compatibility routes and focused testing. +""" + +from __future__ import annotations + +import itertools +import json as _json +import os +from typing import Any, Dict, List, Optional +from urllib.parse import quote, urlsplit + +from azure.core.exceptions import ( + ClientAuthenticationError, + HttpResponseError, + ResourceExistsError, + ResourceNotFoundError, + ResourceNotModifiedError, +) +from azure.core.rest import HttpRequest + +from pydo.custom_extensions import _wrap + +from .custom_models import ( + META_CODE, + META_INVOKE, + META_SEARCH, + GatewayProtocolError, + GatewayToolError, + ToolResultStatus, +) + +DEFAULT_GATEWAY_BASE_URL = "https://actions.do-ai.run" +_ENV_VAR = "PYDO_GATEWAY_ENDPOINT" + + +def resolve_gateway_base_url(explicit: Optional[str] = None) -> str: + url = explicit or os.environ.get(_ENV_VAR) or DEFAULT_GATEWAY_BASE_URL + url = url.rstrip("/") + if "://" not in url: + url = f"https://{url}" + return url + + +_ERROR_MAP = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, +} + +MCP_PROTOCOL_VERSION = "2025-06-18" +SESSION_ID_HEADER = "X-Session-Id" +ACTOR_ID_HEADER = "X-Actor-Id" + +_MCP_PATH = "/mcp" +_MCP_META_PATH = "/mcp/meta" +_REST_TOOLS_PATH = "/tools" +_REST_SEARCH_PATH = "/tools/search" +_REST_INVOKE_PATH = "/tools/invoke" +_REST_CODE_PATH = "/code/execute" + +_MCP_HEADERS = { + "Content-Type": "application/json", + "MCP-Protocol-Version": MCP_PROTOCOL_VERSION, + "Accept": "application/json, text/event-stream", +} + +_REST_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", +} + +# Static meta-tool catalog for REST list(meta=True). Mirrors /mcp/meta. +_META_TOOL_DEFINITIONS: List[Dict[str, Any]] = [ + { + "name": META_SEARCH, + "title": "Action Search", + "description": ( + "Discover the catalog tools needed to satisfy one or more user " + "use cases. Call this before action_invoke whenever you need a " + "catalog tool you do not already have." + ), + "inputSchema": { + "type": "object", + "properties": { + "queries": { + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": { + "type": "object", + "properties": { + "use_case": {"type": "string"}, + "known_fields": {"type": "string"}, + }, + "required": ["use_case"], + }, + }, + "providers": {"type": "array", "items": {"type": "string"}}, + "tags": {"type": "array", "items": {"type": "string"}}, + "limit": {"type": "integer"}, + }, + "required": ["queries"], + }, + }, + { + "name": META_INVOKE, + "title": "Action Invoke", + "description": "Invoke 1–10 catalog tools in parallel.", + "inputSchema": { + "type": "object", + "properties": { + "tools": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "items": { + "type": "object", + "properties": { + "tool": {"type": "string"}, + "tool_slug": {"type": "string"}, + "arguments": {"type": "object"}, + }, + "anyOf": [ + {"required": ["tool"]}, + {"required": ["tool_slug"]}, + ], + }, + }, + "rationale": {"type": "string", "maxLength": 512}, + }, + "required": ["tools"], + }, + }, + { + "name": META_CODE, + "title": "Action Code", + "description": ( + "Run Python in an ephemeral sandbox. Use for computation, " + "parsing, or data processing — no prior action_search needed." + ), + "inputSchema": { + "type": "object", + "properties": { + "code": {"type": "string"}, + "code_to_execute": {"type": "string"}, + "thought": {"type": "string"}, + }, + "anyOf": [ + {"required": ["code"]}, + {"required": ["code_to_execute"]}, + ], + }, + }, +] + + +def _response_body_text(response: Any) -> str: + try: + body = response.text() if hasattr(response, "text") else response.body() + if isinstance(body, bytes): + body = body.decode("utf-8", errors="replace") + return body or "" + except Exception: # noqa: BLE001 — best-effort error detail for callers + return "" + + +def _raise_gateway_http_error(response: Any) -> None: + body = _response_body_text(response) + message = body.strip() or getattr(response, "reason", None) or "request failed" + if response.status_code == 412: + message = ( + "team is not enabled for the Action Infra release " + f"(412 Precondition Failed): {message}" + ) + error_type = _ERROR_MAP.get(response.status_code) + if error_type: + raise error_type( + message=message, + response=response, + error_format=lambda _body: None, + ) + raise HttpResponseError(message=message, response=response) + + +def _content_text(content: Optional[List[Dict[str, Any]]]) -> str: + parts = [] + for block in content or []: + if isinstance(block, dict) and block.get("type") == "text": + parts.append(block.get("text") or "") + return "\n".join(p for p in parts if p) + + +def _unwrap_call_result(result: Dict[str, Any]) -> Any: + """Normalize an MCP ``tools/call`` result to its useful payload.""" + if result.get("isError"): + structured = result.get("structuredContent") + meta = result.get("_meta") + error = None + if isinstance(structured, dict): + error = structured.get("error") or ( + structured if "message" in structured else None + ) + if error: + raise GatewayToolError.from_error_payload( + error, + invocation_id=( + structured.get("invocation_id") + if isinstance(structured, dict) + else None + ), + meta=meta if isinstance(meta, dict) else None, + ) + raise GatewayToolError( + _content_text(result.get("content")) or "tool call failed", + meta=meta if isinstance(meta, dict) else None, + ) + + structured = result.get("structuredContent") + if structured is not None: + return _wrap(structured) + + text = _content_text(result.get("content")) + try: + return _wrap(_json.loads(text)) + except (TypeError, ValueError): + return text + + +def _parse_json_body(body: Any) -> Any: + if isinstance(body, bytes): + body = body.decode("utf-8", errors="replace") + if isinstance(body, (dict, list)): + return body + try: + return _json.loads(body) + except (TypeError, ValueError) as exc: + raise GatewayProtocolError( + f"gateway returned a non-JSON response: {body!r}" + ) from exc + + +def _parse_jsonrpc(body: Any) -> Dict[str, Any]: + if isinstance(body, bytes): + body = body.decode("utf-8", errors="replace") + if isinstance(body, str) and any( + line.startswith("data:") for line in body.splitlines() + ): + events = [] + data_lines = [] + for line in body.splitlines(): + if not line: + if data_lines: + events.append("\n".join(data_lines)) + data_lines = [] + continue + if line.startswith("data:"): + data_lines.append(line[5:].lstrip()) + if data_lines: + events.append("\n".join(data_lines)) + for event in events: + try: + candidate = _parse_json_body(event) + except GatewayProtocolError: + continue + if isinstance(candidate, dict) and ( + "result" in candidate or "error" in candidate + ): + body = candidate + break + envelope = _parse_json_body(body) + if not isinstance(envelope, dict): + raise GatewayProtocolError( + f"gateway returned an unexpected JSON-RPC envelope: {envelope!r}" + ) + error = envelope.get("error") + if error: + raise GatewayProtocolError( + error.get("message") or "JSON-RPC error", + code=error.get("code"), + data=error.get("data"), + ) + result = envelope.get("result") + if not isinstance(result, dict): + raise GatewayProtocolError( + f"gateway JSON-RPC response is missing a result: {envelope!r}" + ) + return result + + +def _decode_output(value: Any) -> Any: + if isinstance(value, (bytes, bytearray)): + value = value.decode("utf-8", errors="replace") + if isinstance(value, str): + try: + return _json.loads(value) + except (TypeError, ValueError): + return value + return value + + +def _unwrap_tool_result(payload: Any) -> Any: + """Unwrap a REST ``ToolResult`` envelope; raise on failure.""" + if not isinstance(payload, dict): + return _wrap(payload) + status = payload.get("status") + if status and status != ToolResultStatus.SUCCEEDED: + error = payload.get("error") or {} + raise GatewayToolError.from_error_payload( + dict(error) if isinstance(error, dict) else {"message": str(error)}, + invocation_id=payload.get("invocation_id") or payload.get("call_id"), + ) + if "output" in payload: + return _wrap(_decode_output(payload.get("output"))) + return _wrap(payload) + + +def session_mcp_url(gateway_base_url: str, session_urn: str) -> str: + """Build the session-pinned MCP URL for external MCP clients.""" + base = gateway_base_url.rstrip("/") + session_id = _external_session_id(session_urn) + return f"{base}/mcp/session/{session_id}" + + +def _external_session_id(session_urn: str) -> str: + """Return the bare session ID accepted by Action Gateway ingress.""" + return session_urn.rsplit(":", 1)[-1] + + +class GatewayTransport: + """Swappable wire layer; MCP semantics are the lowest common denominator.""" + + def list_tools(self, *, meta: bool) -> List[Any]: + raise NotImplementedError + + def call_tool(self, name: str, arguments: Dict[str, Any], *, meta: bool) -> Any: + raise NotImplementedError + + def decide_approval(self, approval_id: str, decision: str) -> Any: + raise NotImplementedError + + def approve(self, approval_id: str) -> Any: + return self.decide_approval(approval_id, "approve") + + +class RESTTransport(GatewayTransport): + """REST over ``/tools``, ``/tools/search``, ``/tools/invoke``, ``/code/execute``. + + Requires a session URN or ID and actor ID on every request. + """ + + def __init__(self, base_url_proxy: Any, *, session_id: str, actor_id: str): + if not session_id: + raise ValueError("session_id is required for RESTTransport") + if not actor_id or not str(actor_id).strip(): + raise ValueError("actor_id is required for RESTTransport") + self._client = base_url_proxy + self.session_id = _external_session_id(session_id) + self.actor_id = str(actor_id).strip() + + def _headers(self) -> Dict[str, str]: + headers = dict(_REST_HEADERS) + headers[SESSION_ID_HEADER] = self.session_id + headers[ACTOR_ID_HEADER] = self.actor_id + return headers + + def _request( + self, + method: str, + path: str, + payload: Optional[Dict[str, Any]] = None, + ) -> Any: + kwargs: Dict[str, Any] = {"headers": self._headers()} + if payload is not None: + kwargs["json"] = payload + request = HttpRequest(method, path, **kwargs) + request.url = self._client.format_url(request.url) + pipeline_response = self._client._pipeline.run(request) + response = pipeline_response.http_response + body = response.text() if hasattr(response, "text") else response.body() + if response.status_code != 200: + _raise_gateway_http_error(response) + return _parse_json_body(body) + + def list_tools(self, *, meta: bool) -> List[Any]: + if meta: + return _wrap([dict(tool) for tool in _META_TOOL_DEFINITIONS]) + catalog = self._request("GET", _REST_TOOLS_PATH) + if isinstance(catalog, dict): + return _wrap(catalog.get("tools") or []) + return _wrap(catalog or []) + + def call_tool(self, name: str, arguments: Dict[str, Any], *, meta: bool) -> Any: + arguments = arguments or {} + if name == META_SEARCH or (meta and name == META_SEARCH): + return _unwrap_tool_result( + self._request("POST", _REST_SEARCH_PATH, arguments) + ) + if name == META_INVOKE or (meta and name == META_INVOKE): + # Invoke returns the batch envelope directly (not ToolResult). + return _wrap(self._request("POST", _REST_INVOKE_PATH, arguments)) + if name == META_CODE or (meta and name == META_CODE): + return _unwrap_tool_result( + self._request("POST", _REST_CODE_PATH, arguments) + ) + # Concrete catalog tool → single-item invoke. + envelope = self._request( + "POST", + _REST_INVOKE_PATH, + {"tools": [{"tool": name, "arguments": arguments}]}, + ) + results = (envelope or {}).get("results") or [] + if not results: + raise GatewayToolError(f"invoke of {name!r} returned no results") + item = results[0] + item_result = item.get("result") if isinstance(item, dict) else item + return _unwrap_tool_result(item_result) + + def decide_approval(self, approval_id: str, decision: str) -> Any: + if not approval_id or not str(approval_id).strip(): + raise ValueError("approval_id is required") + if decision not in ("approve", "deny"): + raise ValueError("decision must be 'approve' or 'deny'") + approval_id = quote(str(approval_id).strip(), safe="") + return self._request( + "POST", + f"/approvals/{approval_id}", + {"decision": decision}, + ) + + +class MCPTransport(GatewayTransport): + """JSON-RPC 2.0 over plain HTTP POST to ``/mcp`` and ``/mcp/meta``.""" + + def __init__( + self, + base_url_proxy: Any, + *, + session_id: Optional[str] = None, + actor_id: str, + endpoint_url: Optional[str] = None, + ): + if not actor_id or not str(actor_id).strip(): + raise ValueError("actor_id is required for MCPTransport") + self._client = base_url_proxy + self._ids = itertools.count(1) + self.session_id = _external_session_id(session_id) if session_id else None + self.actor_id = str(actor_id).strip() + self.endpoint_url = endpoint_url + + def _headers(self) -> Dict[str, str]: + headers = dict(_MCP_HEADERS) + if self.session_id: + headers[SESSION_ID_HEADER] = self.session_id + headers[ACTOR_ID_HEADER] = self.actor_id + return headers + + def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]: + if self.endpoint_url: + path = self.endpoint_url + request = HttpRequest( + "POST", + path, + headers=self._headers(), + json=payload, + ) + request.url = self._client.format_url(request.url) + pipeline_response = self._client._pipeline.run(request) + response = pipeline_response.http_response + body = response.text() if hasattr(response, "text") else response.body() + if response.status_code != 200: + _raise_gateway_http_error(response) + return _parse_jsonrpc(body) + + def _rpc( + self, + method: str, + params: Optional[Dict[str, Any]] = None, + *, + meta: bool, + ) -> Dict[str, Any]: + payload: Dict[str, Any] = { + "jsonrpc": "2.0", + "id": next(self._ids), + "method": method, + } + if params is not None: + payload["params"] = params + return self._post(_MCP_META_PATH if meta else _MCP_PATH, payload) + + def list_tools(self, *, meta: bool) -> List[Any]: + result = self._rpc("tools/list", meta=meta) + return _wrap(result.get("tools") or []) + + def call_tool(self, name: str, arguments: Dict[str, Any], *, meta: bool) -> Any: + result = self._rpc( + "tools/call", + {"name": name, "arguments": arguments or {}}, + meta=meta, + ) + return _unwrap_call_result(result) + + def decide_approval(self, approval_id: str, decision: str) -> Any: + if not approval_id or not str(approval_id).strip(): + raise ValueError("approval_id is required") + if decision not in ("approve", "deny"): + raise ValueError("decision must be 'approve' or 'deny'") + endpoint = urlsplit(self.endpoint_url or self._client._base_url) + approval_id = quote(str(approval_id).strip(), safe="") + url = f"{endpoint.scheme}://{endpoint.netloc}/approvals/{approval_id}" + request = HttpRequest( + "POST", + url, + headers={**self._headers(), "Accept": "application/json"}, + json={"decision": decision}, + ) + pipeline_response = self._client._pipeline.run(request) + response = pipeline_response.http_response + body = response.text() if hasattr(response, "text") else response.body() + if response.status_code not in (200, 201, 202, 204): + _raise_gateway_http_error(response) + return _wrap(_parse_json_body(body)) if body else None + + +__all__ = [ + "GatewayTransport", + "RESTTransport", + "MCPTransport", + "MCP_PROTOCOL_VERSION", + "SESSION_ID_HEADER", + "session_mcp_url", + "DEFAULT_GATEWAY_BASE_URL", + "resolve_gateway_base_url", +] diff --git a/src/pydo/operations/__init__.py b/src/pydo/operations/__init__.py index 2de68fec..3960eadb 100644 --- a/src/pydo/operations/__init__.py +++ b/src/pydo/operations/__init__.py @@ -4,6 +4,11 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +from ._operations import ToolsOperations +from ._operations import ToolbeltsOperations +from ._operations import ConnectionsOperations +from ._operations import UsersOperations +from ._operations import SessionsOperations from ._operations import OneClicksOperations from ._operations import AccountOperations from ._operations import SshKeysOperations @@ -63,6 +68,11 @@ from ._patch import patch_sdk as _patch_sdk __all__ = [ + "ToolsOperations", + "ToolbeltsOperations", + "ConnectionsOperations", + "UsersOperations", + "SessionsOperations", "OneClicksOperations", "AccountOperations", "SshKeysOperations", diff --git a/src/pydo/operations/_operations.py b/src/pydo/operations/_operations.py index d587a331..d7451f57 100644 --- a/src/pydo/operations/_operations.py +++ b/src/pydo/operations/_operations.py @@ -51,6 +51,511 @@ _SERIALIZER.client_side_validation = False +def build_tools_list_request( + *, + toolkit_id: Optional[str] = None, + page: int = 1, + per_page: int = 20, + **kwargs: Any, +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/tools" + + # Construct parameters + if toolkit_id is not None: + _params["toolkit_id"] = _SERIALIZER.query("toolkit_id", toolkit_id, "str") + if page is not None: + _params["page"] = _SERIALIZER.query("page", page, "int", minimum=1) + if per_page is not None: + _params["per_page"] = _SERIALIZER.query( + "per_page", per_page, "int", maximum=200, minimum=1 + ) + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest( + method="GET", url=_url, params=_params, headers=_headers, **kwargs + ) + + +def build_tools_list_toolkits_request(**kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/tools/toolkits" + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) + + +def build_tools_list_providers_request(**kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/tools/providers" + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) + + +def build_tools_get_definition_request( + name: str, + *, + version: Optional[str] = None, + toolkit_id: Optional[str] = None, + **kwargs: Any, +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/tools/{name}/definition" + path_format_arguments = { + "name": _SERIALIZER.url("name", name, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + if version is not None: + _params["version"] = _SERIALIZER.query("version", version, "str") + if toolkit_id is not None: + _params["toolkit_id"] = _SERIALIZER.query("toolkit_id", toolkit_id, "str") + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest( + method="GET", url=_url, params=_params, headers=_headers, **kwargs + ) + + +def build_toolbelts_list_request( + *, status: str = "active", page: int = 1, per_page: int = 20, **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/toolbelts" + + # Construct parameters + if status is not None: + _params["status"] = _SERIALIZER.query("status", status, "str") + if page is not None: + _params["page"] = _SERIALIZER.query("page", page, "int", minimum=1) + if per_page is not None: + _params["per_page"] = _SERIALIZER.query( + "per_page", per_page, "int", maximum=200, minimum=1 + ) + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest( + method="GET", url=_url, params=_params, headers=_headers, **kwargs + ) + + +def build_toolbelts_create_request(**kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/toolbelts" + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header( + "content_type", content_type, "str" + ) + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) + + +def build_toolbelts_get_request( + name: str, *, version: Optional[str] = None, **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/toolbelts/{name}" + path_format_arguments = { + "name": _SERIALIZER.url( + "name", name, "str", pattern=r"^[a-z][a-z0-9_-]{0,63}$" + ), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct parameters + if version is not None: + _params["version"] = _SERIALIZER.query( + "version", version, "str", pattern=r"^[0-9]+$" + ) + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest( + method="GET", url=_url, params=_params, headers=_headers, **kwargs + ) + + +def build_toolbelts_delete_request(name: str, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/toolbelts/{name}" + path_format_arguments = { + "name": _SERIALIZER.url( + "name", name, "str", pattern=r"^[a-z][a-z0-9_-]{0,63}$" + ), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="DELETE", url=_url, headers=_headers, **kwargs) + + +def build_toolbelts_add_tools_request(name: str, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/toolbelts/{name}/tools/add" + path_format_arguments = { + "name": _SERIALIZER.url( + "name", name, "str", pattern=r"^[a-z][a-z0-9_-]{0,63}$" + ), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header( + "content_type", content_type, "str" + ) + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) + + +def build_toolbelts_delete_tools_request(name: str, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/toolbelts/{name}/tools/remove" + path_format_arguments = { + "name": _SERIALIZER.url( + "name", name, "str", pattern=r"^[a-z][a-z0-9_-]{0,63}$" + ), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header( + "content_type", content_type, "str" + ) + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) + + +def build_connections_list_request( + *, + provider: Optional[str] = None, + user_id: Optional[str] = None, + status: Optional[str] = None, + sort: Optional[str] = None, + sort_direction: Optional[str] = None, + page: int = 1, + per_page: int = 20, + **kwargs: Any, +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/connections" + + # Construct parameters + if provider is not None: + _params["provider"] = _SERIALIZER.query("provider", provider, "str") + if user_id is not None: + _params["user_id"] = _SERIALIZER.query("user_id", user_id, "str") + if status is not None: + _params["status"] = _SERIALIZER.query("status", status, "str") + if sort is not None: + _params["sort"] = _SERIALIZER.query("sort", sort, "str") + if sort_direction is not None: + _params["sort_direction"] = _SERIALIZER.query( + "sort_direction", sort_direction, "str" + ) + if page is not None: + _params["page"] = _SERIALIZER.query("page", page, "int", minimum=1) + if per_page is not None: + _params["per_page"] = _SERIALIZER.query( + "per_page", per_page, "int", maximum=200, minimum=1 + ) + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest( + method="GET", url=_url, params=_params, headers=_headers, **kwargs + ) + + +def build_connections_create_request(**kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/connections" + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header( + "content_type", content_type, "str" + ) + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) + + +def build_connections_get_request(id: str, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/connections/{id}" + path_format_arguments = { + "id": _SERIALIZER.url("id", id, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) + + +def build_connections_update_request(id: str, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/connections/{id}" + path_format_arguments = { + "id": _SERIALIZER.url("id", id, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header( + "content_type", content_type, "str" + ) + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="PATCH", url=_url, headers=_headers, **kwargs) + + +def build_connections_delete_request(id: str, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/connections/{id}" + path_format_arguments = { + "id": _SERIALIZER.url("id", id, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="DELETE", url=_url, headers=_headers, **kwargs) + + +def build_users_list_request( + *, page: int = 1, per_page: int = 20, **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/users" + + # Construct parameters + if page is not None: + _params["page"] = _SERIALIZER.query("page", page, "int", minimum=1) + if per_page is not None: + _params["per_page"] = _SERIALIZER.query( + "per_page", per_page, "int", maximum=200, minimum=1 + ) + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest( + method="GET", url=_url, params=_params, headers=_headers, **kwargs + ) + + +def build_users_get_request(user_id: str, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/users/{user_id}" + path_format_arguments = { + "user_id": _SERIALIZER.url("user_id", user_id, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) + + +def build_sessions_list_request( + *, + end_user_id: Optional[str] = None, + page: int = 1, + per_page: int = 20, + **kwargs: Any, +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/sessions" + + # Construct parameters + if end_user_id is not None: + _params["end_user_id"] = _SERIALIZER.query("end_user_id", end_user_id, "str") + if page is not None: + _params["page"] = _SERIALIZER.query("page", page, "int", minimum=1) + if per_page is not None: + _params["per_page"] = _SERIALIZER.query( + "per_page", per_page, "int", maximum=200, minimum=1 + ) + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest( + method="GET", url=_url, params=_params, headers=_headers, **kwargs + ) + + +def build_sessions_create_request(**kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/sessions" + + # Construct headers + if content_type is not None: + _headers["Content-Type"] = _SERIALIZER.header( + "content_type", content_type, "str" + ) + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) + + +def build_sessions_delete_request(session_urn: str, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/v2/action-gateway/sessions/{session_urn}" + path_format_arguments = { + "session_urn": _SERIALIZER.url("session_urn", session_urn, "str"), + } + + _url: str = _url.format(**path_format_arguments) # type: ignore + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="DELETE", url=_url, headers=_headers, **kwargs) + + def build_one_clicks_list_request( *, type: Optional[str] = None, **kwargs: Any ) -> HttpRequest: @@ -74,9 +579,9 @@ def build_one_clicks_list_request( ) -def build_one_clicks_install_kubernetes_request( +def build_one_clicks_install_kubernetes_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -1481,9 +1986,9 @@ def build_apps_get_metrics_bandwidth_daily_request( # pylint: disable=name-too- ) -def build_apps_list_metrics_bandwidth_daily_request( +def build_apps_list_metrics_bandwidth_daily_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -3862,9 +4367,9 @@ def build_dedicated_inferences_list_request( ) -def build_dedicated_inferences_create_request( +def build_dedicated_inferences_create_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -4061,9 +4566,9 @@ def build_dedicated_inferences_delete_tokens_request( # pylint: disable=name-to return HttpRequest(method="DELETE", url=_url, headers=_headers, **kwargs) -def build_dedicated_inferences_list_sizes_request( +def build_dedicated_inferences_list_sizes_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -4803,9 +5308,9 @@ def build_droplets_destroy_retry_with_associated_resources_request( # pylint: d return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) -def build_droplets_list_neighbors_ids_request( +def build_droplets_list_neighbors_ids_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -6673,9 +7178,9 @@ def build_kubernetes_add_registries_request(**kwargs: Any) -> HttpRequest: return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) -def build_kubernetes_remove_registries_request( +def build_kubernetes_remove_registries_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -6996,9 +7501,9 @@ def build_monitoring_list_alert_policy_request( # pylint: disable=name-too-long ) -def build_monitoring_create_alert_policy_request( +def build_monitoring_create_alert_policy_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -8532,9 +9037,9 @@ def build_monitoring_get_database_mysql_schema_latency_request( # pylint: disab ) -def build_monitoring_create_destination_request( +def build_monitoring_create_destination_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -8555,9 +9060,9 @@ def build_monitoring_create_destination_request( return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) -def build_monitoring_list_destinations_request( +def build_monitoring_list_destinations_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -9498,9 +10003,9 @@ def build_projects_assign_resources_request( return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) -def build_projects_list_resources_default_request( +def build_projects_list_resources_default_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -9514,9 +10019,9 @@ def build_projects_list_resources_default_request( return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_projects_assign_resources_default_request( +def build_projects_assign_resources_default_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -9658,9 +10163,9 @@ def build_registries_get_docker_credentials_request( # pylint: disable=name-too return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_registries_get_subscription_request( +def build_registries_get_subscription_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -9674,9 +10179,9 @@ def build_registries_get_subscription_request( return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_registries_update_subscription_request( +def build_registries_update_subscription_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -10083,9 +10588,9 @@ def build_registry_get_subscription_request(**kwargs: Any) -> HttpRequest: return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_registry_update_subscription_request( +def build_registry_update_subscription_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -11104,9 +11609,9 @@ def build_security_list_settings_request( ) -def build_security_update_settings_plan_request( +def build_security_update_settings_plan_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -11127,9 +11632,9 @@ def build_security_update_settings_plan_request( return HttpRequest(method="PUT", url=_url, headers=_headers, **kwargs) -def build_security_create_suppression_request( +def build_security_create_suppression_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -11873,9 +12378,9 @@ def build_vector_databases_delete_request(id: str, **kwargs: Any) -> HttpRequest return HttpRequest(method="DELETE", url=_url, headers=_headers, **kwargs) -def build_vector_databases_list_backups_request( +def build_vector_databases_list_backups_request( # pylint: disable=name-too-long id: str, **kwargs: Any -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -11966,9 +12471,9 @@ def build_vector_databases_get_credentials_request( # pylint: disable=name-too- return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_vector_databases_post_resize_request( +def build_vector_databases_post_resize_request( # pylint: disable=name-too-long id: str, **kwargs: Any -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -11994,9 +12499,9 @@ def build_vector_databases_post_resize_request( return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) -def build_vector_databases_update_tags_request( +def build_vector_databases_update_tags_request( # pylint: disable=name-too-long id: str, **kwargs: Any -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -13858,9 +14363,9 @@ def build_genai_list_anthropic_api_keys_request( # pylint: disable=name-too-lon ) -def build_genai_create_anthropic_api_key_request( +def build_genai_create_anthropic_api_key_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -14126,9 +14631,9 @@ def build_genai_list_evaluation_datasets_request( # pylint: disable=name-too-lo ) -def build_genai_create_evaluation_dataset_request( +def build_genai_create_evaluation_dataset_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -14214,9 +14719,9 @@ def build_genai_get_evaluation_dataset_download_url_request( # pylint: disable= return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_genai_list_evaluation_metrics_request( +def build_genai_list_evaluation_metrics_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -14230,9 +14735,9 @@ def build_genai_list_evaluation_metrics_request( return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_genai_create_custom_evaluation_metric_request( +def build_genai_create_custom_evaluation_metric_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -14302,9 +14807,9 @@ def build_genai_delete_custom_evaluation_metric_request( # pylint: disable=name return HttpRequest(method="DELETE", url=_url, headers=_headers, **kwargs) -def build_genai_run_evaluation_test_case_request( +def build_genai_run_evaluation_test_case_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -14408,9 +14913,9 @@ def build_genai_get_evaluation_run_prompt_results_request( # pylint: disable=na return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_genai_list_evaluation_test_cases_request( +def build_genai_list_evaluation_test_cases_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -14424,9 +14929,9 @@ def build_genai_list_evaluation_test_cases_request( return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_genai_create_evaluation_test_case_request( +def build_genai_create_evaluation_test_case_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -14707,9 +15212,9 @@ def build_genai_list_knowledge_bases_request( ) -def build_genai_create_knowledge_base_request( +def build_genai_create_knowledge_base_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -14920,9 +15425,9 @@ def build_genai_get_knowledge_base_request(uuid: str, **kwargs: Any) -> HttpRequ return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_genai_update_knowledge_base_request( +def build_genai_update_knowledge_base_request( # pylint: disable=name-too-long uuid: str, **kwargs: Any -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -14948,9 +15453,9 @@ def build_genai_update_knowledge_base_request( return HttpRequest(method="PUT", url=_url, headers=_headers, **kwargs) -def build_genai_delete_knowledge_base_request( +def build_genai_delete_knowledge_base_request( # pylint: disable=name-too-long uuid: str, **kwargs: Any -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -14992,9 +15497,9 @@ def build_genai_create_model_eval_dataset_upload_presigned_urls_request( # pyli return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) -def build_genai_list_model_evaluation_metrics_request( +def build_genai_list_model_evaluation_metrics_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -15008,9 +15513,9 @@ def build_genai_list_model_evaluation_metrics_request( return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_genai_list_model_evaluation_presets_request( +def build_genai_list_model_evaluation_presets_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) accept = _headers.pop("Accept", "application/json") @@ -15125,9 +15630,9 @@ def build_genai_list_model_evaluation_runs_request( # pylint: disable=name-too- ) -def build_genai_create_model_evaluation_run_request( +def build_genai_create_model_evaluation_run_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -15643,9 +16148,9 @@ def build_genai_delete_model_router_request(uuid: str, **kwargs: Any) -> HttpReq return HttpRequest(method="DELETE", url=_url, headers=_headers, **kwargs) -def build_genai_create_oauth2_dropbox_tokens_request( +def build_genai_create_oauth2_dropbox_tokens_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -15716,9 +16221,9 @@ def build_genai_list_openai_api_keys_request( ) -def build_genai_create_openai_api_key_request( +def build_genai_create_openai_api_key_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -15875,9 +16380,9 @@ def build_genai_list_datacenter_regions_request( # pylint: disable=name-too-lon ) -def build_genai_create_scheduled_indexing_request( +def build_genai_create_scheduled_indexing_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -16135,9 +16640,9 @@ def build_genai_list_evaluation_test_cases_by_workspace_request( # pylint: disa return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) -def build_inference_create_chat_completion_request( +def build_inference_create_chat_completion_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -16256,9 +16761,9 @@ def build_inference_create_response_request(**kwargs: Any) -> HttpRequest: return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) -def build_inference_create_async_invoke_request( +def build_inference_create_async_invoke_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -16279,9 +16784,9 @@ def build_inference_create_async_invoke_request( return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) -def build_inference_create_batch_file_request( +def build_inference_create_batch_file_request( # pylint: disable=name-too-long **kwargs: Any, -) -> HttpRequest: # pylint: disable=name-too-long +) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) content_type: Optional[str] = kwargs.pop( @@ -16469,14 +16974,14 @@ def build_agent_inference_create_chat_completion_request( # pylint: disable=nam ) -class OneClicksOperations: +class ToolsOperations: """ .. warning:: **DO NOT** instantiate this class directly. Instead, you should access the following operations through :class:`~pydo.GeneratedClient`'s - :attr:`one_clicks` attribute. + :attr:`tools` attribute. """ def __init__(self, *args, **kwargs): @@ -16489,22 +16994,406 @@ def __init__(self, *args, **kwargs): ) @distributed_trace - def list(self, *, type: Optional[str] = None, **kwargs: Any) -> JSON: - """List 1-Click Applications. + def list( + self, + *, + toolkit_id: Optional[str] = None, + page: int = 1, + per_page: int = 20, + **kwargs: Any, + ) -> JSON: + # pylint: disable=line-too-long + """List Tools. - To list all available 1-Click applications, send a GET request to ``/v2/1-clicks``. The - ``type`` may - be provided as query paramater in order to restrict results to a certain type of 1-Click, for - example: ``/v2/1-clicks?type=droplet``. Current supported types are ``kubernetes`` and - ``droplet``. + Lists active Action Gateway tools visible to the authenticated team. - The response will be a JSON object with a key called ``1_clicks``. This will be set to an array - of - 1-Click application data, each of which will contain the the slug and type for the 1-Click. + :keyword toolkit_id: Filter tools by toolkit identifier. Default value is None. + :paramtype toolkit_id: str + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "definitions": [ + { + "annotations": { + "destructiveHint": bool, # Optional. + "idempotentHint": bool, # Optional. + "openWorldHint": bool, # Optional. + "readOnlyHint": bool, # Optional. + "title": "str" # Optional. + }, + "auth": { + "baseUrlResolution": { + "httpLookup": { + "baseUrlTemplate": "str", # + Optional. HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "caseInsensitive": bool, # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "extractField": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "matchField": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "matchValue": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "match_value_parameter": "str", # + Optional. HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "method": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "requiredScopes": [ + "str" # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access + token), selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When + both are empty, exactly one entry whose own "scopes" + array contains required_scopes must exist. Configuring + only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + ], + "trimTrailingSlash": bool, # + Optional. HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + "url": "str" # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both + are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one + match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + } + }, + "credentialBinding": "str", # Optional. + "credentialRefSource": "str", # Optional. + "doManagedCredentialRef": "str", # Optional. + "injection": { + "location": "str", # Optional. + "name": "str", # Optional. + "scheme": "str" # Optional. + }, + "modes": [ + "str" # Optional. + ], + "provider": "str", # Optional. + "scopes": [ + "str" # Optional. + ] + }, + "classification": { + "dataClasses": [ + "str" # Optional. + ], + "operation": "str", # Optional. + "risk": "str" # Optional. + }, + "description": "str", # Optional. + "execution": { + "adapterVersion": "str", # Optional. + "configRef": "str", # Optional. + "http": { + "allowedHosts": [ + "str" # Optional. + ], + "baseUrl": "str", # Optional. + "method": "str", # Optional. + "path": "str", # Optional. + "requestEncoding": "str", # Optional. + "responseFormat": "str" # Optional. + }, + "mcp": { + "allowedHosts": [ + "str" # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote + MCP server (as opposed to a plain HTTP endpoint). endpoint is + the remote MCP server's URL, tool_name is the name the remote + server expects on tools/call (may differ from this tool's + registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and + server_ref is an opaque label identifying the remote server + for logging/metrics/allowlisting. + ], + "endpoint": "str", # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote MCP + server (as opposed to a plain HTTP endpoint). endpoint is the + remote MCP server's URL, tool_name is the name the remote server + expects on tools/call (may differ from this tool's registry + name), transport selects the wire protocol ("streamable_http" is + the only kind implemented today), and server_ref is an opaque + label identifying the remote server for + logging/metrics/allowlisting. + "serverRef": "str", # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote MCP + server (as opposed to a plain HTTP endpoint). endpoint is the + remote MCP server's URL, tool_name is the name the remote server + expects on tools/call (may differ from this tool's registry + name), transport selects the wire protocol ("streamable_http" is + the only kind implemented today), and server_ref is an opaque + label identifying the remote server for + logging/metrics/allowlisting. + "toolName": "str", # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote MCP + server (as opposed to a plain HTTP endpoint). endpoint is the + remote MCP server's URL, tool_name is the name the remote server + expects on tools/call (may differ from this tool's registry + name), transport selects the wire protocol ("streamable_http" is + the only kind implemented today), and server_ref is an opaque + label identifying the remote server for + logging/metrics/allowlisting. + "transport": "str" # Optional. MCPExecution + describes how to invoke a tool that is fronted by a remote MCP + server (as opposed to a plain HTTP endpoint). endpoint is the + remote MCP server's URL, tool_name is the name the remote server + expects on tools/call (may differ from this tool's registry + name), transport selects the wire protocol ("streamable_http" is + the only kind implemented today), and server_ref is an opaque + label identifying the remote server for + logging/metrics/allowlisting. + }, + "type": "str" # Optional. + }, + "flipperName": "str", # Optional. + "hooks": { + "usage": { + "billable": bool, # Optional. When usage + metadata is present, false prevents billing. Omitting usage + metadata leaves consumers' legacy billing classification + unchanged. + "meters": [ + { + "quantitySource": "str", # + Optional. + "sku": "str", # Optional. + "unit": "str" # Optional. + } + ] + } + }, + "inputSchema": {}, # Optional. Any object. + "name": "str", # Optional. + "outputSchema": {}, # Optional. Any object. + "parallelizable": bool, # Optional. + "policy": { + "permission": "str" # Optional. + }, + "reliability": { + "maxOutputBytes": "str", # Optional. + "retry": { + "backoff": "str", # Optional. + "maxAttempts": 0, # Optional. + "retryOn": [ + "str" # Optional. + ] + }, + "timeoutMs": 0 # Optional. + }, + "schemaVersion": "str", # Optional. + "status": "str", # Optional. + "streamingSafe": bool, # Optional. + "tags": [ + "str" # Optional. + ], + "title": "str", # Optional. + "toolId": "str", # Optional. + "toolSlug": "str", # Optional. tool_slug is the + provider-qualified, stable tool identifier + ":code:``_:code:``". Pass this value back verbatim to + the toolbelt add/remove endpoints; clients should treat it as opaque. + "toolkitId": "str", # Optional. + "transform": { + "input": {}, # Optional. Any object. + "language": "str", # Optional. + "output": {} # Optional. Any object. + }, + "version": "str" # Optional. + } + ], + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + }, + "tools": [ + { + "annotations": { + "destructiveHint": bool, # Optional. + "idempotentHint": bool, # Optional. + "openWorldHint": bool, # Optional. + "readOnlyHint": bool, # Optional. + "title": "str" # Optional. + }, + "description": "str", # Optional. + "inputSchema": {}, # Optional. Any object. + "name": "str", # Optional. + "outputSchema": {}, # Optional. Any object. + "parallelizable": bool, # Optional. + "streamingSafe": bool, # Optional. + "title": "str", # Optional. + "toolSlug": "str", # Optional. tool_slug is the + provider-qualified, stable tool identifier + ":code:``_:code:``". Pass this value back verbatim to + the toolbelt add/remove endpoints; clients should treat it as opaque + rather than reconstructing it from toolkit_id and name. + "toolkitId": "str", # Optional. + "version": "str" # Optional. + } + ], + "version": "str" # Optional. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_tools_list_request( + toolkit_id=toolkit_id, + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace + def list_toolkits(self, **kwargs: Any) -> JSON: + """List Toolkits. + + Lists the toolkits that group Action Gateway tools. - :keyword type: Restrict results to a certain type of 1-Click. Known values are: "droplet" and - "kubernetes". Default value is None. - :paramtype type: str :return: JSON object :rtype: JSON :raises ~azure.core.exceptions.HttpResponseError: @@ -16514,12 +17403,4231 @@ def list(self, *, type: Optional[str] = None, **kwargs: Any) -> JSON: # response body for status code(s): 200 response == { - "1_clicks": [ + "toolkits": [ { - "slug": "str", # The slug identifier for the 1-Click - application. Required. - "type": "str" # The type of the 1-Click application. - Required. + "description": "str", # Optional. + "id": "str", # Optional. + "name": "str" # Optional. + } + ], + "version": "str" # Optional. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_tools_list_toolkits_request( + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace + def list_providers(self, **kwargs: Any) -> JSON: + """List Tool Providers. + + Lists Action Gateway providers and their connection requirements. + + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "providers": [ + { + "auth_type": "str", # Optional. + "connection_parameters": [ + { + "allowed_host_suffixes": [ + "str" # Optional. + ], + "allowed_values": [ + "str" # Optional. + ], + "description": "str", # Optional. + "input_kind": "str", # Optional. + "key": "str", # Optional. + "label": "str", # Optional. + "max_length": 0, # Optional. + "normalization": "str", # Optional. + "required": bool # Optional. + } + ], + "description": "str", # Optional. + "display_name": "str", # Optional. + "name": "str", # Optional. + "scopes": [ + "str" # Optional. + ] + } + ] + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_tools_list_providers_request( + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace + def get_definition( + self, + name: str, + *, + version: Optional[str] = None, + toolkit_id: Optional[str] = None, + **kwargs: Any, + ) -> JSON: + # pylint: disable=line-too-long + """Retrieve a Tool Definition. + + Retrieves the executable definition for an active Action Gateway tool. + + :param name: The provider-qualified tool name. Required. + :type name: str + :keyword version: The tool version. Omit to retrieve the current version. Default value is + None. + :paramtype version: str + :keyword toolkit_id: The toolkit identifier used to disambiguate a bare tool name. Default + value is None. + :paramtype toolkit_id: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "annotations": { + "destructiveHint": bool, # Optional. + "idempotentHint": bool, # Optional. + "openWorldHint": bool, # Optional. + "readOnlyHint": bool, # Optional. + "title": "str" # Optional. + }, + "auth": { + "baseUrlResolution": { + "httpLookup": { + "baseUrlTemplate": "str", # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "caseInsensitive": bool, # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "extractField": "str", # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "matchField": "str", # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "matchValue": "str", # Optional. HTTPLookupSpec + resolves a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "match_value_parameter": "str", # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting extract_field from + that entry, and substituting it for "{value}" in base_url_template. + When match_field and match_value are both set, they select the entry. + When both are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one match field + is invalid. Resolution fails fast on zero or multiple compatible + entries. + "method": "str", # Optional. HTTPLookupSpec resolves + a base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON array, + extracting extract_field from that entry, and substituting it for + "{value}" in base_url_template. When match_field and match_value are + both set, they select the entry. When both are empty, exactly one + entry whose own "scopes" array contains required_scopes must exist. + Configuring only one match field is invalid. Resolution fails fast on + zero or multiple compatible entries. + "requiredScopes": [ + "str" # Optional. HTTPLookupSpec resolves a + base_url by calling url (bearer-authenticated with the + just-exchanged access token), selecting an entry in the JSON + array, extracting extract_field from that entry, and substituting + it for "{value}" in base_url_template. When match_field and + match_value are both set, they select the entry. When both are + empty, exactly one entry whose own "scopes" array contains + required_scopes must exist. Configuring only one match field is + invalid. Resolution fails fast on zero or multiple compatible + entries. + ], + "trimTrailingSlash": bool, # Optional. + HTTPLookupSpec resolves a base_url by calling url + (bearer-authenticated with the just-exchanged access token), + selecting an entry in the JSON array, extracting extract_field from + that entry, and substituting it for "{value}" in base_url_template. + When match_field and match_value are both set, they select the entry. + When both are empty, exactly one entry whose own "scopes" array + contains required_scopes must exist. Configuring only one match field + is invalid. Resolution fails fast on zero or multiple compatible + entries. + "url": "str" # Optional. HTTPLookupSpec resolves a + base_url by calling url (bearer-authenticated with the just-exchanged + access token), selecting an entry in the JSON array, extracting + extract_field from that entry, and substituting it for "{value}" in + base_url_template. When match_field and match_value are both set, + they select the entry. When both are empty, exactly one entry whose + own "scopes" array contains required_scopes must exist. Configuring + only one match field is invalid. Resolution fails fast on zero or + multiple compatible entries. + } + }, + "credentialBinding": "str", # Optional. + "credentialRefSource": "str", # Optional. + "doManagedCredentialRef": "str", # Optional. + "injection": { + "location": "str", # Optional. + "name": "str", # Optional. + "scheme": "str" # Optional. + }, + "modes": [ + "str" # Optional. + ], + "provider": "str", # Optional. + "scopes": [ + "str" # Optional. + ] + }, + "classification": { + "dataClasses": [ + "str" # Optional. + ], + "operation": "str", # Optional. + "risk": "str" # Optional. + }, + "description": "str", # Optional. + "execution": { + "adapterVersion": "str", # Optional. + "configRef": "str", # Optional. + "http": { + "allowedHosts": [ + "str" # Optional. + ], + "baseUrl": "str", # Optional. + "method": "str", # Optional. + "path": "str", # Optional. + "requestEncoding": "str", # Optional. + "responseFormat": "str" # Optional. + }, + "mcp": { + "allowedHosts": [ + "str" # Optional. MCPExecution describes how to + invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, + tool_name is the name the remote server expects on tools/call (may + differ from this tool's registry name), transport selects the wire + protocol ("streamable_http" is the only kind implemented today), and + server_ref is an opaque label identifying the remote server for + logging/metrics/allowlisting. + ], + "endpoint": "str", # Optional. MCPExecution describes how to + invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, tool_name + is the name the remote server expects on tools/call (may differ from this + tool's registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and server_ref is + an opaque label identifying the remote server for + logging/metrics/allowlisting. + "serverRef": "str", # Optional. MCPExecution describes how + to invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, tool_name + is the name the remote server expects on tools/call (may differ from this + tool's registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and server_ref is + an opaque label identifying the remote server for + logging/metrics/allowlisting. + "toolName": "str", # Optional. MCPExecution describes how to + invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, tool_name + is the name the remote server expects on tools/call (may differ from this + tool's registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and server_ref is + an opaque label identifying the remote server for + logging/metrics/allowlisting. + "transport": "str" # Optional. MCPExecution describes how to + invoke a tool that is fronted by a remote MCP server (as opposed to a + plain HTTP endpoint). endpoint is the remote MCP server's URL, tool_name + is the name the remote server expects on tools/call (may differ from this + tool's registry name), transport selects the wire protocol + ("streamable_http" is the only kind implemented today), and server_ref is + an opaque label identifying the remote server for + logging/metrics/allowlisting. + }, + "type": "str" # Optional. + }, + "flipperName": "str", # Optional. + "hooks": { + "usage": { + "billable": bool, # Optional. When usage metadata is + present, false prevents billing. Omitting usage metadata leaves + consumers' legacy billing classification unchanged. + "meters": [ + { + "quantitySource": "str", # Optional. + "sku": "str", # Optional. + "unit": "str" # Optional. + } + ] + } + }, + "inputSchema": {}, # Optional. Any object. + "name": "str", # Optional. + "outputSchema": {}, # Optional. Any object. + "parallelizable": bool, # Optional. + "policy": { + "permission": "str" # Optional. + }, + "reliability": { + "maxOutputBytes": "str", # Optional. + "retry": { + "backoff": "str", # Optional. + "maxAttempts": 0, # Optional. + "retryOn": [ + "str" # Optional. + ] + }, + "timeoutMs": 0 # Optional. + }, + "schemaVersion": "str", # Optional. + "status": "str", # Optional. + "streamingSafe": bool, # Optional. + "tags": [ + "str" # Optional. + ], + "title": "str", # Optional. + "toolId": "str", # Optional. + "toolSlug": "str", # Optional. tool_slug is the provider-qualified, stable + tool identifier ":code:``_:code:``". Pass this value back + verbatim to the toolbelt add/remove endpoints; clients should treat it as opaque. + "toolkitId": "str", # Optional. + "transform": { + "input": {}, # Optional. Any object. + "language": "str", # Optional. + "output": {} # Optional. Any object. + }, + "version": "str" # Optional. + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_tools_get_definition_request( + name=name, + version=version, + toolkit_id=toolkit_id, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + +class ToolbeltsOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.GeneratedClient`'s + :attr:`toolbelts` attribute. + """ + + def __init__(self, *args, **kwargs): + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace + def list( + self, + *, + status: str = "active", + page: int = 1, + per_page: int = 20, + **kwargs: Any, + ) -> JSON: + """List Toolbelts. + + Lists the latest version of each toolbelt owned by the authenticated team. + + :keyword status: Filter toolbelts by status. Known values are: "active", "deprecated", and + "all". Default value is "active". + :paramtype status: str + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + }, + "toolbelts": [ + { + "latest_version": "str", # Required. + "name": "str", # Required. + "reference_latest": "str", # Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "updated_at": "2020-02-20 00:00:00", # Required. + "version_count": 0, # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + ] + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_toolbelts_list_request( + status=status, + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + def create( + self, body: JSON, *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a Toolbelt. + + Creates a versioned collection of provider-qualified Action Gateway tool names. + + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "name": "str", # Required. + "tools": [ + "str" # Required. + ], + "description": "str", # Optional. + "display_name": "str", # Optional. + "version": "1" # Optional. Default value is "1". + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + def create( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a Toolbelt. + + Creates a versioned collection of provider-qualified Action Gateway tool names. + + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace + def create(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Create a Toolbelt. + + Creates a versioned collection of provider-qualified Action Gateway tool names. + + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "name": "str", # Required. + "tools": [ + "str" # Required. + ], + "description": "str", # Optional. + "display_name": "str", # Optional. + "version": "1" # Optional. Default value is "1". + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_toolbelts_create_request( + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 409]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 409: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace + def get(self, name: str, *, version: Optional[str] = None, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Retrieve a Toolbelt. + + Retrieves the latest active version or a specified immutable version of a toolbelt. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :keyword version: An immutable numeric toolbelt version. Omit to retrieve the latest active + version. Default value is None. + :paramtype version: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_toolbelts_get_request( + name=name, + version=version, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace + def delete(self, name: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Delete a Toolbelt. + + Deprecates the latest active version of a toolbelt. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_toolbelts_delete_request( + name=name, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + def add_tools( + self, + name: str, + body: JSON, + *, + content_type: str = "application/json", + **kwargs: Any, + ) -> JSON: + # pylint: disable=line-too-long + """Add Tools to a Toolbelt. + + Adds provider-qualified tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "tools": [ + "str" # Required. + ] + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + def add_tools( + self, + name: str, + body: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any, + ) -> JSON: + # pylint: disable=line-too-long + """Add Tools to a Toolbelt. + + Adds provider-qualified tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace + def add_tools(self, name: str, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Add Tools to a Toolbelt. + + Adds provider-qualified tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "tools": [ + "str" # Required. + ] + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_toolbelts_add_tools_request( + name=name, + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + def delete_tools( + self, + name: str, + body: JSON, + *, + content_type: str = "application/json", + **kwargs: Any, + ) -> JSON: + # pylint: disable=line-too-long + """Remove Tools from a Toolbelt. + + Removes tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "tools": [ + "str" # Required. + ] + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + def delete_tools( + self, + name: str, + body: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any, + ) -> JSON: + # pylint: disable=line-too-long + """Remove Tools from a Toolbelt. + + Removes tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace + def delete_tools( + self, name: str, body: Union[JSON, IO[bytes]], **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Remove Tools from a Toolbelt. + + Removes tool names and creates a new immutable toolbelt version. + + :param name: The natural key identifying the toolbelt. Required. + :type name: str + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "tools": [ + "str" # Required. + ] + } + + # response body for status code(s): 200 + response == { + "toolbelt": { + "created_at": "2020-02-20 00:00:00", # Required. + "name": "str", # Required. + "reference": "str", # A reference pinned to this immutable toolbelt + version. Required. + "reference_latest": "str", # An unversioned reference to the latest + active version. Required. + "status": "str", # Required. Known values are: "active" and + "deprecated". + "tool_count": 0, # Required. + "tools": [ + "str" # Required. + ], + "updated_at": "2020-02-20 00:00:00", # Required. + "version": "str", # Required. + "description": "str", # Optional. Required. + "display_name": "str" # Optional. Required. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_toolbelts_delete_tools_request( + name=name, + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + +class ConnectionsOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.GeneratedClient`'s + :attr:`connections` attribute. + """ + + def __init__(self, *args, **kwargs): + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace + def list( + self, + *, + provider: Optional[str] = None, + user_id: Optional[str] = None, + status: Optional[str] = None, + sort: Optional[str] = None, + sort_direction: Optional[str] = None, + page: int = 1, + per_page: int = 20, + **kwargs: Any, + ) -> JSON: + """List Connections. + + Lists OAuth connections owned by the authenticated team. + + :keyword provider: Filter by provider name. Default value is None. + :paramtype provider: str + :keyword user_id: Filter by end-user identifier. Default value is None. + :paramtype user_id: str + :keyword status: Filter by connection status. Default value is None. + :paramtype status: str + :keyword sort: Field used to sort results. Default value is None. + :paramtype sort: str + :keyword sort_direction: Sort direction. Default value is None. + :paramtype sort_direction: str + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "connections": [ + { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. + "granted_at": "2020-02-20 00:00:00", # Optional. + "id": "str", # Optional. + "provider": "str", # Optional. + "provider_display_name": "str", # Optional. + "revoked_at": "2020-02-20 00:00:00", # Optional. + "scopes": [ + "str" # Optional. + ], + "status": "str", # Optional. + "updated_at": "2020-02-20 00:00:00", # Optional. + "user_id": "str" # Optional. + } + ], + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + } + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_connections_list_request( + provider=provider, + user_id=user_id, + status=status, + sort=sort, + sort_direction=sort_direction, + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + def create( + self, body: JSON, *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a Connection. + + Creates or begins authorization for an OAuth connection to an Action Gateway provider. + + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "connection_parameters": {}, # Optional. Any object. + "provider": "str", # Optional. + "scopes": [ + "str" # Optional. + ], + "user_id": "str" # Optional. + } + + # response body for status code(s): 200 + response == { + "authorization": { + "connect_url": "str", # Optional. ConnectionAuthorization is present + only while a connection is pending. The UI sends the user to connect_url and + polls GetConnection until the connection becomes active or expires. The + Secrets Manager poll URL is never exposed. + "expires_at": "2020-02-20 00:00:00", # Optional. + ConnectionAuthorization is present only while a connection is pending. The UI + sends the user to connect_url and polls GetConnection until the connection + becomes active or expires. The Secrets Manager poll URL is never exposed. + "status": "str", # Optional. ConnectionAuthorization is present only + while a connection is pending. The UI sends the user to connect_url and polls + GetConnection until the connection becomes active or expires. The Secrets + Manager poll URL is never exposed. + "verification_code": "str" # Optional. ConnectionAuthorization is + present only while a connection is pending. The UI sends the user to + connect_url and polls GetConnection until the connection becomes active or + expires. The Secrets Manager poll URL is never exposed. + }, + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + def create( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a Connection. + + Creates or begins authorization for an OAuth connection to an Action Gateway provider. + + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "authorization": { + "connect_url": "str", # Optional. ConnectionAuthorization is present + only while a connection is pending. The UI sends the user to connect_url and + polls GetConnection until the connection becomes active or expires. The + Secrets Manager poll URL is never exposed. + "expires_at": "2020-02-20 00:00:00", # Optional. + ConnectionAuthorization is present only while a connection is pending. The UI + sends the user to connect_url and polls GetConnection until the connection + becomes active or expires. The Secrets Manager poll URL is never exposed. + "status": "str", # Optional. ConnectionAuthorization is present only + while a connection is pending. The UI sends the user to connect_url and polls + GetConnection until the connection becomes active or expires. The Secrets + Manager poll URL is never exposed. + "verification_code": "str" # Optional. ConnectionAuthorization is + present only while a connection is pending. The UI sends the user to + connect_url and polls GetConnection until the connection becomes active or + expires. The Secrets Manager poll URL is never exposed. + }, + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace + def create(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Create a Connection. + + Creates or begins authorization for an OAuth connection to an Action Gateway provider. + + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "connection_parameters": {}, # Optional. Any object. + "provider": "str", # Optional. + "scopes": [ + "str" # Optional. + ], + "user_id": "str" # Optional. + } + + # response body for status code(s): 200 + response == { + "authorization": { + "connect_url": "str", # Optional. ConnectionAuthorization is present + only while a connection is pending. The UI sends the user to connect_url and + polls GetConnection until the connection becomes active or expires. The + Secrets Manager poll URL is never exposed. + "expires_at": "2020-02-20 00:00:00", # Optional. + ConnectionAuthorization is present only while a connection is pending. The UI + sends the user to connect_url and polls GetConnection until the connection + becomes active or expires. The Secrets Manager poll URL is never exposed. + "status": "str", # Optional. ConnectionAuthorization is present only + while a connection is pending. The UI sends the user to connect_url and polls + GetConnection until the connection becomes active or expires. The Secrets + Manager poll URL is never exposed. + "verification_code": "str" # Optional. ConnectionAuthorization is + present only while a connection is pending. The UI sends the user to + connect_url and polls GetConnection until the connection becomes active or + expires. The Secrets Manager poll URL is never exposed. + }, + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 409 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_connections_create_request( + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 409]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 409: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace + def get(self, id: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Retrieve a Connection. + + Retrieves an OAuth connection owned by the authenticated team. + + :param id: The connection UUID. Required. + :type id: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "authorization": { + "connect_url": "str", # Optional. ConnectionAuthorization is present + only while a connection is pending. The UI sends the user to connect_url and + polls GetConnection until the connection becomes active or expires. The + Secrets Manager poll URL is never exposed. + "expires_at": "2020-02-20 00:00:00", # Optional. + ConnectionAuthorization is present only while a connection is pending. The UI + sends the user to connect_url and polls GetConnection until the connection + becomes active or expires. The Secrets Manager poll URL is never exposed. + "status": "str", # Optional. ConnectionAuthorization is present only + while a connection is pending. The UI sends the user to connect_url and polls + GetConnection until the connection becomes active or expires. The Secrets + Manager poll URL is never exposed. + "verification_code": "str" # Optional. ConnectionAuthorization is + present only while a connection is pending. The UI sends the user to + connect_url and polls GetConnection until the connection becomes active or + expires. The Secrets Manager poll URL is never exposed. + }, + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_connections_get_request( + id=id, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + def update( + self, + id: str, + body: JSON, + *, + content_type: str = "application/json", + **kwargs: Any, + ) -> JSON: + # pylint: disable=line-too-long + """Update Connection Parameters. + + Updates non-sensitive connection parameters for an OAuth connection. + + :param id: The connection UUID. Required. + :type id: str + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "connection_parameters": {}, # Optional. Any object. + "id": "str" # Optional. + } + + # response body for status code(s): 200 + response == { + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + def update( + self, + id: str, + body: IO[bytes], + *, + content_type: str = "application/json", + **kwargs: Any, + ) -> JSON: + # pylint: disable=line-too-long + """Update Connection Parameters. + + Updates non-sensitive connection parameters for an OAuth connection. + + :param id: The connection UUID. Required. + :type id: str + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace + def update(self, id: str, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Update Connection Parameters. + + Updates non-sensitive connection parameters for an OAuth connection. + + :param id: The connection UUID. Required. + :type id: str + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "connection_parameters": {}, # Optional. Any object. + "id": "str" # Optional. + } + + # response body for status code(s): 200 + response == { + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 400, 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_connections_update_request( + id=id, + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace + def delete(self, id: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Delete a Connection. + + Revokes and deletes an OAuth connection owned by the authenticated team. + + :param id: The connection UUID. Required. + :type id: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "connection": { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "granted_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "id": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "provider_display_name": "str", # Optional. ---- OAuth connection + resources -------------------------- OAuthConnection is the public, + team-scoped connection metadata returned to the UI. It deliberately excludes + the team ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + "revoked_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "scopes": [ + "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team + ID, Secrets Manager assignment, actor identifiers, poll URL, and + authorization handle. + ], + "status": "str", # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + "updated_at": "2020-02-20 00:00:00", # Optional. ---- OAuth + connection resources -------------------------- OAuthConnection is the + public, team-scoped connection metadata returned to the UI. It deliberately + excludes the team ID, Secrets Manager assignment, actor identifiers, poll + URL, and authorization handle. + "user_id": "str" # Optional. ---- OAuth connection resources + -------------------------- OAuthConnection is the public, team-scoped + connection metadata returned to the UI. It deliberately excludes the team ID, + Secrets Manager assignment, actor identifiers, poll URL, and authorization + handle. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_connections_delete_request( + id=id, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + +class UsersOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.GeneratedClient`'s + :attr:`users` attribute. + """ + + def __init__(self, *args, **kwargs): + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace + def list(self, *, page: int = 1, per_page: int = 20, **kwargs: Any) -> JSON: + """List Action Gateway Users. + + Lists end-user identifiers derived from sessions and OAuth connections for the authenticated + team. + + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + }, + "user_ids": [ + "str" # Optional. + ] + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_users_list_request( + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace + def get(self, user_id: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Retrieve an Action Gateway User. + + Retrieves a derived end-user view containing its sessions and OAuth connections. + + :param user_id: The end-user identifier. Required. + :type user_id: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "user": { + "connections": [ + { + "connection_parameters": {}, # Optional. Any object. + "created_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "granted_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "id": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "provider": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "provider_display_name": "str", # Optional. User is + a derived, team-scoped view across sessions and OAuth connections. + "revoked_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "scopes": [ + "str" # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + ], + "status": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "updated_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "user_id": "str" # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + } + ], + "sessions": [ + { + "created_at": "2020-02-20 00:00:00", # Optional. + User is a derived, team-scoped view across sessions and OAuth + connections. + "name": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "session_urn": "str", # Optional. User is a derived, + team-scoped view across sessions and OAuth connections. + "updated_at": "2020-02-20 00:00:00" # Optional. User + is a derived, team-scoped view across sessions and OAuth connections. + } + ], + "user_id": "str" # Optional. User is a derived, team-scoped view + across sessions and OAuth connections. + } + } + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_users_get_request( + user_id=user_id, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + +class SessionsOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.GeneratedClient`'s + :attr:`sessions` attribute. + """ + + def __init__(self, *args, **kwargs): + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace + def list( + self, + *, + end_user_id: Optional[str] = None, + page: int = 1, + per_page: int = 20, + **kwargs: Any, + ) -> JSON: + # pylint: disable=line-too-long + """List Action Gateway Sessions. + + Lists Action Gateway sessions owned by the authenticated team. + + :keyword end_user_id: Filter sessions by actor identifier. Default value is None. + :paramtype end_user_id: str + :keyword page: Which 'page' of paginated results to return. Default value is 1. + :paramtype page: int + :keyword per_page: Number of items returned per page. Default value is 20. + :paramtype per_page: int + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "pagination": { + "page": 0, # Required. + "per_page": 0, # Required. + "total": 0 # Required. + }, + "sessions": [ + { + "actorId": "str", # Optional. actor_id is empty when the + session is not bound to an actor. + "config": {}, # Optional. Preserved as an opaque object. + Gateway currently interprets config.preloadTools to add selected direct + tools to the session MCP. + "createdAt": "2020-02-20 00:00:00", # Optional. + "name": "str", # Optional. name is the required + human-readable session name. + "policy": { + "defaultAction": "ask", # Optional. Default value is + "ask". SessionPolicyAction is the disposition applied to a tool call. + Lowercase values are canonical so ProtoJSON matches the public REST + vocabulary; the prefixed aliases preserve compatibility for existing + protobuf clients. Known values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default + value is "ask". SessionPolicyAction is the disposition + applied to a tool call. Lowercase values are canonical so + ProtoJSON matches the public REST vocabulary; the prefixed + aliases preserve compatibility for existing protobuf clients. + Known values are: "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. + Dictionary of :code:``. + }, + "tool": "str" # Optional. + SessionPolicySpec is the Gateway-relevant subset of a + session's permission policy. Filesystem and network policy + remain enforced by the sandbox. + } + ] + }, + "sessionUrn": "str", # Optional. + "tools": { + "references": [ + { + "kind": "str", # Optional. Known + values are: "SESSION_TOOL_REFERENCE_KIND_TOOL" and + "SESSION_TOOL_REFERENCE_KIND_TOOLBELT". + "name": "str", # Optional. Omitted + when the request omitted tools (all tools). A present + selection with no references represents tools: []. + "version": "str" # Optional. Omitted + when the request omitted tools (all tools). A present + selection with no references represents tools: []. + } + ] + }, + "updatedAt": "2020-02-20 00:00:00" # Optional. + } + ] + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_sessions_list_request( + end_user_id=end_user_id, + page=page, + per_page=per_page, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @overload + def create( + self, body: JSON, *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create an Action Gateway Session. + + Creates a session with a tool selection, invocation policy, and optional direct-tool preload + configuration. + + :param body: Required. + :type body: JSON + :keyword content_type: Body Parameter content-type. Content type parameter for JSON body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "actor_id": "str", # Required. + "name": "str", # Required. + "config": { + "preloadTools": [ + "str" # Optional. Concrete tools or pinned toolbelts to + expose directly beside the session meta-tools. + ] + }, + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. Known + values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. + Lowercase values are canonical so ProtoJSON matches the public REST + vocabulary; the prefixed aliases preserve compatibility for existing + protobuf clients. Known values are: "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary of + :code:``. + }, + "tool": "str" # Optional. Invocation policy. Omit to + use a default action of ask. + } + ] + }, + "tools": [ + "str" # Optional. Omitted enables every tool. An explicit empty + array enables no tools. Direct tools may be :code:`` or + @:code:``; toolbelt references must be version-pinned as + toolbelt::code:``@:code:``. + ] + } + + # response body for status code(s): 200 + response == { + "mcpUrl": "str", # Public session-pinned MCP URL. Required. + "session": { + "actorId": "str", # Optional. actor_id is empty when the session is + not bound to an actor. + "config": {}, # Optional. Preserved as an opaque object. Gateway + currently interprets config.preloadTools to add selected direct tools to the + session MCP. + "createdAt": "2020-02-20 00:00:00", # Optional. A session and the + tool-permission policy bound to it. Required. + "name": "str", # Optional. name is the required human-readable + session name. + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. + Known values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value + is "ask". SessionPolicyAction is the disposition applied to a + tool call. Lowercase values are canonical so ProtoJSON matches + the public REST vocabulary; the prefixed aliases preserve + compatibility for existing protobuf clients. Known values are: + "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary + of :code:``. + }, + "tool": "str" # Optional. SessionPolicySpec + is the Gateway-relevant subset of a session's permission policy. + Filesystem and network policy remain enforced by the sandbox. + } + ] + }, + "sessionUrn": "str", # Optional. A session and the tool-permission + policy bound to it. Required. + "tools": { + "references": [ + { + "kind": "str", # Optional. Known values are: + "SESSION_TOOL_REFERENCE_KIND_TOOL" and + "SESSION_TOOL_REFERENCE_KIND_TOOLBELT". + "name": "str", # Optional. Omitted when the + request omitted tools (all tools). A present selection with no + references represents tools: []. + "version": "str" # Optional. Omitted when + the request omitted tools (all tools). A present selection with + no references represents tools: []. + } + ] + }, + "updatedAt": "2020-02-20 00:00:00" # Optional. A session and the + tool-permission policy bound to it. Required. + }, + "tools": [ + "str" # Canonical, version-pinned selected tool references. + Required. + ] + } + # response body for status code(s): 400 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @overload + def create( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create an Action Gateway Session. + + Creates a session with a tool selection, invocation policy, and optional direct-tool preload + configuration. + + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "mcpUrl": "str", # Public session-pinned MCP URL. Required. + "session": { + "actorId": "str", # Optional. actor_id is empty when the session is + not bound to an actor. + "config": {}, # Optional. Preserved as an opaque object. Gateway + currently interprets config.preloadTools to add selected direct tools to the + session MCP. + "createdAt": "2020-02-20 00:00:00", # Optional. A session and the + tool-permission policy bound to it. Required. + "name": "str", # Optional. name is the required human-readable + session name. + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. + Known values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value + is "ask". SessionPolicyAction is the disposition applied to a + tool call. Lowercase values are canonical so ProtoJSON matches + the public REST vocabulary; the prefixed aliases preserve + compatibility for existing protobuf clients. Known values are: + "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary + of :code:``. + }, + "tool": "str" # Optional. SessionPolicySpec + is the Gateway-relevant subset of a session's permission policy. + Filesystem and network policy remain enforced by the sandbox. + } + ] + }, + "sessionUrn": "str", # Optional. A session and the tool-permission + policy bound to it. Required. + "tools": { + "references": [ + { + "kind": "str", # Optional. Known values are: + "SESSION_TOOL_REFERENCE_KIND_TOOL" and + "SESSION_TOOL_REFERENCE_KIND_TOOLBELT". + "name": "str", # Optional. Omitted when the + request omitted tools (all tools). A present selection with no + references represents tools: []. + "version": "str" # Optional. Omitted when + the request omitted tools (all tools). A present selection with + no references represents tools: []. + } + ] + }, + "updatedAt": "2020-02-20 00:00:00" # Optional. A session and the + tool-permission policy bound to it. Required. + }, + "tools": [ + "str" # Canonical, version-pinned selected tool references. + Required. + ] + } + # response body for status code(s): 400 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + + @distributed_trace + def create(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Create an Action Gateway Session. + + Creates a session with a tool selection, invocation policy, and optional direct-tool preload + configuration. + + :param body: Is either a JSON type or a IO[bytes] type. Required. + :type body: JSON or IO[bytes] + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # JSON input template you can fill out and use as your body input. + body = { + "actor_id": "str", # Required. + "name": "str", # Required. + "config": { + "preloadTools": [ + "str" # Optional. Concrete tools or pinned toolbelts to + expose directly beside the session meta-tools. + ] + }, + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. Known + values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. + Lowercase values are canonical so ProtoJSON matches the public REST + vocabulary; the prefixed aliases preserve compatibility for existing + protobuf clients. Known values are: "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary of + :code:``. + }, + "tool": "str" # Optional. Invocation policy. Omit to + use a default action of ask. + } + ] + }, + "tools": [ + "str" # Optional. Omitted enables every tool. An explicit empty + array enables no tools. Direct tools may be :code:`` or + @:code:``; toolbelt references must be version-pinned as + toolbelt::code:``@:code:``. + ] + } + + # response body for status code(s): 200 + response == { + "mcpUrl": "str", # Public session-pinned MCP URL. Required. + "session": { + "actorId": "str", # Optional. actor_id is empty when the session is + not bound to an actor. + "config": {}, # Optional. Preserved as an opaque object. Gateway + currently interprets config.preloadTools to add selected direct tools to the + session MCP. + "createdAt": "2020-02-20 00:00:00", # Optional. A session and the + tool-permission policy bound to it. Required. + "name": "str", # Optional. name is the required human-readable + session name. + "policy": { + "defaultAction": "ask", # Optional. Default value is "ask". + SessionPolicyAction is the disposition applied to a tool call. Lowercase + values are canonical so ProtoJSON matches the public REST vocabulary; the + prefixed aliases preserve compatibility for existing protobuf clients. + Known values are: "allow", "ask", and "deny". + "rules": [ + { + "action": "ask", # Optional. Default value + is "ask". SessionPolicyAction is the disposition applied to a + tool call. Lowercase values are canonical so ProtoJSON matches + the public REST vocabulary; the prefixed aliases preserve + compatibility for existing protobuf clients. Known values are: + "allow", "ask", and "deny". + "match": { + "str": "str" # Optional. Dictionary + of :code:``. + }, + "tool": "str" # Optional. SessionPolicySpec + is the Gateway-relevant subset of a session's permission policy. + Filesystem and network policy remain enforced by the sandbox. + } + ] + }, + "sessionUrn": "str", # Optional. A session and the tool-permission + policy bound to it. Required. + "tools": { + "references": [ + { + "kind": "str", # Optional. Known values are: + "SESSION_TOOL_REFERENCE_KIND_TOOL" and + "SESSION_TOOL_REFERENCE_KIND_TOOLBELT". + "name": "str", # Optional. Omitted when the + request omitted tools (all tools). A present selection with no + references represents tools: []. + "version": "str" # Optional. Omitted when + the request omitted tools (all tools). A present selection with + no references represents tools: []. + } + ] + }, + "updatedAt": "2020-02-20 00:00:00" # Optional. A session and the + tool-permission policy bound to it. Required. + }, + "tools": [ + "str" # Canonical, version-pinned selected tool references. + Required. + ] + } + # response body for status code(s): 400 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = kwargs.pop("params", {}) or {} + + content_type: Optional[str] = kwargs.pop( + "content_type", _headers.pop("Content-Type", None) + ) + cls: ClsType[JSON] = kwargs.pop("cls", None) + + content_type = content_type or "application/json" + _json = None + _content = None + if isinstance(body, (IOBase, bytes)): + _content = body + else: + _json = body + + _request = build_sessions_create_request( + content_type=content_type, + json=_json, + content=_content, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 400]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 400: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + @distributed_trace + def delete(self, session_urn: str, **kwargs: Any) -> JSON: + # pylint: disable=line-too-long + """Delete an Action Gateway Session. + + Deletes an Action Gateway session owned by the authenticated team. + + :param session_urn: The URL-encoded managed agents session URN. Required. + :type session_urn: str + :return: JSON or JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 404 + response == { + "id": "str", # A short identifier corresponding to the HTTP status code + returned. For example, the ID for a response returning a 404 status code would + be "not_found.". Required. + "message": "str", # A message providing additional information about the + error, including details to help resolve it when possible. Required. + "request_id": "str" # Optional. Optionally, some endpoints may include a + request ID that should be provided when reporting bugs or opening support + tickets to help identify the issue. + } + """ + error_map: MutableMapping[int, Type[HttpResponseError]] = { + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + 401: cast( + Type[HttpResponseError], + lambda response: ClientAuthenticationError(response=response), + ), + 429: HttpResponseError, + 500: HttpResponseError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[JSON] = kwargs.pop("cls", None) + + _request = build_sessions_delete_request( + session_urn=session_urn, + headers=_headers, + params=_params, + ) + _request.url = self._client.format_url(_request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + _request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200, 404]: + if _stream: + response.read() # Load the body in memory and close the socket + map_error(status_code=response.status_code, response=response, error_map=error_map) # type: ignore + raise HttpResponseError(response=response) + + response_headers = {} + if response.status_code == 200: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if response.status_code == 404: + response_headers["ratelimit-limit"] = self._deserialize( + "int", response.headers.get("ratelimit-limit") + ) + response_headers["ratelimit-remaining"] = self._deserialize( + "int", response.headers.get("ratelimit-remaining") + ) + response_headers["ratelimit-reset"] = self._deserialize( + "int", response.headers.get("ratelimit-reset") + ) + + if response.content: + deserialized = response.json() + else: + deserialized = None + + if cls: + return cls(pipeline_response, cast(JSON, deserialized), response_headers) # type: ignore + + return cast(JSON, deserialized) # type: ignore + + +class OneClicksOperations: + """ + .. warning:: + **DO NOT** instantiate this class directly. + + Instead, you should access the following operations through + :class:`~pydo.GeneratedClient`'s + :attr:`one_clicks` attribute. + """ + + def __init__(self, *args, **kwargs): + input_args = list(args) + self._client = input_args.pop(0) if input_args else kwargs.pop("client") + self._config = input_args.pop(0) if input_args else kwargs.pop("config") + self._serialize = input_args.pop(0) if input_args else kwargs.pop("serializer") + self._deserialize = ( + input_args.pop(0) if input_args else kwargs.pop("deserializer") + ) + + @distributed_trace + def list(self, *, type: Optional[str] = None, **kwargs: Any) -> JSON: + """List 1-Click Applications. + + To list all available 1-Click applications, send a GET request to ``/v2/1-clicks``. The + ``type`` may + be provided as query paramater in order to restrict results to a certain type of 1-Click, for + example: ``/v2/1-clicks?type=droplet``. Current supported types are ``kubernetes`` and + ``droplet``. + + The response will be a JSON object with a key called ``1_clicks``. This will be set to an array + of + 1-Click application data, each of which will contain the the slug and type for the 1-Click. + + :keyword type: Restrict results to a certain type of 1-Click. Known values are: "droplet" and + "kubernetes". Default value is None. + :paramtype type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 200 + response == { + "1_clicks": [ + { + "slug": "str", # The slug identifier for the 1-Click + application. Required. + "type": "str" # The type of the 1-Click application. + Required. } ] } @@ -29207,7 +34315,7 @@ def create( }, "project_id": "str" # Optional. The ID of the project the app should be assigned to. If omitted, it will be assigned to your default project. - :code:`
`:code:`
`Requires ``project:update`` scope. + :code:`
`:code:`
`Requires ``project:assign_resource`` scope. } # response body for status code(s): 200 @@ -45930,7 +51038,7 @@ def create(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: }, "project_id": "str" # Optional. The ID of the project the app should be assigned to. If omitted, it will be assigned to your default project. - :code:`
`:code:`
`Requires ``project:update`` scope. + :code:`
`:code:`
`Requires ``project:assign_resource`` scope. } # response body for status code(s): 200 @@ -127197,8 +132305,10 @@ def list_clusters(self, *, tag_name: Optional[str] = None, **kwargs: Any) -> JSO } ], "pg_allow_replication": bool # - Optional. For Postgres clusters, set to ``true`` for a user - with replication rights. This option is not currently + Optional. For PostgreSQL clusters, set to ``true`` to grant + the user replication privileges. When omitted on create or + update, the value defaults to ``false`` and replication + privileges are not granted. This option is not currently supported for other database engines. } } @@ -127474,7 +132584,7 @@ def create_cluster( "project_id": "str", # Optional. The ID of the project that the database cluster is assigned to. If excluded when creating a new database cluster, it will be assigned to your default project.:code:`
`:code:`
`Requires - ``project:update`` scope. + ``project:assign_resource`` scope. "rules": [ { "type": "str", # The type of resource that the firewall rule @@ -127651,9 +132761,10 @@ def create_cluster( } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user replication + privileges. When omitted on create or update, the value defaults to + ``false`` and replication privileges are not granted. This option is + not currently supported for other database engines. } } ], @@ -127955,9 +133066,11 @@ def create_cluster( } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user + replication privileges. When omitted on create or update, the + value defaults to ``false`` and replication privileges are not + granted. This option is not currently supported for other + database engines. } } ], @@ -128328,9 +133441,11 @@ def create_cluster( } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user + replication privileges. When omitted on create or update, the + value defaults to ``false`` and replication privileges are not + granted. This option is not currently supported for other + database engines. } } ], @@ -128522,7 +133637,7 @@ def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: "project_id": "str", # Optional. The ID of the project that the database cluster is assigned to. If excluded when creating a new database cluster, it will be assigned to your default project.:code:`
`:code:`
`Requires - ``project:update`` scope. + ``project:assign_resource`` scope. "rules": [ { "type": "str", # The type of resource that the firewall rule @@ -128699,9 +133814,10 @@ def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user replication + privileges. When omitted on create or update, the value defaults to + ``false`` and replication privileges are not granted. This option is + not currently supported for other database engines. } } ], @@ -129003,9 +134119,11 @@ def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user + replication privileges. When omitted on create or update, the + value defaults to ``false`` and replication privileges are not + granted. This option is not currently supported for other + database engines. } } ], @@ -129439,9 +134557,11 @@ def get_cluster(self, database_cluster_uuid: str, **kwargs: Any) -> JSON: } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user + replication privileges. When omitted on create or update, the + value defaults to ``false`` and replication privileges are not + granted. This option is not currently supported for other + database engines. } } ], @@ -131249,10 +136369,11 @@ def update_firewall_rules( resources should be able to open connections to the database. You may limit connections to specific Droplets, Kubernetes clusters, or IP addresses. When a tag is provided, any Droplet or Kubernetes node with that tag applied to it will have access. The firewall is limited to 100 - rules (or trusted sources). When possible, we recommend `placing your databases into a VPC - network `_ to limit access to them - instead of using a firewall. - A successful. + rules (or trusted sources). You cannot add IPv6 addresses as trusted sources. For additional + limits, see your database engine's limits page. When possible, we recommend `placing your + databases into a VPC network `_ to + limit access to them instead of using a firewall. + A successful request returns a 204 status code with no content. :param database_cluster_uuid: A unique identifier for a database cluster. Required. :type database_cluster_uuid: str @@ -131321,10 +136442,11 @@ def update_firewall_rules( resources should be able to open connections to the database. You may limit connections to specific Droplets, Kubernetes clusters, or IP addresses. When a tag is provided, any Droplet or Kubernetes node with that tag applied to it will have access. The firewall is limited to 100 - rules (or trusted sources). When possible, we recommend `placing your databases into a VPC - network `_ to limit access to them - instead of using a firewall. - A successful. + rules (or trusted sources). You cannot add IPv6 addresses as trusted sources. For additional + limits, see your database engine's limits page. When possible, we recommend `placing your + databases into a VPC network `_ to + limit access to them instead of using a firewall. + A successful request returns a 204 status code with no content. :param database_cluster_uuid: A unique identifier for a database cluster. Required. :type database_cluster_uuid: str @@ -131365,10 +136487,11 @@ def update_firewall_rules( resources should be able to open connections to the database. You may limit connections to specific Droplets, Kubernetes clusters, or IP addresses. When a tag is provided, any Droplet or Kubernetes node with that tag applied to it will have access. The firewall is limited to 100 - rules (or trusted sources). When possible, we recommend `placing your databases into a VPC - network `_ to limit access to them - instead of using a firewall. - A successful. + rules (or trusted sources). You cannot add IPv6 addresses as trusted sources. For additional + limits, see your database engine's limits page. When possible, we recommend `placing your + databases into a VPC network `_ to + limit access to them instead of using a firewall. + A successful request returns a 204 status code with no content. :param database_cluster_uuid: A unique identifier for a database cluster. Required. :type database_cluster_uuid: str @@ -133781,6 +138904,11 @@ def list_users(self, database_cluster_uuid: str, **kwargs: Any) -> JSON: For MySQL clusters, additional options will be contained in the mysql_settings object. + For PostgreSQL clusters, additional options will be contained in the ``settings`` + object (for example, ``pg_allow_replication``\\ ). + + For Kafka clusters, additional options will be contained in the ``settings`` object. + For MongoDB clusters, additional information will be contained in the mongo_user_settings object. @@ -133871,9 +138999,10 @@ def list_users(self, database_cluster_uuid: str, **kwargs: Any) -> JSON: } ], "pg_allow_replication": bool # Optional. For - Postgres clusters, set to ``true`` for a user with replication - rights. This option is not currently supported for other database - engines. + PostgreSQL clusters, set to ``true`` to grant the user replication + privileges. When omitted on create or update, the value defaults to + ``false`` and replication privileges are not granted. This option is + not currently supported for other database engines. } } ] @@ -133988,10 +139117,14 @@ def add_user( When adding a user to a MySQL cluster, additional options can be configured in the ``mysql_settings`` object. + When adding a user to a PostgreSQL cluster, additional options can be configured in + the ``settings`` object (for example, ``pg_allow_replication``\\ ). When + ``pg_allow_replication`` is omitted, it defaults to ``false``. + When adding a user to a Kafka cluster, additional options can be configured in the ``settings`` object. - When adding a user to a MongoDB cluster, additional options can be configured in + When adding a user to a MongoDB cluster, additional options can be configured in the ``settings.mongo_user_settings`` object. The response will be a JSON object with a key called ``user``. The value of this will be an @@ -134083,9 +139216,11 @@ def add_user( values are: "deny", "admin", "read", "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres clusters, set - to ``true`` for a user with replication rights. This option is not currently - supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL clusters, + set to ``true`` to grant the user replication privileges. When omitted on + create or update, the value defaults to ``false`` and replication privileges + are not granted. This option is not currently supported for other database + engines. } } @@ -134160,9 +139295,11 @@ def add_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -134199,10 +139336,14 @@ def add_user( When adding a user to a MySQL cluster, additional options can be configured in the ``mysql_settings`` object. + When adding a user to a PostgreSQL cluster, additional options can be configured in + the ``settings`` object (for example, ``pg_allow_replication``\\ ). When + ``pg_allow_replication`` is omitted, it defaults to ``false``. + When adding a user to a Kafka cluster, additional options can be configured in the ``settings`` object. - When adding a user to a MongoDB cluster, additional options can be configured in + When adding a user to a MongoDB cluster, additional options can be configured in the ``settings.mongo_user_settings`` object. The response will be a JSON object with a key called ``user``. The value of this will be an @@ -134294,9 +139435,11 @@ def add_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -134328,10 +139471,14 @@ def add_user( When adding a user to a MySQL cluster, additional options can be configured in the ``mysql_settings`` object. + When adding a user to a PostgreSQL cluster, additional options can be configured in + the ``settings`` object (for example, ``pg_allow_replication``\\ ). When + ``pg_allow_replication`` is omitted, it defaults to ``false``. + When adding a user to a Kafka cluster, additional options can be configured in the ``settings`` object. - When adding a user to a MongoDB cluster, additional options can be configured in + When adding a user to a MongoDB cluster, additional options can be configured in the ``settings.mongo_user_settings`` object. The response will be a JSON object with a key called ``user``. The value of this will be an @@ -134420,9 +139567,11 @@ def add_user( values are: "deny", "admin", "read", "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres clusters, set - to ``true`` for a user with replication rights. This option is not currently - supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL clusters, + set to ``true`` to grant the user replication privileges. When omitted on + create or update, the value defaults to ``false`` and replication privileges + are not granted. This option is not currently supported for other database + engines. } } @@ -134497,9 +139646,11 @@ def add_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -134626,6 +139777,9 @@ def get_user( For MySQL clusters, additional options will be contained in the ``mysql_settings`` object. + For PostgreSQL clusters, additional options will be contained in the ``settings`` + object (for example, ``pg_allow_replication``\\ ). + For Kafka clusters, additional options will be contained in the ``settings`` object. For MongoDB clusters, additional information will be contained in the mongo_user_settings @@ -134713,9 +139867,11 @@ def get_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -134944,8 +140100,14 @@ def update_user( the name of a user, you must recreate a new user. + For PostgreSQL clusters, you can update ``settings.pg_allow_replication`` to enable or + disable replication privileges for the user. When omitted, the value defaults to ``false``. + + For Kafka and OpenSearch clusters, additional options can be configured in the + ``settings`` object (for example, topic or index ACLs). + The response will be a JSON object with a key called ``user``. The value of this will be an - object that contains the name of the update database user, along with the ``settings`` object + object that contains the name of the updated database user, along with the ``settings`` object that has been updated. @@ -135014,9 +140176,11 @@ def update_user( values are: "deny", "admin", "read", "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres clusters, set - to ``true`` for a user with replication rights. This option is not currently - supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL clusters, + set to ``true`` to grant the user replication privileges. When omitted on + create or update, the value defaults to ``false`` and replication privileges + are not granted. This option is not currently supported for other database + engines. } } @@ -135091,9 +140255,11 @@ def update_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -135131,8 +140297,14 @@ def update_user( the name of a user, you must recreate a new user. + For PostgreSQL clusters, you can update ``settings.pg_allow_replication`` to enable or + disable replication privileges for the user. When omitted, the value defaults to ``false``. + + For Kafka and OpenSearch clusters, additional options can be configured in the + ``settings`` object (for example, topic or index ACLs). + The response will be a JSON object with a key called ``user``. The value of this will be an - object that contains the name of the update database user, along with the ``settings`` object + object that contains the name of the updated database user, along with the ``settings`` object that has been updated. @@ -135223,9 +140395,11 @@ def update_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -135261,8 +140435,14 @@ def update_user( the name of a user, you must recreate a new user. + For PostgreSQL clusters, you can update ``settings.pg_allow_replication`` to enable or + disable replication privileges for the user. When omitted, the value defaults to ``false``. + + For Kafka and OpenSearch clusters, additional options can be configured in the + ``settings`` object (for example, topic or index ACLs). + The response will be a JSON object with a key called ``user``. The value of this will be an - object that contains the name of the update database user, along with the ``settings`` object + object that contains the name of the updated database user, along with the ``settings`` object that has been updated. @@ -135328,9 +140508,11 @@ def update_user( values are: "deny", "admin", "read", "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres clusters, set - to ``true`` for a user with replication rights. This option is not currently - supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL clusters, + set to ``true`` to grant the user replication privileges. When omitted on + create or update, the value defaults to ``false`` and replication privileges + are not granted. This option is not currently supported for other database + engines. } } @@ -135405,9 +140587,11 @@ def update_user( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -135638,9 +140822,11 @@ def reset_auth( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -135767,9 +140953,11 @@ def reset_auth( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -135903,9 +141091,11 @@ def reset_auth( "readwrite", and "write". } ], - "pg_allow_replication": bool # Optional. For Postgres - clusters, set to ``true`` for a user with replication rights. This option - is not currently supported for other database engines. + "pg_allow_replication": bool # Optional. For PostgreSQL + clusters, set to ``true`` to grant the user replication privileges. When + omitted on create or update, the value defaults to ``false`` and + replication privileges are not granted. This option is not currently + supported for other database engines. } } } @@ -149457,9 +154647,10 @@ class of Droplets created from this size. For example: Basic, General The unit of measure for the disk size. }, "type": "str" # Optional. The type - of disk. All Droplets contain a ``local`` disk. Additionally, - GPU Droplets can also have a ``scratch`` disk for - non-persistent data. Known values are: "local" and "scratch". + of disk. All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk + for non-persistent data. Known values are: "local", "remote", + and "scratch". } ], "gpu_info": { @@ -149504,9 +154695,10 @@ class of Droplets created from this size. For example: Basic, General of measure for the disk size. }, "type": "str" # Optional. The type of disk. - All Droplets contain a ``local`` disk. Additionally, GPU Droplets - can also have a ``scratch`` disk for non-persistent data. Known - values are: "local" and "scratch". + All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk for + non-persistent data. Known values are: "local", "remote", and + "scratch". } ], "gpu_info": { @@ -150130,9 +155322,10 @@ def get(self, droplet_id: int, **kwargs: Any) -> JSON: of measure for the disk size. }, "type": "str" # Optional. The type of disk. - All Droplets contain a ``local`` disk. Additionally, GPU Droplets - can also have a ``scratch`` disk for non-persistent data. Known - values are: "local" and "scratch". + All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk for + non-persistent data. Known values are: "local", "remote", and + "scratch". } ], "gpu_info": { @@ -150175,9 +155368,9 @@ def get(self, droplet_id: int, **kwargs: Any) -> JSON: measure for the disk size. }, "type": "str" # Optional. The type of disk. All - Droplets contain a ``local`` disk. Additionally, GPU Droplets can - also have a ``scratch`` disk for non-persistent data. Known values - are: "local" and "scratch". + Droplets contain a ``local`` or ``remote`` disk. Additionally, GPU + Droplets can also have a ``scratch`` disk for non-persistent data. + Known values are: "local", "remote", and "scratch". } ], "gpu_info": { @@ -151703,9 +156896,10 @@ class of Droplets created from this size. For example: Basic, General The unit of measure for the disk size. }, "type": "str" # Optional. The type - of disk. All Droplets contain a ``local`` disk. Additionally, - GPU Droplets can also have a ``scratch`` disk for - non-persistent data. Known values are: "local" and "scratch". + of disk. All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk + for non-persistent data. Known values are: "local", "remote", + and "scratch". } ], "gpu_info": { @@ -151750,9 +156944,10 @@ class of Droplets created from this size. For example: Basic, General of measure for the disk size. }, "type": "str" # Optional. The type of disk. - All Droplets contain a ``local`` disk. Additionally, GPU Droplets - can also have a ``scratch`` disk for non-persistent data. Known - values are: "local" and "scratch". + All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk for + non-persistent data. Known values are: "local", "remote", and + "scratch". } ], "gpu_info": { @@ -165915,6 +171110,10 @@ def list_clusters( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -166227,6 +171426,10 @@ def create_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the Peer-to-peer OCI + registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -166446,257 +171649,265 @@ def create_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, - "rdma_shared_dev_plugin": { - "enabled": bool # Optional. Indicates whether the RDMA - shared device plugin is enabled. - }, - "registry_enabled": bool, # Optional. A read-only boolean value - indicating if a container registry is integrated with the cluster. - "routing_agent": { + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, + "rdma_shared_dev_plugin": { + "enabled": bool # Optional. Indicates whether the RDMA + shared device plugin is enabled. + }, + "registry_enabled": bool, # Optional. A read-only boolean value + indicating if a container registry is integrated with the cluster. + "routing_agent": { + "enabled": bool # Optional. Indicates whether the + routing-agent component is enabled. + }, + "service_subnet": "str", # Optional. The range of assignable IP + addresses for services running in the Kubernetes cluster in CIDR notation. + "sso": { + "client_id": "str", # Optional. The OIDC client ID + registered with the identity provider. Required when ``enabled`` is + ``true``. + "enabled": False, # Optional. Default value is False. + Indicates whether SSO authentication is enabled for the cluster. + "issuer_url": "str", # Optional. The OIDC issuer URL for the + identity provider. Required when ``enabled`` is ``true``. + "required": False # Optional. Default value is False. + Indicates whether any non-SSO forms of authentication are disallowed. Can + only be set to ``true`` when ``enabled`` is ``true``. + }, + "status": { + "message": "str", # Optional. An optional message providing + additional information about the current cluster state. + "state": "str" # Optional. A string indicating the current + status of the cluster. Known values are: "running", "provisioning", + "degraded", "error", "deleted", "upgrading", and "deleting". + }, + "surge_upgrade": False, # Optional. Default value is False. A + boolean value indicating whether surge upgrade is enabled/disabled for the + cluster. Surge upgrade makes cluster upgrades fast and reliable by bringing + up new nodes before destroying the outdated nodes. + "tags": [ + "str" # Optional. An array of tags to apply to the + Kubernetes cluster. All clusters are automatically tagged ``k8s`` and + ``k8s:$K8S_CLUSTER_ID``. :code:`
`:code:`
`Requires ``tag:read`` + and ``tag:create`` scope, as well as ``tag:delete`` if existing tags are + getting removed. + ], + "updated_at": "2020-02-20 00:00:00", # Optional. A time value given + in ISO8601 combined date and time format that represents when the Kubernetes + cluster was last updated. + "vpc_uuid": "str", # Optional. A string specifying the UUID of the + VPC to which the Kubernetes cluster is + assigned.:code:`
`:code:`
`Requires ``vpc:read`` scope. + "worker_subnet_uuid": "str" # Optional. The UUID of the VPC subnet + to attach worker nodes to. When omitted on create, the default subnet for the + VPC is used. This value cannot be changed after the cluster is created. + ``vpc_uuid`` must also be set. :code:`
`:code:`
`Requires ``vpc:read`` + scope. + } + } + """ + + @overload + def create_cluster( + self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any + ) -> JSON: + # pylint: disable=line-too-long + """Create a New Kubernetes Cluster. + + To create a new Kubernetes cluster, send a POST request to + ``/v2/kubernetes/clusters``. The request must contain at least one node pool + with at least one worker. + + The request may contain a maintenance window policy describing a time period + when disruptive maintenance tasks may be carried out. Omitting the policy + implies that a window will be chosen automatically. See + `here `_ + for details. + + :param body: Required. + :type body: IO[bytes] + :keyword content_type: Body Parameter content-type. Content type parameter for binary body. + Default value is "application/json". + :paramtype content_type: str + :return: JSON object + :rtype: JSON + :raises ~azure.core.exceptions.HttpResponseError: + + Example: + .. code-block:: python + + # response body for status code(s): 201 + response == { + "kubernetes_cluster": { + "name": "str", # A human-readable name for a Kubernetes cluster. + Required. + "node_pools": [ + { + "auto_scale": bool, # Optional. A boolean value + indicating whether auto-scaling is enabled for this node pool. + "count": 0, # Optional. The number of Droplet + instances in the node pool. + "id": "str", # Optional. A unique ID that can be + used to identify and reference a specific node pool. + "labels": {}, # Optional. An object of key/value + mappings specifying labels to apply to all nodes in a pool. Labels + will automatically be applied to all existing nodes and any + subsequent nodes added to the pool. Note that when a label is + removed, it is not deleted from the nodes in the pool. + "max_nodes": 0, # Optional. The maximum number of + nodes that this node pool can be auto-scaled to. The value will be + ``0`` if ``auto_scale`` is set to ``false``. + "min_nodes": 0, # Optional. The minimum number of + nodes that this node pool can be auto-scaled to. The value will be + ``0`` if ``auto_scale`` is set to ``false``. + "name": "str", # Optional. A human-readable name for + the node pool. + "nodes": [ + { + "created_at": "2020-02-20 00:00:00", + # Optional. A time value given in ISO8601 combined date and + time format that represents when the node was created. + "droplet_id": "str", # Optional. The + ID of the Droplet used for the worker node. + "id": "str", # Optional. A unique ID + that can be used to identify and reference the node. + "name": "str", # Optional. An + automatically generated, human-readable name for the node. + "status": { + "state": "str" # Optional. A + string indicating the current status of the node. Known + values are: "provisioning", "running", "draining", and + "deleting". + }, + "updated_at": "2020-02-20 00:00:00" + # Optional. A time value given in ISO8601 combined date and + time format that represents when the node was last updated. + } + ], + "size": "str", # Optional. The slug identifier for + the type of Droplet used as workers in the node pool. + "tags": [ + "str" # Optional. An array containing the + tags applied to the node pool. All node pools are automatically + tagged ``k8s``"" , ``k8s-worker``"" , and + ``k8s:$K8S_CLUSTER_ID``. :code:`
`:code:`
`Requires + ``tag:read`` scope. + ], + "taints": [ + { + "effect": "str", # Optional. How the + node reacts to pods that it won't tolerate. Available effect + values are ``NoSchedule``"" , ``PreferNoSchedule``"" , and + ``NoExecute``. Known values are: "NoSchedule", + "PreferNoSchedule", and "NoExecute". + "key": "str", # Optional. An + arbitrary string. The ``key`` and ``value`` fields of the + ``taint`` object form a key-value pair. For example, if the + value of the ``key`` field is "special" and the value of the + ``value`` field is "gpu", the key value pair would be + ``special=gpu``. + "value": "str" # Optional. An + arbitrary string. The ``key`` and ``value`` fields of the + ``taint`` object form a key-value pair. For example, if the + value of the ``key`` field is "special" and the value of the + ``value`` field is "gpu", the key value pair would be + ``special=gpu``. + } + ] + } + ], + "region": "str", # The slug identifier for the region where the + Kubernetes cluster is located. Required. + "version": "str", # The slug identifier for the version of + Kubernetes used for the cluster. If set to a minor version (e.g. "1.14"), the + latest version within it will be used (e.g. "1.14.6-do.1"); if set to + "latest", the latest published version will be used. See the + ``/v2/kubernetes/options`` endpoint to find all currently available versions. + Required. + "amd_gpu_device_metrics_exporter_plugin": { + "enabled": bool # Optional. Indicates whether the AMD Device + Metrics Exporter is enabled. + }, + "amd_gpu_device_plugin": { + "enabled": bool # Optional. Indicates whether the AMD GPU + Device Plugin is enabled. + }, + "auto_upgrade": False, # Optional. Default value is False. A boolean + value indicating whether the cluster will be automatically upgraded to new + patch releases during its maintenance window. + "cluster_autoscaler_configuration": { + "expanders": [ + "str" # Optional. Customizes expanders used by + cluster-autoscaler. The autoscaler will apply each expander from the + provided list to narrow down the selection of node types created to + scale up, until either a single node type is left, or the list of + expanders is exhausted. If this flag is unset, autoscaler will use + its default expander ``random``. Passing an empty list ("" *not* + ``null``"" ) will unset any previous expander customizations. + Available expanders: * ``random``"" : Randomly selects a node group + to scale. * `priority`: Selects the node group with the highest + priority as per [user-provided + configuration](https://docs.digitalocean.com/products/kubernetes/how-to/autoscale/#configuring-priority-expander) + * ``least_waste``"" : Selects the node group that will result in the + least amount of idle resources. + ], + "scale_down_unneeded_time": "str", # Optional. Used to + customize how long a node is unneeded before being scaled down. + "scale_down_utilization_threshold": 0.0 # Optional. Used to + customize when cluster autoscaler scales down non-empty nodes by setting + the node utilization threshold. + }, + "cluster_subnet": "str", # Optional. The range of IP addresses for + the overlay network of the Kubernetes cluster in CIDR notation. + "control_plane_firewall": { + "allowed_addresses": [ + "str" # Optional. An array of public addresses (IPv4 + or CIDR) allowed to access the control plane. + ], + "enabled": bool # Optional. Indicates whether the control + plane firewall is enabled. + }, + "coredns_autoscaler": { + "enabled": bool # Optional. Indicates whether the CoreDNS + Cluster Proportional Autoscaler add-on is enabled. + }, + "created_at": "2020-02-20 00:00:00", # Optional. A time value given + in ISO8601 combined date and time format that represents when the Kubernetes + cluster was created. + "endpoint": "str", # Optional. The base URL of the API server on the + Kubernetes master node. + "ha": bool, # Optional. A boolean value indicating whether the + control plane is run in a highly available configuration in the cluster. + Highly available control planes incur less downtime. The property cannot be + disabled. When omitted on create, the default is version-dependent; for DOKS + 1.36.0 and later, the default is true; for earlier versions, the default is + false. + "id": "str", # Optional. A unique ID that can be used to identify + and reference a Kubernetes cluster. + "ipv4": "str", # Optional. The public IPv4 address of the Kubernetes + master node. This will not be set if high availability is configured on the + cluster (v1.21+). + "maintenance_policy": { + "day": "str", # Optional. The day of the maintenance window + policy. May be one of ``monday`` through ``sunday``"" , or ``any`` to + indicate an arbitrary week day. Known values are: "any", "monday", + "tuesday", "wednesday", "thursday", "friday", "saturday", and "sunday". + "duration": "str", # Optional. The duration of the + maintenance window policy in human-readable format. + "start_time": "str" # Optional. The start time in UTC of the + maintenance window policy in 24-hour clock format / HH:MM notation (e.g., + ``15:00``"" ). + }, + "nvidia_gpu_device_plugin": { + "enabled": bool # Optional. Indicates whether the Nvidia GPU + Device Plugin is enabled. + }, + "p2p_oci_registry_plugin": { "enabled": bool # Optional. Indicates whether the - routing-agent component is enabled. - }, - "service_subnet": "str", # Optional. The range of assignable IP - addresses for services running in the Kubernetes cluster in CIDR notation. - "sso": { - "client_id": "str", # Optional. The OIDC client ID - registered with the identity provider. Required when ``enabled`` is - ``true``. - "enabled": False, # Optional. Default value is False. - Indicates whether SSO authentication is enabled for the cluster. - "issuer_url": "str", # Optional. The OIDC issuer URL for the - identity provider. Required when ``enabled`` is ``true``. - "required": False # Optional. Default value is False. - Indicates whether any non-SSO forms of authentication are disallowed. Can - only be set to ``true`` when ``enabled`` is ``true``. - }, - "status": { - "message": "str", # Optional. An optional message providing - additional information about the current cluster state. - "state": "str" # Optional. A string indicating the current - status of the cluster. Known values are: "running", "provisioning", - "degraded", "error", "deleted", "upgrading", and "deleting". - }, - "surge_upgrade": False, # Optional. Default value is False. A - boolean value indicating whether surge upgrade is enabled/disabled for the - cluster. Surge upgrade makes cluster upgrades fast and reliable by bringing - up new nodes before destroying the outdated nodes. - "tags": [ - "str" # Optional. An array of tags to apply to the - Kubernetes cluster. All clusters are automatically tagged ``k8s`` and - ``k8s:$K8S_CLUSTER_ID``. :code:`
`:code:`
`Requires ``tag:read`` - and ``tag:create`` scope, as well as ``tag:delete`` if existing tags are - getting removed. - ], - "updated_at": "2020-02-20 00:00:00", # Optional. A time value given - in ISO8601 combined date and time format that represents when the Kubernetes - cluster was last updated. - "vpc_uuid": "str", # Optional. A string specifying the UUID of the - VPC to which the Kubernetes cluster is - assigned.:code:`
`:code:`
`Requires ``vpc:read`` scope. - "worker_subnet_uuid": "str" # Optional. The UUID of the VPC subnet - to attach worker nodes to. When omitted on create, the default subnet for the - VPC is used. This value cannot be changed after the cluster is created. - ``vpc_uuid`` must also be set. :code:`
`:code:`
`Requires ``vpc:read`` - scope. - } - } - """ - - @overload - def create_cluster( - self, body: IO[bytes], *, content_type: str = "application/json", **kwargs: Any - ) -> JSON: - # pylint: disable=line-too-long - """Create a New Kubernetes Cluster. - - To create a new Kubernetes cluster, send a POST request to - ``/v2/kubernetes/clusters``. The request must contain at least one node pool - with at least one worker. - - The request may contain a maintenance window policy describing a time period - when disruptive maintenance tasks may be carried out. Omitting the policy - implies that a window will be chosen automatically. See - `here `_ - for details. - - :param body: Required. - :type body: IO[bytes] - :keyword content_type: Body Parameter content-type. Content type parameter for binary body. - Default value is "application/json". - :paramtype content_type: str - :return: JSON object - :rtype: JSON - :raises ~azure.core.exceptions.HttpResponseError: - - Example: - .. code-block:: python - - # response body for status code(s): 201 - response == { - "kubernetes_cluster": { - "name": "str", # A human-readable name for a Kubernetes cluster. - Required. - "node_pools": [ - { - "auto_scale": bool, # Optional. A boolean value - indicating whether auto-scaling is enabled for this node pool. - "count": 0, # Optional. The number of Droplet - instances in the node pool. - "id": "str", # Optional. A unique ID that can be - used to identify and reference a specific node pool. - "labels": {}, # Optional. An object of key/value - mappings specifying labels to apply to all nodes in a pool. Labels - will automatically be applied to all existing nodes and any - subsequent nodes added to the pool. Note that when a label is - removed, it is not deleted from the nodes in the pool. - "max_nodes": 0, # Optional. The maximum number of - nodes that this node pool can be auto-scaled to. The value will be - ``0`` if ``auto_scale`` is set to ``false``. - "min_nodes": 0, # Optional. The minimum number of - nodes that this node pool can be auto-scaled to. The value will be - ``0`` if ``auto_scale`` is set to ``false``. - "name": "str", # Optional. A human-readable name for - the node pool. - "nodes": [ - { - "created_at": "2020-02-20 00:00:00", - # Optional. A time value given in ISO8601 combined date and - time format that represents when the node was created. - "droplet_id": "str", # Optional. The - ID of the Droplet used for the worker node. - "id": "str", # Optional. A unique ID - that can be used to identify and reference the node. - "name": "str", # Optional. An - automatically generated, human-readable name for the node. - "status": { - "state": "str" # Optional. A - string indicating the current status of the node. Known - values are: "provisioning", "running", "draining", and - "deleting". - }, - "updated_at": "2020-02-20 00:00:00" - # Optional. A time value given in ISO8601 combined date and - time format that represents when the node was last updated. - } - ], - "size": "str", # Optional. The slug identifier for - the type of Droplet used as workers in the node pool. - "tags": [ - "str" # Optional. An array containing the - tags applied to the node pool. All node pools are automatically - tagged ``k8s``"" , ``k8s-worker``"" , and - ``k8s:$K8S_CLUSTER_ID``. :code:`
`:code:`
`Requires - ``tag:read`` scope. - ], - "taints": [ - { - "effect": "str", # Optional. How the - node reacts to pods that it won't tolerate. Available effect - values are ``NoSchedule``"" , ``PreferNoSchedule``"" , and - ``NoExecute``. Known values are: "NoSchedule", - "PreferNoSchedule", and "NoExecute". - "key": "str", # Optional. An - arbitrary string. The ``key`` and ``value`` fields of the - ``taint`` object form a key-value pair. For example, if the - value of the ``key`` field is "special" and the value of the - ``value`` field is "gpu", the key value pair would be - ``special=gpu``. - "value": "str" # Optional. An - arbitrary string. The ``key`` and ``value`` fields of the - ``taint`` object form a key-value pair. For example, if the - value of the ``key`` field is "special" and the value of the - ``value`` field is "gpu", the key value pair would be - ``special=gpu``. - } - ] - } - ], - "region": "str", # The slug identifier for the region where the - Kubernetes cluster is located. Required. - "version": "str", # The slug identifier for the version of - Kubernetes used for the cluster. If set to a minor version (e.g. "1.14"), the - latest version within it will be used (e.g. "1.14.6-do.1"); if set to - "latest", the latest published version will be used. See the - ``/v2/kubernetes/options`` endpoint to find all currently available versions. - Required. - "amd_gpu_device_metrics_exporter_plugin": { - "enabled": bool # Optional. Indicates whether the AMD Device - Metrics Exporter is enabled. - }, - "amd_gpu_device_plugin": { - "enabled": bool # Optional. Indicates whether the AMD GPU - Device Plugin is enabled. - }, - "auto_upgrade": False, # Optional. Default value is False. A boolean - value indicating whether the cluster will be automatically upgraded to new - patch releases during its maintenance window. - "cluster_autoscaler_configuration": { - "expanders": [ - "str" # Optional. Customizes expanders used by - cluster-autoscaler. The autoscaler will apply each expander from the - provided list to narrow down the selection of node types created to - scale up, until either a single node type is left, or the list of - expanders is exhausted. If this flag is unset, autoscaler will use - its default expander ``random``. Passing an empty list ("" *not* - ``null``"" ) will unset any previous expander customizations. - Available expanders: * ``random``"" : Randomly selects a node group - to scale. * `priority`: Selects the node group with the highest - priority as per [user-provided - configuration](https://docs.digitalocean.com/products/kubernetes/how-to/autoscale/#configuring-priority-expander) - * ``least_waste``"" : Selects the node group that will result in the - least amount of idle resources. - ], - "scale_down_unneeded_time": "str", # Optional. Used to - customize how long a node is unneeded before being scaled down. - "scale_down_utilization_threshold": 0.0 # Optional. Used to - customize when cluster autoscaler scales down non-empty nodes by setting - the node utilization threshold. - }, - "cluster_subnet": "str", # Optional. The range of IP addresses for - the overlay network of the Kubernetes cluster in CIDR notation. - "control_plane_firewall": { - "allowed_addresses": [ - "str" # Optional. An array of public addresses (IPv4 - or CIDR) allowed to access the control plane. - ], - "enabled": bool # Optional. Indicates whether the control - plane firewall is enabled. - }, - "coredns_autoscaler": { - "enabled": bool # Optional. Indicates whether the CoreDNS - Cluster Proportional Autoscaler add-on is enabled. - }, - "created_at": "2020-02-20 00:00:00", # Optional. A time value given - in ISO8601 combined date and time format that represents when the Kubernetes - cluster was created. - "endpoint": "str", # Optional. The base URL of the API server on the - Kubernetes master node. - "ha": bool, # Optional. A boolean value indicating whether the - control plane is run in a highly available configuration in the cluster. - Highly available control planes incur less downtime. The property cannot be - disabled. When omitted on create, the default is version-dependent; for DOKS - 1.36.0 and later, the default is true; for earlier versions, the default is - false. - "id": "str", # Optional. A unique ID that can be used to identify - and reference a Kubernetes cluster. - "ipv4": "str", # Optional. The public IPv4 address of the Kubernetes - master node. This will not be set if high availability is configured on the - cluster (v1.21+). - "maintenance_policy": { - "day": "str", # Optional. The day of the maintenance window - policy. May be one of ``monday`` through ``sunday``"" , or ``any`` to - indicate an arbitrary week day. Known values are: "any", "monday", - "tuesday", "wednesday", "thursday", "friday", "saturday", and "sunday". - "duration": "str", # Optional. The duration of the - maintenance window policy in human-readable format. - "start_time": "str" # Optional. The start time in UTC of the - maintenance window policy in 24-hour clock format / HH:MM notation (e.g., - ``15:00``"" ). - }, - "nvidia_gpu_device_plugin": { - "enabled": bool # Optional. Indicates whether the Nvidia GPU - Device Plugin is enabled. + Peer-to-peer OCI registry component is enabled. }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA @@ -166936,6 +172147,10 @@ def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the Peer-to-peer OCI + registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -167155,6 +172370,10 @@ def create_cluster(self, body: Union[JSON, IO[bytes]], **kwargs: Any) -> JSON: "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -167468,6 +172687,10 @@ def get_cluster(self, cluster_id: str, **kwargs: Any) -> JSON: "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -167710,6 +172933,10 @@ def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the Peer-to-peer OCI + registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -167906,6 +173133,10 @@ def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -168170,6 +173401,10 @@ def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -168327,6 +173562,10 @@ def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the Peer-to-peer OCI + registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -168523,6 +173762,10 @@ def update_cluster( "enabled": bool # Optional. Indicates whether the Nvidia GPU Device Plugin is enabled. }, + "p2p_oci_registry_plugin": { + "enabled": bool # Optional. Indicates whether the + Peer-to-peer OCI registry component is enabled. + }, "rdma_shared_dev_plugin": { "enabled": bool # Optional. Indicates whether the RDMA shared device plugin is enabled. @@ -194111,9 +199354,10 @@ def assign_resources( To assign resources to a project, send a POST request to ``/v2/projects/$PROJECT_ID/resources``. - You must have both ``project:update`` and ``:read`` scopes to assign new resources. - For example, to assign a Droplet to a project, include both the ``project:update`` and - ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to a project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param project_id: A unique identifier for a project. Required. :type project_id: str @@ -194185,9 +199429,10 @@ def assign_resources( To assign resources to a project, send a POST request to ``/v2/projects/$PROJECT_ID/resources``. - You must have both ``project:update`` and ``:read`` scopes to assign new resources. - For example, to assign a Droplet to a project, include both the ``project:update`` and - ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to a project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param project_id: A unique identifier for a project. Required. :type project_id: str @@ -194245,9 +199490,10 @@ def assign_resources( To assign resources to a project, send a POST request to ``/v2/projects/$PROJECT_ID/resources``. - You must have both ``project:update`` and ``:read`` scopes to assign new resources. - For example, to assign a Droplet to a project, include both the ``project:update`` and - ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to a project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param project_id: A unique identifier for a project. Required. :type project_id: str @@ -194534,9 +199780,10 @@ def assign_resources_default( To assign resources to your default project, send a POST request to ``/v2/projects/default/resources``. - You must have both project:update and :code:``:read scopes to assign new resources. - For example, to assign a Droplet to the default project, include both the ``project:update`` - and ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to the default project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param body: Required. :type body: JSON @@ -194601,9 +199848,10 @@ def assign_resources_default( To assign resources to your default project, send a POST request to ``/v2/projects/default/resources``. - You must have both project:update and :code:``:read scopes to assign new resources. - For example, to assign a Droplet to the default project, include both the ``project:update`` - and ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to the default project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param body: Required. :type body: IO[bytes] @@ -194659,9 +199907,10 @@ def assign_resources_default( To assign resources to your default project, send a POST request to ``/v2/projects/default/resources``. - You must have both project:update and :code:``:read scopes to assign new resources. - For example, to assign a Droplet to the default project, include both the ``project:update`` - and ``droplet:read`` scopes. + You must have both ``project:assign_resource`` and ``:read`` scopes to assign new + resources. For example, to assign a Droplet to the default project, include both the + ``project:assign_resource`` and ``droplet:read`` scopes. The ``project:update`` scope also + grants ``project:assign_resource``. :param body: Is either a JSON type or a IO[bytes] type. Required. :type body: JSON or IO[bytes] @@ -207942,9 +213191,10 @@ def list(self, *, per_page: int = 20, page: int = 1, **kwargs: Any) -> JSON: of measure for the disk size. }, "type": "str" # Optional. The type of disk. - All Droplets contain a ``local`` disk. Additionally, GPU Droplets - can also have a ``scratch`` disk for non-persistent data. Known - values are: "local" and "scratch". + All Droplets contain a ``local`` or ``remote`` disk. + Additionally, GPU Droplets can also have a ``scratch`` disk for + non-persistent data. Known values are: "local", "remote", and + "scratch". } ], "gpu_info": { diff --git a/tests/gateway/__init__.py b/tests/gateway/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/gateway/conftest.py b/tests/gateway/conftest.py new file mode 100644 index 00000000..134f8f36 --- /dev/null +++ b/tests/gateway/conftest.py @@ -0,0 +1,291 @@ +# pylint: disable=missing-function-docstring,protected-access,missing-class-docstring,too-few-public-methods +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Shared fakes for gateway tests — no network, fake pipeline plumbing.""" + +from __future__ import annotations + +import json +from types import SimpleNamespace +from typing import Any, List, Optional +from unittest.mock import MagicMock + +from pydo.aio.gateway import AsyncGatewayResources +from pydo.aio.gateway.custom_operations import AsyncRESTTransport +from pydo.aio.operations import SessionsOperations as AsyncSessionsOperations +from pydo.custom_extensions import _BaseURLProxy +from pydo.gateway import GatewayResources, RESTTransport +from pydo.operations import SessionsOperations + +TEST_SESSION_URN = "do:managed_agents_session:test-session" +TEST_GATEWAY_URL = "https://actions.do-ai-test.run" + + +class FakeResponse: + def __init__(self, status_code: int, body: Any = None): + self.status_code = status_code + self.reason = "" + self.headers: dict = {} + if isinstance(body, (dict, list)): + self._body_bytes = json.dumps(body).encode("utf-8") + elif isinstance(body, str): + self._body_bytes = body.encode("utf-8") + elif isinstance(body, bytes): + self._body_bytes = body + else: + self._body_bytes = b"" + + def text(self) -> str: + return self._body_bytes.decode("utf-8") + + @property + def content(self) -> bytes: + return self._body_bytes + + def json(self) -> Any: + return json.loads(self._body_bytes) + + def body(self) -> bytes: + return self._body_bytes + + def read(self) -> bytes: + return self._body_bytes + + def close(self) -> None: + pass + + +class AsyncFakeResponse(FakeResponse): + def __init__(self, status_code: int, body: Any = None): + super().__init__(status_code, body) + self.read_calls = 0 + + async def read(self) -> bytes: # pylint: disable=invalid-overridden-method + self.read_calls += 1 + return self._body_bytes + + +class FakePipeline: + def __init__(self, responses: List[FakeResponse]): + self._responses = list(responses) + self.calls: List[Any] = [] + + def run(self, request, *, stream=False): + self.calls.append(SimpleNamespace(request=request, stream=stream)) + return SimpleNamespace(http_response=self._responses.pop(0)) + + +class AsyncFakePipeline: + def __init__(self, responses: List[AsyncFakeResponse]): + self._responses = list(responses) + self.calls: List[Any] = [] + + async def run(self, request, *, stream=False): + self.calls.append(SimpleNamespace(request=request, stream=stream)) + return SimpleNamespace(http_response=self._responses.pop(0)) + + +def jsonrpc_result(result: Any, *, rpc_id: int = 1) -> dict: + return {"jsonrpc": "2.0", "id": rpc_id, "result": result} + + +def jsonrpc_error(code: int, message: str, *, rpc_id: int = 1) -> dict: + return { + "jsonrpc": "2.0", + "id": rpc_id, + "error": {"code": code, "message": message}, + } + + +def call_result( + structured: Any = None, + *, + is_error: bool = False, + text: str = "", + meta: Any = None, +) -> dict: + """MCP tools/call result shape (legacy helper for MCP-specific tests).""" + result: dict = {"isError": is_error} + if structured is not None: + result["structuredContent"] = structured + if text: + result["content"] = [{"type": "text", "text": text}] + if meta is not None: + result["_meta"] = meta + return result + + +def tool_result( + output: Any = None, *, error: Any = None, call_id: str = "call_1" +) -> dict: + """REST ToolResult envelope (search / code).""" + if error is not None: + return {"status": "failed", "error": error, "call_id": call_id} + return {"status": "succeeded", "output": output, "call_id": call_id} + + +def invoke_envelope( + results: List[dict] | None = None, + *, + tool: str = "web_search", + output: Any = None, + error: Any = None, + invocation_id: str | None = None, +) -> dict: + """Build a gateway ``action_invoke`` result envelope for tests.""" + if results is None: + if error is not None: + result_body: dict = {"status": "failed", "error": error} + else: + if output is None: + output = {"answer": 1} + result_body = {"status": "succeeded", "output": output} + entry: dict = {"index": 0, "tool": tool, "result": result_body} + if invocation_id is not None: + entry["invocation_id"] = invocation_id + results = [entry] + return { + "total_count": len(results), + "success_count": sum( + 1 for item in results if item["result"].get("status") == "succeeded" + ), + "error_count": sum( + 1 for item in results if item["result"].get("status") != "succeeded" + ), + "results": results, + } + + +def chat_tool_response( + *, + name: str = "web_search", + arguments: str = '{"query": "do"}', + call_id: str = "call_1", +) -> dict: + """Build a chat-completions response containing one tool call.""" + return { + "choices": [ + { + "message": { + "role": "assistant", + "tool_calls": [ + { + "id": call_id, + "type": "function", + "function": { + "name": name, + "arguments": arguments, + }, + } + ], + } + } + ] + } + + +def session_create_response( + *, + session_urn: str = TEST_SESSION_URN, + name: str = "test-session", +) -> dict: + return { + "session": { + "sessionUrn": session_urn, + "name": name, + "actorId": "actor-123", + "policy": {"defaultAction": "ask", "rules": []}, + }, + "mcpUrl": f"{TEST_GATEWAY_URL}/mcp/session/test-session", + "tools": [], + } + + +def make_parent(responses: List[FakeResponse]) -> MagicMock: + parent = MagicMock() + parent._client = MagicMock() + parent._client._pipeline = FakePipeline(responses) + parent._client.format_url = lambda url, **_kwargs: ( + url if str(url).startswith("http") else f"https://api.digitalocean.com{url}" + ) + parent.sessions = SessionsOperations( + parent._client, + MagicMock(), + MagicMock(), + MagicMock(), + ) + return parent + + +def make_async_parent(responses: List[AsyncFakeResponse]) -> MagicMock: + parent = MagicMock() + parent._client = MagicMock() + parent._client._pipeline = AsyncFakePipeline(responses) + parent._client.format_url = lambda url, **_kwargs: ( + url if str(url).startswith("http") else f"https://api.digitalocean.com{url}" + ) + parent.sessions = AsyncSessionsOperations( + parent._client, + MagicMock(), + MagicMock(), + MagicMock(), + ) + return parent + + +def make_gateway( + responses: List[FakeResponse], + provider=None, + *, + session_id: str = TEST_SESSION_URN, + actor_id: str = "actor-123", +) -> GatewayResources: + parent = make_parent(responses) + proxy = _BaseURLProxy(parent._client, TEST_GATEWAY_URL) + transport = RESTTransport(proxy, session_id=session_id, actor_id=actor_id) + return GatewayResources( + parent, + gateway_endpoint=TEST_GATEWAY_URL, + provider=provider, + transport=transport, + ) + + +def make_async_gateway( + responses: List[AsyncFakeResponse], + provider=None, + *, + session_id: str = TEST_SESSION_URN, + actor_id: str = "actor-123", +) -> AsyncGatewayResources: + parent = make_async_parent(responses) + proxy = _BaseURLProxy(parent._client, TEST_GATEWAY_URL) + transport = AsyncRESTTransport(proxy, session_id=session_id, actor_id=actor_id) + return AsyncGatewayResources( + parent, + gateway_endpoint=TEST_GATEWAY_URL, + provider=provider, + transport=transport, + ) + + +def pipeline_of(gateway) -> Any: + return gateway._transport._client._original._pipeline + + +def sent_request(gateway, index: int = 0) -> Any: + return pipeline_of(gateway).calls[index].request + + +def sent_payload(gateway, index: int = 0) -> Optional[dict]: + request = sent_request(gateway, index) + content = request.content + if content is None: + return None + if isinstance(content, bytes): + content = content.decode("utf-8") + if not content: + return None + return json.loads(content) diff --git a/tests/gateway/test_action_gateway_client.py b/tests/gateway/test_action_gateway_client.py new file mode 100644 index 00000000..c32c3ae9 --- /dev/null +++ b/tests/gateway/test_action_gateway_client.py @@ -0,0 +1,275 @@ +# pylint: disable=missing-function-docstring,protected-access,missing-class-docstring,too-few-public-methods,import-outside-toplevel +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Smoke tests for ``pydo.action_gateway.ActionGatewayClient``.""" + +from __future__ import annotations + +import json +from unittest.mock import AsyncMock + +import pytest +from azure.core.exceptions import ResourceExistsError + +import pydo +import pydo.action_gateway +import pydo.aio +from pydo.action_gateway import ActionGatewayClient +from pydo.gateway.transport import _META_TOOL_DEFINITIONS +from pydo.gateway import ( + ChatCompletionsProvider, + GatewayProtocolError, + MessagesProvider, + Toolbelt, +) + +from .conftest import ( + FakeResponse, + call_result, + chat_tool_response, + jsonrpc_result, + session_create_response, +) + +try: + import aiohttp # pylint: disable=unused-import + + _HAS_AIO = True +except ImportError: # pragma: no cover + _HAS_AIO = False + + +def test_namespace_module_exports(): + assert hasattr(pydo.action_gateway, "Client") + assert hasattr(pydo.action_gateway, "ActionGatewayClient") + assert hasattr(pydo.action_gateway, "Session") + assert hasattr(pydo.action_gateway, "TokenCredentials") + assert "Client" in pydo.action_gateway.__all__ + + +def test_namespace_client_is_subclass_of_core_client(): + assert issubclass(ActionGatewayClient, pydo.Client) + + +def test_namespace_client_dir_is_gateway_focused(): + client = ActionGatewayClient(token="dummy") + surface = set(dir(client)) + expected = { + "sessions", + "sessions_api", + "connections", + "provider", + "base_url", + "chat", + "create_toolbelt", + "messages", + "responses", + "session", + "toolbelts", + "tools", + "users", + } + assert expected <= surface + for attr in ("code", "handle_tool_calls", "droplets"): + assert attr not in surface + + +def test_namespace_client_repr_is_distinct(): + client = ActionGatewayClient(token="dummy") + assert repr(client) == "" + + +def test_sessions_delegate_to_gateway(): + client = ActionGatewayClient(token="dummy") + assert client.sessions is client.gateway.sessions + assert client.session is client.sessions + assert client.sessions_api is not client.sessions + assert client.connections is not None + assert client.tools is not None + assert client.toolbelts is not None + assert client.users is not None + assert client.provider is client.gateway.provider + + +def test_gateway_provider_kwarg(): + client = ActionGatewayClient( + token="dummy", + gateway_provider=MessagesProvider(), + ) + assert isinstance(client.provider, MessagesProvider) + assert not isinstance(client.provider, ChatCompletionsProvider) + + +def test_create_toolbelt_convenience_method(monkeypatch): + client = ActionGatewayClient(token="dummy") + response = FakeResponse( + 200, + { + "toolbelt": { + "name": "search-toolbelt", + "version": "1", + "reference": "search-toolbelt@1", + "tools": ["exa_web_search"], + } + }, + ) + + class Pipeline: + def __init__(self): + self.calls = [] + + def run(self, request, **_kwargs): + self.calls.append(request) + return type("R", (), {"http_response": response})() + + monkeypatch.setattr(client._client, "_pipeline", Pipeline()) + + toolbelt = client.create_toolbelt( + name="search-toolbelt", + tools=["exa_web_search"], + ) + + assert toolbelt.ref == "search-toolbelt@1" + request = client._client._pipeline.calls[0] + assert request.url.endswith("/v2/action-gateway/toolbelts") + assert json.loads(request.content) == { + "name": "search-toolbelt", + "tools": ["exa_web_search"], + } + + +def test_create_toolbelt_accepts_flat_api_response(monkeypatch): + client = ActionGatewayClient(token="dummy") + monkeypatch.setattr( + client.toolbelts, + "create", + lambda **_kwargs: { + "name": "search-toolbelt", + "version": "1", + "reference": "search-toolbelt@1", + "tools": ["exa_web_search"], + }, + ) + + toolbelt = client.create_toolbelt( + name="search-toolbelt", + tools=["exa_web_search"], + ) + + assert toolbelt.ref == "search-toolbelt@1" + + +def test_create_toolbelt_raises_generated_conflict(monkeypatch): + client = ActionGatewayClient(token="dummy") + response = FakeResponse( + 409, + { + "id": "conflict", + "message": "A toolbelt with this name already exists.", + }, + ) + + class Pipeline: + def run(self, request, **_kwargs): + response.request = request + return type("R", (), {"http_response": response})() + + monkeypatch.setattr(client._client, "_pipeline", Pipeline()) + + with pytest.raises(ResourceExistsError): + client.create_toolbelt( + name="search-toolbelt", + tools=["exa_web_search"], + ) + + +def test_toolbelt_rejects_unexpected_create_response(): + with pytest.raises(GatewayProtocolError, match="missing toolbelt reference"): + Toolbelt.from_response({"name": "search-toolbelt"}) + + +def test_create_toolbelt_rejects_string_tools(): + client = ActionGatewayClient(token="dummy") + with pytest.raises(TypeError, match="iterable of tool names"): + client.create_toolbelt(name="search-toolbelt", tools="exa_web_search") + + +def test_session_create_and_handle_tool_calls(monkeypatch): + responses = [ + FakeResponse(200, session_create_response()), + FakeResponse(200, jsonrpc_result({"tools": _META_TOOL_DEFINITIONS})), + FakeResponse(200, jsonrpc_result(call_result(structured={"ok": True}))), + ] + client = ActionGatewayClient(token="dummy") + + class Pipeline: + def __init__(self): + self.calls = [] + + def run(self, request, **_kwargs): + self.calls.append(request) + return type("R", (), {"http_response": responses.pop(0)})() + + monkeypatch.setattr(client._client, "_pipeline", Pipeline()) + + session = client.session.create(actor_id="user-123") + tools = session.tools() + assert tools[0]["function"]["name"] == "action_search" + assert "mcp/session/" in session.url + + messages = session.handle_tool_calls(chat_tool_response()) + assert messages[0]["role"] == "tool" + create_body = json.loads( + client._client._pipeline.calls[0].content + if isinstance(client._client._pipeline.calls[0].content, str) + else client._client._pipeline.calls[0].content.decode("utf-8") + ) + assert create_body["actor_id"] == "user-123" + assert "end_user_id" not in create_body + + +@pytest.mark.skipif(not _HAS_AIO, reason="aiohttp extra not installed") +def test_async_namespace_mirrors_sync(): + import pydo.action_gateway.aio as action_gateway_aio + from pydo.action_gateway.aio import ActionGatewayClient as AsyncActionGatewayClient + + assert hasattr(action_gateway_aio, "Client") + assert issubclass(action_gateway_aio.Client, pydo.aio.Client) + client = AsyncActionGatewayClient(token="dummy") + assert repr(client) == "" + assert client.sessions is client.gateway.sessions + assert client.session is client.sessions + assert client.sessions_api is not client.sessions + assert client.connections is not None + assert client.tools is not None + assert client.toolbelts is not None + assert client.users is not None + + +@pytest.mark.skipif(not _HAS_AIO, reason="aiohttp extra not installed") +def test_async_create_toolbelt_accepts_flat_api_response(monkeypatch): + from pydo.action_gateway.aio import ActionGatewayClient as AsyncActionGatewayClient + + client = AsyncActionGatewayClient(token="dummy") + create = AsyncMock( + return_value={ + "name": "search-toolbelt", + "version": "1", + "reference": "search-toolbelt@1", + "tools": ["exa_web_search"], + } + ) + monkeypatch.setattr(client.toolbelts, "create", create) + + async def scenario(): + return await client.create_toolbelt( + name="search-toolbelt", + tools=["exa_web_search"], + ) + + import asyncio + + toolbelt = asyncio.run(scenario()) + assert toolbelt.ref == "search-toolbelt@1" diff --git a/tests/gateway/test_async_gateway.py b/tests/gateway/test_async_gateway.py new file mode 100644 index 00000000..f665f6bc --- /dev/null +++ b/tests/gateway/test_async_gateway.py @@ -0,0 +1,245 @@ +# pylint: disable=missing-function-docstring,protected-access +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Async smoke tests for :mod:`pydo.aio.gateway`.""" + +from __future__ import annotations + +import asyncio +import json +from unittest.mock import AsyncMock, MagicMock + +import pytest +from azure.core.exceptions import HttpResponseError + +from pydo.aio.gateway import ( + AsyncGatewayResources, + AsyncMCPTransport, + AsyncSessionsOperations, +) +from pydo.custom_extensions import _BaseURLProxy +from pydo.gateway import ( + ACTOR_ID_HEADER, + SESSION_ID_HEADER, + ChatCompletionsProvider, + GatewayToolError, +) + +from .conftest import ( + TEST_GATEWAY_URL, + TEST_SESSION_URN, + AsyncFakeResponse, + jsonrpc_result, + chat_tool_response, + invoke_envelope, + make_async_gateway, + make_async_parent, + session_create_response, + tool_result, +) + + +def _run(coro): + return asyncio.run(coro) + + +def _sent_request(gateway, index=0): + pipeline = gateway._transport._client._original._pipeline + return pipeline.calls[index].request + + +def _sent_payload(gateway, index=0): + content = _sent_request(gateway, index).content + if isinstance(content, bytes): + content = content.decode("utf-8") + return json.loads(content) + + +def test_list_defaults_to_meta(): + gateway = make_async_gateway([]) + tools = _run(gateway.tools.list()) + assert [t.name for t in tools] == [ + "action_search", + "action_invoke", + "action_code", + ] + + +def test_invoke_and_invoke_one(): + envelope = invoke_envelope(output={"answer": 7}) + gateway = make_async_gateway([AsyncFakeResponse(200, envelope)]) + output = _run(gateway.tools.invoke_one("web_search", {"query": "do"})) + assert output.answer == 7 + request = _sent_request(gateway) + assert request.url.endswith("/tools/invoke") + assert request.headers[SESSION_ID_HEADER] == "test-session" + assert request.headers[ACTOR_ID_HEADER] == "actor-123" + assert _sent_payload(gateway)["tools"][0]["tool"] == "web_search" + + +def test_code_execute_failure_raises(): + gateway = make_async_gateway( + [ + AsyncFakeResponse( + 200, + tool_result(error={"class": "execution_failed", "message": "crash"}), + ) + ] + ) + with pytest.raises(GatewayToolError, match="crash"): + _run(gateway.code.execute("1/0")) + + +def test_http_error_reads_async_response_once(): + response = AsyncFakeResponse(400, "bad request") + gateway = make_async_gateway([response]) + with pytest.raises(HttpResponseError, match="bad request"): + _run(gateway.tools.list(include_all=True)) + assert response.read_calls == 1 + + +def test_mcp_transport_parses_sse_response(): + response = ( + "event: message\n" + 'data: {"jsonrpc":"2.0","id":1,"result":{"tools":' + '[{"name":"action_search"}]}}\n\n' + ) + parent = make_async_parent([AsyncFakeResponse(200, response)]) + proxy = _BaseURLProxy(parent._client, TEST_GATEWAY_URL) + gateway = AsyncGatewayResources( + parent, + gateway_endpoint=TEST_GATEWAY_URL, + transport=AsyncMCPTransport( + proxy, session_id=TEST_SESSION_URN, actor_id="actor-123" + ), + ) + assert _run(gateway.tools.list())[0].name == "action_search" + + +def test_session_create_delegates_to_generated_operation(): + parent = MagicMock() + parent.sessions.create = AsyncMock( + return_value=session_create_response(name="named") + ) + operations = AsyncSessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL) + + session = _run( + operations.create( + "actor-123", + name="named", + tools=["web_search@v1"], + config={"preloadTools": ["web_search@v1"]}, + ) + ) + + parent.sessions.create.assert_awaited_once() + body = parent.sessions.create.await_args.kwargs["body"] + assert body["actor_id"] == "actor-123" + assert body["name"] == "named" + assert body["policy"]["defaultAction"] == "ask" + assert body["config"]["preloadTools"] == ["web_search@v1"] + assert session.name == "named" + + +def test_session_create_uses_public_api_and_actor_header(): + parent = make_async_parent( + [ + AsyncFakeResponse(200, session_create_response()), + AsyncFakeResponse(200, jsonrpc_result({"tools": []})), + ] + ) + operations = AsyncSessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL) + + async def scenario(): + session = await operations.create( + "actor-123", + name="named", + tools=["web_search@v1"], + config={"preloadTools": ["web_search@v1"]}, + ) + await session.tools.list(include_all=True) + return session + + session = _run(scenario()) + create_request = parent._client._pipeline.calls[0].request + assert create_request.url.endswith("/v2/action-gateway/sessions") + assert json.loads(create_request.content) == { + "actor_id": "actor-123", + "name": "named", + "policy": {"defaultAction": "ask"}, + "tools": ["web_search@v1"], + "config": {"preloadTools": ["web_search@v1"]}, + } + tool_request = parent._client._pipeline.calls[1].request + assert tool_request.url == session.url + assert tool_request.headers[SESSION_ID_HEADER] == "test-session" + assert tool_request.headers[ACTOR_ID_HEADER] == "actor-123" + assert session.actor_id == "actor-123" + assert session.selected_tools == [] + + +def test_session_approve_posts_to_gateway(): + parent = make_async_parent( + [ + AsyncFakeResponse(200, session_create_response()), + AsyncFakeResponse(200, {"status": "approved"}), + ] + ) + operations = AsyncSessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL) + + async def scenario(): + session = await operations.create("actor-123") + result = await session.approve("approval-123") + return session, result + + session, result = _run(scenario()) + request = parent._client._pipeline.calls[1].request + assert request.url == f"{TEST_GATEWAY_URL}/approvals/approval-123" + assert request.headers[SESSION_ID_HEADER] == "test-session" + assert request.headers[ACTOR_ID_HEADER] == "actor-123" + assert json.loads(request.content) == {"decision": "approve"} + assert result.status == "approved" + assert session.actor_id == "actor-123" + + +def test_session_deny_posts_to_gateway(): + parent = make_async_parent( + [ + AsyncFakeResponse(200, session_create_response()), + AsyncFakeResponse(200, {"status": "denied"}), + ] + ) + operations = AsyncSessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL) + + async def scenario(): + session = await operations.create("actor-123") + return await session.deny("approval-123") + + result = _run(scenario()) + request = parent._client._pipeline.calls[1].request + assert json.loads(request.content) == {"decision": "deny"} + assert result.status == "denied" + + +def test_tools_callable_and_handle_tool_calls(): + envelope = invoke_envelope(output={"ok": True}) + gateway = make_async_gateway( + [AsyncFakeResponse(200, envelope)], + provider=ChatCompletionsProvider(), + ) + + async def scenario(): + tools = await gateway.tools() + messages = await gateway.handle_tool_calls(chat_tool_response()) + return tools, messages + + tools, messages = _run(scenario()) + assert [t["function"]["name"] for t in tools] == [ + "action_search", + "action_invoke", + "action_code", + ] + assert messages[0]["role"] == "tool" + assert json.loads(messages[0]["content"]) == {"ok": True} diff --git a/tests/gateway/test_code.py b/tests/gateway/test_code.py new file mode 100644 index 00000000..bc79a89c --- /dev/null +++ b/tests/gateway/test_code.py @@ -0,0 +1,70 @@ +# pylint: disable=missing-function-docstring,protected-access,duplicate-code +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Unit tests for :class:`pydo.gateway.custom_operations.CodeOperations`.""" + +from __future__ import annotations + +import pytest + +from pydo.gateway import GatewayToolError + +from .conftest import ( + FakeResponse, + make_gateway, + sent_payload, + sent_request, + tool_result, +) + + +def test_execute_happy_path(): + output = {"stdout": "hello\n", "stderr": "", "exit_code": 0} + gateway = make_gateway([FakeResponse(200, tool_result(output))]) + result = gateway.code.execute("print('hello')", thought="say hello") + + request = sent_request(gateway) + assert request.url.endswith("/code/execute") + payload = sent_payload(gateway) + assert payload == { + "code": "print('hello')", + "thought": "say hello", + } + + assert result.stdout == "hello\n" + assert result.exit_code == 0 + + +def test_execute_omits_empty_thought(): + output = {"stdout": "", "stderr": "", "exit_code": 0} + gateway = make_gateway([FakeResponse(200, tool_result(output))]) + gateway.code.execute("pass") + assert "thought" not in sent_payload(gateway) + + +def test_execute_rejects_empty_code(): + gateway = make_gateway([]) + with pytest.raises(ValueError, match="empty"): + gateway.code.execute(" ") + + +def test_execute_sandbox_failure_raises(): + gateway = make_gateway( + [ + FakeResponse( + 200, + tool_result( + error={ + "class": "execution_failed", + "message": "sandbox crashed", + "retriable": False, + } + ), + ) + ] + ) + with pytest.raises(GatewayToolError, match="sandbox crashed") as excinfo: + gateway.code.execute("1/0") + assert excinfo.value.error_class == "execution_failed" diff --git a/tests/gateway/test_providers.py b/tests/gateway/test_providers.py new file mode 100644 index 00000000..7581c228 --- /dev/null +++ b/tests/gateway/test_providers.py @@ -0,0 +1,461 @@ +# pylint: disable=missing-function-docstring,protected-access +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Unit tests for :mod:`pydo.gateway.providers` and ``handle_tool_calls``.""" + +from __future__ import annotations + +import json + +import pytest + +from pydo.custom_extensions import _wrap +from pydo.gateway import ( + ChatCompletionsProvider, + MessagesProvider, + ResponsesProvider, + normalize_invoke_arguments, + simplify_messages_input_schema, +) + +from .conftest import ( + FakeResponse, + chat_tool_response, + invoke_envelope, + make_gateway, + sent_payload, + sent_request, + tool_result, +) + +_CATALOG = [ + { + "name": "web_search", + "title": "Web Search", + "description": "Search the public web", + "inputSchema": { + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + } +] + +_META_TOOLS = [ + { + "name": "action_search", + "description": "Find tools", + "inputSchema": {"type": "object"}, + }, + { + "name": "action_invoke", + "description": "Run tools", + "inputSchema": {"type": "object"}, + }, + { + "name": "action_code", + "description": "Run code", + "inputSchema": {"type": "object"}, + }, +] + + +# -- wrap_tools --------------------------------------------------------------- + + +def test_chat_completions_wrap_tools(): + tools = ChatCompletionsProvider().wrap_tools(_CATALOG) + assert tools == [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "Search the public web", + "parameters": _CATALOG[0]["inputSchema"], + }, + } + ] + + +def test_messages_wrap_tools(): + tools = MessagesProvider().wrap_tools(_CATALOG) + assert tools == [ + { + "name": "web_search", + "description": "Search the public web", + "input_schema": _CATALOG[0]["inputSchema"], + } + ] + + +def test_messages_wrap_tools_preserves_meta_tool_names(): + tools = MessagesProvider().wrap_tools(_META_TOOLS) + assert [tool["name"] for tool in tools] == [ + "action_search", + "action_invoke", + "action_code", + ] + + +def test_simplify_messages_input_schema_strips_top_level_any_of(): + schema = { + "type": "object", + "properties": { + "code": {"type": "string"}, + "code_to_execute": {"type": "string"}, + }, + "anyOf": [ + {"required": ["code"]}, + {"required": ["code_to_execute"]}, + ], + } + simplified = simplify_messages_input_schema(schema) + assert "anyOf" not in simplified + assert simplified["properties"]["code"]["type"] == "string" + + +def test_chat_completions_wrap_tools_strips_any_of_from_code_meta_tool(): + tools = ChatCompletionsProvider().wrap_tools( + [ + { + "name": "action_code", + "description": "Run Python", + "inputSchema": { + "type": "object", + "properties": {"code": {"type": "string"}}, + "anyOf": [{"required": ["code"]}], + }, + } + ] + ) + assert tools[0]["function"]["name"] == "action_code" + assert "anyOf" not in tools[0]["function"]["parameters"] + + +def test_responses_wrap_tools_preserves_meta_tool_names(): + tools = ResponsesProvider().wrap_tools(_META_TOOLS) + assert [tool["name"] for tool in tools] == [ + "action_search", + "action_invoke", + "action_code", + ] + + +def test_responses_wrap_tools(): + tools = ResponsesProvider().wrap_tools(_CATALOG) + assert tools == [ + { + "type": "function", + "name": "web_search", + "description": "Search the public web", + "parameters": _CATALOG[0]["inputSchema"], + } + ] + + +def test_wrap_tools_falls_back_to_title_and_empty_schema(): + tools = ChatCompletionsProvider().wrap_tools([{"name": "t", "title": "T"}]) + function = tools[0]["function"] + assert function["description"] == "T" + assert function["parameters"] == {"type": "object", "properties": {}} + + +# -- extract_tool_calls ------------------------------------------------------- + + +def _chat_response(arguments='{"query": "do"}'): + return chat_tool_response(arguments=arguments) + + +def _messages_response(): + return { + "content": [ + {"type": "text", "text": "let me check"}, + { + "type": "tool_use", + "id": "toolu_1", + "name": "web_search", + "input": {"query": "do"}, + }, + ] + } + + +def _responses_response(): + return { + "output": [ + { + "type": "function_call", + "call_id": "fc_1", + "name": "web_search", + "arguments": '{"query": "do"}', + } + ] + } + + +@pytest.mark.parametrize("wrap", [lambda x: x, _wrap], ids=["dict", "DotDict"]) +def test_chat_completions_extract(wrap): + calls = ChatCompletionsProvider().extract_tool_calls(wrap(_chat_response())) + assert len(calls) == 1 + assert calls[0].call_id == "call_1" + assert calls[0].name == "web_search" + assert calls[0].arguments == {"query": "do"} + + +@pytest.mark.parametrize("wrap", [lambda x: x, _wrap], ids=["dict", "DotDict"]) +def test_messages_extract(wrap): + calls = MessagesProvider().extract_tool_calls(wrap(_messages_response())) + assert len(calls) == 1 + assert calls[0].call_id == "toolu_1" + assert calls[0].arguments == {"query": "do"} + + +@pytest.mark.parametrize("wrap", [lambda x: x, _wrap], ids=["dict", "DotDict"]) +def test_responses_extract(wrap): + calls = ResponsesProvider().extract_tool_calls(wrap(_responses_response())) + assert len(calls) == 1 + assert calls[0].call_id == "fc_1" + assert calls[0].arguments == {"query": "do"} + + +def test_extract_returns_empty_without_tool_calls(): + assert not ChatCompletionsProvider().extract_tool_calls( + {"choices": [{"message": {"content": "hi"}}]} + ) + assert not MessagesProvider().extract_tool_calls({"content": []}) + assert not ResponsesProvider().extract_tool_calls({"output": []}) + + +# -- format_tool_results ------------------------------------------------------ + + +def test_format_results_per_provider(): + provider = ChatCompletionsProvider() + calls = provider.extract_tool_calls(_chat_response()) + messages = provider.format_tool_results(calls, [{"answer": 1}]) + assert messages == [ + {"role": "tool", "tool_call_id": "call_1", "content": '{"answer": 1}'} + ] + + provider = MessagesProvider() + calls = provider.extract_tool_calls(_messages_response()) + messages = provider.format_tool_results(calls, [{"answer": 1}]) + assert messages[0]["role"] == "user" + assert messages[0]["content"][0]["type"] == "tool_result" + assert messages[0]["content"][0]["tool_use_id"] == "toolu_1" + + provider = ResponsesProvider() + calls = provider.extract_tool_calls(_responses_response()) + items = provider.format_tool_results(calls, [{"answer": 1}]) + assert items == [ + { + "type": "function_call_output", + "call_id": "fc_1", + "output": '{"answer": 1}', + } + ] + + +# -- tools() callable --------------------------------------------------------- + + +def test_tools_callable_wraps_meta_tools_by_default(): + gateway = make_gateway([], provider=ChatCompletionsProvider()) + tools = gateway.tools() + assert [t["function"]["name"] for t in tools] == [ + "action_search", + "action_invoke", + "action_code", + ] + + +def test_tools_callable_wraps_meta_tools_for_messages(): + gateway = make_gateway([], provider=MessagesProvider()) + tools = gateway.tools() + assert [t["name"] for t in tools] == [ + "action_search", + "action_invoke", + "action_code", + ] + + +def test_tools_callable_include_all_wraps_catalog(): + gateway = make_gateway( + [FakeResponse(200, {"tools": _CATALOG})], + provider=ChatCompletionsProvider(), + ) + tools = gateway.tools(include_all=True) + assert tools[0]["function"]["name"] == "web_search" + + +def test_tools_callable_names_filter_and_missing(): + gateway = make_gateway( + [ + FakeResponse(200, {"tools": _CATALOG}), + FakeResponse(200, {"tools": _CATALOG}), + ], + provider=ChatCompletionsProvider(), + ) + tools = gateway.tools(names=["web_search"]) + assert len(tools) == 1 + with pytest.raises(LookupError, match="nope"): + gateway.tools(names=["nope"]) + + +def test_tools_callable_via_search(): + search_payload = { + "results": [ + { + "index": 1, + "use_case": "web", + "results": [_CATALOG[0], _CATALOG[0]], # dupes collapse + } + ] + } + gateway = make_gateway( + [FakeResponse(200, tool_result(search_payload))], + provider=ChatCompletionsProvider(), + ) + tools = gateway.tools(search="search the web", limit=2) + assert len(tools) == 1 + assert tools[0]["function"]["name"] == "web_search" + + +# -- handle_tool_calls -------------------------------------------------------- + + +def test_handle_tool_calls_batches_concrete_tools(): + envelope = invoke_envelope(output={"answer": 42}) + gateway = make_gateway( + [FakeResponse(200, envelope)], + provider=ChatCompletionsProvider(), + ) + messages = gateway.handle_tool_calls(_chat_response(), rationale="why not") + + assert sent_request(gateway).url.endswith("/tools/invoke") + payload = sent_payload(gateway) + assert payload["rationale"] == "why not" + assert payload["tools"] == [{"tool": "web_search", "arguments": {"query": "do"}}] + + assert messages[0]["role"] == "tool" + assert messages[0]["tool_call_id"] == "call_1" + assert json.loads(messages[0]["content"]) == {"answer": 42} + + +def test_normalize_invoke_arguments_accepts_chat_function_shape(): + arguments = normalize_invoke_arguments( + { + "tools": [ + { + "type": "function", + "function": { + "name": "web_search", + "arguments": '{"query": "digitalocean news"}', + }, + } + ] + } + ) + assert arguments["tools"] == [ + {"tool": "web_search", "arguments": {"query": "digitalocean news"}} + ] + + +def test_handle_tool_calls_normalizes_action_invoke_payload(): + envelope = invoke_envelope(output={"answer": 1}) + gateway = make_gateway( + [FakeResponse(200, envelope)], + provider=ChatCompletionsProvider(), + ) + response = { + "choices": [ + { + "message": { + "tool_calls": [ + { + "id": "call_invoke", + "function": { + "name": "action_invoke", + "arguments": json.dumps( + { + "tools": [ + { + "function": { + "name": "web_search", + "arguments": { + "query": "digitalocean" + }, + } + } + ] + } + ), + }, + } + ] + } + } + ] + } + messages = gateway.handle_tool_calls(response) + assert sent_request(gateway).url.endswith("/tools/invoke") + payload = sent_payload(gateway) + assert payload["tools"] == [ + {"tool": "web_search", "arguments": {"query": "digitalocean"}} + ] + assert json.loads(messages[0]["content"]) == envelope + + +def test_handle_tool_calls_routes_meta_tools_directly(): + gateway = make_gateway( + [FakeResponse(200, tool_result({"results": []}))], + provider=ChatCompletionsProvider(), + ) + response = { + "choices": [ + { + "message": { + "tool_calls": [ + { + "id": "call_meta", + "function": { + "name": "action_search", + "arguments": '{"queries": [{"use_case": "x"}]}', + }, + } + ] + } + } + ] + } + messages = gateway.handle_tool_calls(response) + assert sent_request(gateway).url.endswith("/tools/search") + assert json.loads(messages[0]["content"]) == {"results": []} + + +def test_handle_tool_calls_surfaces_failures_as_content(): + envelope = invoke_envelope( + error={"class": "timeout", "message": "too slow"}, + ) + gateway = make_gateway( + [FakeResponse(200, envelope)], + provider=ChatCompletionsProvider(), + ) + messages = gateway.handle_tool_calls(_chat_response()) + content = json.loads(messages[0]["content"]) + assert content["error"]["class"] == "timeout" + + +def test_handle_tool_calls_no_calls_returns_empty(): + gateway = make_gateway([], provider=ChatCompletionsProvider()) + assert gateway.handle_tool_calls({"choices": [{"message": {}}]}) == [] + + +def test_tools_callable_requires_provider(): + gateway = make_gateway([], provider=None) + gateway.tools._provider = None + with pytest.raises(RuntimeError, match="provider"): + gateway.tools() diff --git a/tests/gateway/test_session.py b/tests/gateway/test_session.py new file mode 100644 index 00000000..21b091a1 --- /dev/null +++ b/tests/gateway/test_session.py @@ -0,0 +1,260 @@ +# pylint: disable=missing-function-docstring,protected-access +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Unit tests for Action Gateway sessions.""" + +from __future__ import annotations + +import json +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest +from azure.core.exceptions import ResourceNotFoundError + +from pydo.gateway import ( + ACTOR_ID_HEADER, + SESSION_ID_HEADER, + SessionsOperations, + normalize_permissions, +) +from pydo.gateway.transport import _META_TOOL_DEFINITIONS + +from .conftest import ( + TEST_GATEWAY_URL, + TEST_SESSION_URN, + FakeResponse, + call_result, + chat_tool_response, + jsonrpc_result, + make_parent, + session_create_response, +) + + +def test_normalize_permissions_defaults_to_ask(): + assert normalize_permissions(None) == {"defaultAction": "ask"} + + +def test_normalize_permissions_accepts_snake_case(): + policy = normalize_permissions( + { + "default_action": "ask", + "rules": [ + {"tool": "toolbelt:read-only@1.2.3", "action": "allow"}, + {"tool": "gmail", "action": "deny"}, + ], + } + ) + assert policy == { + "defaultAction": "ask", + "rules": [ + {"tool": "toolbelt:read-only@1.2.3", "action": "allow"}, + {"tool": "gmail", "action": "deny"}, + ], + } + + +def test_normalize_permissions_requires_tool(): + with pytest.raises(ValueError, match="requires tool"): + normalize_permissions({"rules": [{"action": "allow"}]}) + + +def test_normalize_permissions_rejects_legacy_toolbelt_key(): + with pytest.raises(ValueError, match="toolbelt permissions are no longer"): + normalize_permissions({"rules": [{"toolbelt": "read-only@1.2.3"}]}) + + +def test_sessions_create_requires_actor_id(): + ops = SessionsOperations(make_parent([]), gateway_endpoint=TEST_GATEWAY_URL) + with pytest.raises(ValueError, match="actor_id"): + ops.create("") + + +def test_sessions_create_delegates_to_generated_operation(): + parent = MagicMock() + parent.sessions.create.return_value = session_create_response(name="named") + operations = SessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL) + + session = operations.create( + "actor-123", + name="named", + tools=["web_search@v1"], + config={"preloadTools": ["web_search@v1"]}, + ) + + parent.sessions.create.assert_called_once() + body = parent.sessions.create.call_args.kwargs["body"] + assert body["actor_id"] == "actor-123" + assert body["name"] == "named" + assert body["tools"] == ["web_search@v1"] + assert body["config"] == {"preloadTools": ["web_search@v1"]} + assert session.name == "named" + + +def test_sessions_create_404_uses_generated_error_mapping(): + response = FakeResponse( + 404, + {"id": "not_found", "message": "Your request could not be routed."}, + ) + response.request = SimpleNamespace( + url="https://api.digitalocean.com/v2/action-gateway/sessions" + ) + ops = SessionsOperations(make_parent([response]), gateway_endpoint=TEST_GATEWAY_URL) + with pytest.raises(ResourceNotFoundError): + ops.create("user-123") + + +def test_sessions_create_posts_to_do_api_and_binds_returned_mcp_url(): + parent = make_parent( + [ + FakeResponse(200, session_create_response()), + FakeResponse(200, jsonrpc_result({"tools": _META_TOOL_DEFINITIONS})), + FakeResponse(200, jsonrpc_result(call_result(structured={"ok": True}))), + ] + ) + ops = SessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL) + session = ops.create("user-123") + + create_req = parent._client._pipeline.calls[0].request + assert create_req.method == "POST" + assert create_req.url.endswith("/v2/action-gateway/sessions") + body = json.loads(create_req.content) + assert body["actor_id"] == "user-123" + assert "end_user_id" not in body + assert body["policy"] == {"defaultAction": "ask"} + assert body["name"].startswith("pydo-session-") + + assert session.session_urn == TEST_SESSION_URN + assert session.actor_id == "user-123" + assert session.url == "https://actions.do-ai-test.run/mcp/session/test-session" + + tools = session.tools() + assert [t["function"]["name"] for t in tools][:1] == ["action_search"] + + messages = session.handle_tool_calls(chat_tool_response()) + invoke_req = parent._client._pipeline.calls[2].request + assert invoke_req.url == session.url + assert invoke_req.headers[SESSION_ID_HEADER] == "test-session" + assert invoke_req.headers[ACTOR_ID_HEADER] == "user-123" + assert json.loads(invoke_req.content)["method"] == "tools/call" + assert messages[0]["role"] == "tool" + + +def test_sessions_create_with_permissions_and_name(): + parent = make_parent( + [ + FakeResponse( + 200, + session_create_response(name="named"), + ) + ] + ) + ops = SessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL) + session = ops.create( + "u1", + name="named", + permissions={ + "default_action": "deny", + "rules": [{"tool": "web_search", "action": "allow"}], + }, + ) + body = json.loads(parent._client._pipeline.calls[0].request.content) + assert body["name"] == "named" + assert body["policy"] == { + "defaultAction": "deny", + "rules": [{"tool": "web_search", "action": "allow"}], + } + assert session.name == "named" + + +def test_sessions_create_sends_tool_selection_and_config(): + parent = make_parent([FakeResponse(200, session_create_response())]) + session = SessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL).create( + "u1", + tools=["web_search@v1", "toolbelt:read-only@2"], + config={"preloadTools": ["web_search@v1"]}, + ) + + body = json.loads(parent._client._pipeline.calls[0].request.content) + assert body["tools"] == ["web_search@v1", "toolbelt:read-only@2"] + assert body["config"] == {"preloadTools": ["web_search@v1"]} + assert not session.selected_tools + + +def test_session_approve_posts_to_gateway(): + parent = make_parent( + [ + FakeResponse(200, session_create_response()), + FakeResponse(200, {"status": "approved"}), + ] + ) + session = SessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL).create( + "user-123" + ) + + result = session.approve("approval-123") + + request = parent._client._pipeline.calls[1].request + assert request.url == f"{TEST_GATEWAY_URL}/approvals/approval-123" + assert request.headers[SESSION_ID_HEADER] == "test-session" + assert request.headers[ACTOR_ID_HEADER] == "user-123" + assert json.loads(request.content) == {"decision": "approve"} + assert result.status == "approved" + + +def test_session_deny_posts_to_gateway(): + parent = make_parent( + [ + FakeResponse(200, session_create_response()), + FakeResponse(200, {"status": "denied"}), + ] + ) + session = SessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL).create( + "user-123" + ) + + result = session.deny("approval-123") + + request = parent._client._pipeline.calls[1].request + assert json.loads(request.content) == {"decision": "deny"} + assert result.status == "denied" + + +def test_handle_tool_calls_preserves_approval_metadata(): + parent = make_parent( + [ + FakeResponse(200, session_create_response()), + FakeResponse( + 200, + jsonrpc_result( + call_result( + structured={ + "results": [ + { + "tool": "exa_web_search", + "result": { + "status": "failed", + "error": {"message": "approval required"}, + "_meta": { + "status": "requires_approval", + "approval_id": "approval-123", + }, + }, + } + ] + }, + ) + ), + ), + ] + ) + session = SessionsOperations(parent, gateway_endpoint=TEST_GATEWAY_URL).create( + "user-123" + ) + + messages = session.handle_tool_calls(chat_tool_response(name="exa_web_search")) + content = json.loads(messages[0]["content"]) + assert content["_meta"]["approval_id"] == "approval-123" diff --git a/tests/gateway/test_tools.py b/tests/gateway/test_tools.py new file mode 100644 index 00000000..c1e17ebe --- /dev/null +++ b/tests/gateway/test_tools.py @@ -0,0 +1,167 @@ +# pylint: disable=missing-function-docstring,protected-access +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Unit tests for :class:`pydo.gateway.custom_operations.ToolsOperations`.""" + +from __future__ import annotations + +import pytest + +from pydo.gateway import GatewayToolError + +from .conftest import ( + FakeResponse, + invoke_envelope, + make_gateway, + sent_payload, + sent_request, + tool_result, +) + +_SEARCH_PAYLOAD = { + "results": [ + { + "index": 1, + "use_case": "search the web", + "results": [ + { + "name": "web_search", + "title": "Web Search", + "description": "Search the public web", + "inputSchema": {"type": "object"}, + "score": 12.3, + } + ], + } + ] +} + + +# -- search ------------------------------------------------------------------ + + +def test_search_accepts_single_string(): + gateway = make_gateway([FakeResponse(200, tool_result(_SEARCH_PAYLOAD))]) + result = gateway.tools.search("search the web") + + assert sent_request(gateway).url.endswith("/tools/search") + assert sent_payload(gateway)["queries"] == [{"use_case": "search the web"}] + assert result.results[0].results[0].name == "web_search" + + +def test_search_accepts_dicts_and_filters(): + gateway = make_gateway([FakeResponse(200, tool_result(_SEARCH_PAYLOAD))]) + gateway.tools.search( + [ + {"use_case": "find stuff", "known_fields": "site:example.com"}, + "another use case", + ], + providers=["exa"], + tags=["web"], + limit=3, + ) + arguments = sent_payload(gateway) + assert arguments["queries"] == [ + {"use_case": "find stuff", "known_fields": "site:example.com"}, + {"use_case": "another use case"}, + ] + assert arguments["providers"] == ["exa"] + assert arguments["tags"] == ["web"] + assert arguments["limit"] == 3 + + +def test_search_rejects_missing_use_case_and_bad_counts(): + gateway = make_gateway([]) + with pytest.raises(ValueError, match="use_case"): + gateway.tools.search([{"known_fields": "x"}]) + with pytest.raises(ValueError, match="between 1 and 5"): + gateway.tools.search(["a", "b", "c", "d", "e", "f"]) + with pytest.raises(TypeError): + gateway.tools.search([42]) + + +# -- invoke ------------------------------------------------------------------ + + +def test_invoke_shapes_arguments_and_returns_envelope(): + envelope = invoke_envelope( + [ + { + "index": 0, + "tool": "web_search", + "result": {"status": "succeeded", "output": {"answer": 1}}, + }, + { + "index": 1, + "tool": "missing_tool", + "result": { + "status": "failed", + "error": {"class": "invalid_argument", "message": "unknown tool"}, + }, + }, + ] + ) + gateway = make_gateway([FakeResponse(200, envelope)]) + result = gateway.tools.invoke( + [ + {"tool": "web_search", "arguments": {"query": "do"}}, + {"tool_slug": "missing_tool"}, + ], + rationale="testing", + ) + + assert sent_request(gateway).url.endswith("/tools/invoke") + payload = sent_payload(gateway) + assert payload["rationale"] == "testing" + assert payload["tools"] == [ + {"tool": "web_search", "arguments": {"query": "do"}}, + {"tool": "missing_tool", "arguments": {}}, + ] + + assert result.error_count == 1 + assert result.results[1].result.status == "failed" + + +def test_invoke_validates_counts_and_entries(): + gateway = make_gateway([]) + with pytest.raises(ValueError, match="between 1 and 10"): + gateway.tools.invoke([]) + with pytest.raises(ValueError, match="between 1 and 10"): + gateway.tools.invoke([{"tool": f"t{i}", "arguments": {}} for i in range(11)]) + with pytest.raises(ValueError, match="'tool' name"): + gateway.tools.invoke([{"arguments": {}}]) + with pytest.raises(TypeError): + gateway.tools.invoke(["web_search"]) + + +def test_invoke_one_returns_output(): + envelope = invoke_envelope( + [ + { + "index": 0, + "tool": "web_search", + "result": {"status": "succeeded", "output": {"answer": 42}}, + } + ] + ) + gateway = make_gateway([FakeResponse(200, envelope)]) + output = gateway.tools.invoke_one("web_search", {"query": "do"}) + assert output.answer == 42 + + +def test_invoke_one_raises_on_failure(): + envelope = invoke_envelope( + error={ + "class": "upstream_error", + "message": "exa is down", + "retriable": True, + }, + invocation_id="inv_9", + ) + gateway = make_gateway([FakeResponse(200, envelope)]) + with pytest.raises(GatewayToolError, match="exa is down") as excinfo: + gateway.tools.invoke_one("web_search", {"query": "do"}) + assert excinfo.value.error_class == "upstream_error" + assert excinfo.value.retriable is True diff --git a/tests/gateway/test_transport.py b/tests/gateway/test_transport.py new file mode 100644 index 00000000..b503ad0f --- /dev/null +++ b/tests/gateway/test_transport.py @@ -0,0 +1,274 @@ +# pylint: disable=missing-function-docstring,protected-access,duplicate-code +# ------------------------------------ +# Copyright (c) DigitalOcean. +# Licensed under the Apache-2.0 License. +# ------------------------------------ +"""Unit tests for :mod:`pydo.gateway.transport` (REST + MCP wire layers).""" + +from __future__ import annotations + +import pytest +from azure.core.exceptions import ( + ClientAuthenticationError, + HttpResponseError, + ResourceNotFoundError, +) + +from pydo.custom_extensions import _BaseURLProxy +from pydo.gateway import ( + ACTOR_ID_HEADER, + GatewayProtocolError, + GatewayResources, + GatewayToolError, + MCPTransport, + RecoveryHint, + SESSION_ID_HEADER, + ToolErrorClass, +) +from pydo.gateway.transport import _META_TOOL_DEFINITIONS, session_mcp_url + +from .conftest import ( + TEST_GATEWAY_URL, + TEST_SESSION_URN, + FakeResponse, + call_result, + invoke_envelope, + jsonrpc_error, + jsonrpc_result, + make_gateway, + make_parent, + sent_payload, + sent_request, + tool_result, +) + + +def test_list_meta_tools_is_local_no_network(): + gateway = make_gateway([]) + tools = gateway.tools.list() + assert [t.name for t in tools] == [ + "action_search", + "action_invoke", + "action_code", + ] + assert pipeline_calls(gateway) == 0 + + +def test_gateway_constants_match_server_contract(): + assert ToolErrorClass.NOT_FOUND == "not_found" + assert ToolErrorClass.CANCELED == "canceled" + assert RecoveryHint.REFRESH_AUTH == "refresh_auth" + assert RecoveryHint.RETRY_LATER == "retry_later" + assert RecoveryHint.NARROW_OUTPUT == "narrow_output" + assert RecoveryHint.CONTACT_SUPPORT == "contact_support" + + +def test_meta_schemas_match_server_constraints(): + definitions = {tool["name"]: tool for tool in _META_TOOL_DEFINITIONS} + invoke_schema = definitions["action_invoke"]["inputSchema"] + assert invoke_schema["properties"]["rationale"]["maxLength"] == 512 + assert invoke_schema["properties"]["tools"]["items"]["anyOf"] == [ + {"required": ["tool"]}, + {"required": ["tool_slug"]}, + ] + assert definitions["action_code"]["inputSchema"]["anyOf"] == [ + {"required": ["code"]}, + {"required": ["code_to_execute"]}, + ] + + +def pipeline_calls(gateway) -> int: + return len(gateway._transport._client._original._pipeline.calls) + + +def test_list_tools_include_all_hits_rest_catalog(): + gateway = make_gateway([FakeResponse(200, {"tools": [{"name": "web_search"}]})]) + tools = gateway.tools.list(include_all=True) + request = sent_request(gateway) + assert request.method == "GET" + assert request.url.endswith("/tools") + assert request.headers[SESSION_ID_HEADER] == "test-session" + assert request.headers[ACTOR_ID_HEADER] == "actor-123" + assert tools[0].name == "web_search" + + +def test_search_posts_rest_and_unwraps_tool_result(): + gateway = make_gateway( + [ + FakeResponse( + 200, + tool_result({"results": [{"use_case": "x", "results": []}]}), + ) + ] + ) + result = gateway.tools.search("search the web") + request = sent_request(gateway) + assert request.method == "POST" + assert request.url.endswith("/tools/search") + assert request.headers[SESSION_ID_HEADER] == "test-session" + assert request.headers[ACTOR_ID_HEADER] == "actor-123" + payload = sent_payload(gateway) + assert payload["queries"] == [{"use_case": "search the web"}] + assert result.results[0].use_case == "x" + + +def test_invoke_posts_rest_envelope(): + gateway = make_gateway([FakeResponse(200, invoke_envelope(output={"ok": True}))]) + result = gateway.tools.invoke( + [{"tool": "web_search", "arguments": {"query": "do"}}] + ) + assert sent_request(gateway).url.endswith("/tools/invoke") + assert result.success_count == 1 + + +def test_code_execute_posts_rest(): + gateway = make_gateway( + [FakeResponse(200, tool_result({"stdout": "hi", "exit_code": 0}))] + ) + result = gateway.code.execute("print('hi')") + assert sent_request(gateway).url.endswith("/code/execute") + assert result.stdout == "hi" + assert result.exit_code == 0 + + +def test_concrete_call_routes_through_invoke(): + gateway = make_gateway([FakeResponse(200, invoke_envelope(output={"answer": 42}))]) + result = gateway.tools.call("web_search", {"query": "x"}) + assert sent_request(gateway).url.endswith("/tools/invoke") + assert result.answer == 42 + + +def test_failed_tool_result_raises(): + gateway = make_gateway( + [ + FakeResponse( + 200, + tool_result( + error={ + "class": "rate_limited", + "message": "slow down", + "retriable": True, + "recovery_hint": "retry_later", + } + ), + ) + ] + ) + with pytest.raises(GatewayToolError) as excinfo: + gateway.code.execute("1") + err = excinfo.value + assert err.error_class == "rate_limited" + assert err.retriable is True + assert err.recovery_hint == "retry_later" + + +def test_non_json_body_raises_protocol_error(): + gateway = make_gateway([FakeResponse(200, "nope")]) + with pytest.raises(GatewayProtocolError, match="non-JSON"): + gateway.tools.list(include_all=True) + + +@pytest.mark.parametrize( + "status,exc", + [ + (401, ClientAuthenticationError), + (404, ResourceNotFoundError), + (400, HttpResponseError), + (412, HttpResponseError), + ], +) +def test_http_errors_are_mapped(status, exc): + gateway = make_gateway([FakeResponse(status, {"type": "invalid_request"})]) + with pytest.raises(exc): + gateway.tools.list(include_all=True) + + +def test_412_message_mentions_release_gate(): + gateway = make_gateway([FakeResponse(412, "nope")]) + with pytest.raises(HttpResponseError, match="Action Infra release"): + gateway.tools.list(include_all=True) + + +def test_session_mcp_url_uses_uuid_from_urn(): + session_uuid = "3a12f86f-ef5c-41e3-a951-2b7a933e151d" + url = session_mcp_url( + TEST_GATEWAY_URL, + f"do:managed_agents_session:{session_uuid}", + ) + assert url == f"https://actions.do-ai-test.run/mcp/session/{session_uuid}" + + +def test_mcp_transport_still_works_with_session_header(): + parent = make_parent( + [FakeResponse(200, jsonrpc_result({"tools": [{"name": "action_search"}]}))] + ) + proxy = _BaseURLProxy(parent._client, TEST_GATEWAY_URL) + transport = MCPTransport(proxy, session_id=TEST_SESSION_URN, actor_id="actor-123") + gateway = GatewayResources( + parent, + gateway_endpoint=TEST_GATEWAY_URL, + transport=transport, + ) + tools = gateway.tools.list() + request = sent_request(gateway) + assert request.url.endswith("/mcp/meta") + assert request.headers[SESSION_ID_HEADER] == "test-session" + assert request.headers[ACTOR_ID_HEADER] == "actor-123" + assert tools[0].name == "action_search" + + +def test_mcp_transport_parses_sse_response(): + response = ( + ": heartbeat\n\n" + "event: message\n" + 'data: {"jsonrpc":"2.0","method":"notifications/progress"}\n\n' + "event: message\n" + 'data: {"jsonrpc":"2.0","id":1,"result":{"tools":' + '[{"name":"action_search"}]}}\n\n' + "data: [DONE]\n\n" + ) + parent = make_parent([FakeResponse(200, response)]) + proxy = _BaseURLProxy(parent._client, TEST_GATEWAY_URL) + gateway = GatewayResources( + parent, + gateway_endpoint=TEST_GATEWAY_URL, + transport=MCPTransport( + proxy, session_id=TEST_SESSION_URN, actor_id="actor-123" + ), + ) + assert gateway.tools.list()[0].name == "action_search" + + +def test_mcp_jsonrpc_error_raises_protocol_error(): + parent = make_parent([FakeResponse(200, jsonrpc_error(-32601, "method not found"))]) + proxy = _BaseURLProxy(parent._client, TEST_GATEWAY_URL) + transport = MCPTransport(proxy, session_id=TEST_SESSION_URN, actor_id="actor-123") + gateway = GatewayResources( + parent, gateway_endpoint=TEST_GATEWAY_URL, transport=transport + ) + with pytest.raises(GatewayProtocolError) as excinfo: + gateway.tools.list() + assert excinfo.value.code == -32601 + + +def test_mcp_is_error_raises_gateway_tool_error(): + structured = { + "invocation_id": "inv_1", + "error": { + "class": "rate_limited", + "message": "slow down", + "retriable": True, + "recovery_hint": "retry_later", + }, + } + parent = make_parent( + [FakeResponse(200, jsonrpc_result(call_result(structured, is_error=True)))] + ) + proxy = _BaseURLProxy(parent._client, TEST_GATEWAY_URL) + transport = MCPTransport(proxy, session_id=TEST_SESSION_URN, actor_id="actor-123") + gateway = GatewayResources( + parent, gateway_endpoint=TEST_GATEWAY_URL, transport=transport + ) + with pytest.raises(GatewayToolError) as excinfo: + gateway.tools.call("web_search", {"query": "x"}) + assert excinfo.value.invocation_id == "inv_1" diff --git a/tests/integration/test_droplets.py b/tests/integration/test_droplets.py index 9b490027..d3bab0b6 100644 --- a/tests/integration/test_droplets.py +++ b/tests/integration/test_droplets.py @@ -41,7 +41,6 @@ def test_droplet_attach_volume(integration_client: Client, public_key: bytes): } with shared.with_test_volume(integration_client, **volume_req) as volume: - vol_attach_resp = integration_client.volume_actions.post_by_id( volume["volume"]["id"], {"type": "attach", "droplet_id": droplet["droplet"]["id"]},