|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from copy import deepcopy |
| 4 | +from pathlib import Path |
| 5 | + |
| 6 | +from typing_extensions import Any, NotRequired, TypedDict, TypeVar |
| 7 | + |
| 8 | +here = Path(__file__).parent |
| 9 | +T = TypeVar("T") |
| 10 | + |
| 11 | + |
| 12 | +def find_value(root, path): |
| 13 | + """ |
| 14 | + Find a value in an object graph. |
| 15 | +
|
| 16 | + This function is used to follow the specified path through the object graph at root |
| 17 | + and return the item in the graph, if any, that the path refers to. |
| 18 | +
|
| 19 | + :param root: the root of the object graph to traverse. |
| 20 | + :param path: the path through the graph to take. |
| 21 | + :return: the resulting value or None. |
| 22 | + """ |
| 23 | + if isinstance(path, str): |
| 24 | + path = path.split("/") |
| 25 | + parent = root |
| 26 | + for part in path: |
| 27 | + if part in parent: |
| 28 | + parent = parent[part] |
| 29 | + else: |
| 30 | + return None |
| 31 | + return parent |
| 32 | + |
| 33 | + |
| 34 | +def expand_refs( |
| 35 | + document, obj |
| 36 | +) -> Any: # Use `Any` for return type to hack around typing requirement |
| 37 | + """ |
| 38 | + Expands `ref`s in the given object. |
| 39 | +
|
| 40 | + Returns an object semantically equivalent to the original but with references expanded. |
| 41 | +
|
| 42 | + Parameters |
| 43 | + ---------- |
| 44 | + document |
| 45 | + the master swagger document containing the responses and definitions. |
| 46 | + obj |
| 47 | + is either a normal swagger object, a ref object, or a swagger object with a schema. |
| 48 | + """ |
| 49 | + if isinstance(obj, list): |
| 50 | + return [expand_refs(document, item) for item in obj] |
| 51 | + elif isinstance(obj, dict): |
| 52 | + if "$ref" in obj: |
| 53 | + ref_path = obj["$ref"].strip("#/").split("/") |
| 54 | + ref_value = find_value(document, ref_path) |
| 55 | + if ref_value is None: |
| 56 | + raise RuntimeError(f"Reference {obj['$ref']} not found in the document.") |
| 57 | + return expand_refs(document, ref_value) |
| 58 | + else: |
| 59 | + return {key: expand_refs(document, value) for key, value in obj.items()} |
| 60 | + else: |
| 61 | + return obj |
| 62 | + |
| 63 | + |
| 64 | +class SwaggerOperation(TypedDict, total=False): |
| 65 | + operationId: str |
| 66 | + tags: list[str] |
| 67 | + summary: str |
| 68 | + description: str |
| 69 | + parameters: list[dict[str, Any]] |
| 70 | + responses: dict[str, Any] |
| 71 | + |
| 72 | + |
| 73 | +class SwaggerDocument(TypedDict): |
| 74 | + paths: NotRequired[dict[str, SwaggerOperation]] |
| 75 | + parameters: NotRequired[dict[str, Any]] |
| 76 | + responses: NotRequired[dict[str, Any]] |
| 77 | + definitions: NotRequired[dict[str, Any]] |
| 78 | + |
| 79 | + |
| 80 | +def expand_all_references(document: SwaggerDocument) -> SwaggerDocument: |
| 81 | + """ |
| 82 | + Expands all JSON references. |
| 83 | +
|
| 84 | + Expands all references ($ref) in the merged swagger document by replacing them with |
| 85 | + their full definitions. |
| 86 | +
|
| 87 | + This returns a new document with all references expanded. |
| 88 | +
|
| 89 | + Arguments |
| 90 | + --------- |
| 91 | + document |
| 92 | + The dictionary representing the Swagger document to process |
| 93 | +
|
| 94 | + Returns |
| 95 | + ------- |
| 96 | + : |
| 97 | + The processed Swagger document with all references expanded. |
| 98 | + """ |
| 99 | + ret_document = deepcopy(document) |
| 100 | + # List of error response keys to ignore |
| 101 | + error_responses = [ |
| 102 | + "BadRequest", |
| 103 | + "Unauthorized", |
| 104 | + "PaymentRequired", |
| 105 | + "Forbidden", |
| 106 | + "NotFound", |
| 107 | + "Conflict", |
| 108 | + "APIError", |
| 109 | + "InternalServerError", |
| 110 | + ] |
| 111 | + |
| 112 | + # We need to expand refs in paths |
| 113 | + if "paths" in ret_document: |
| 114 | + for _path, operations in ret_document["paths"].items(): |
| 115 | + for _method, operation in operations.items(): |
| 116 | + if not isinstance(operation, dict): |
| 117 | + continue |
| 118 | + # Expand refs in parameters |
| 119 | + if "parameters" in operation: |
| 120 | + operation["parameters"] = expand_refs(ret_document, operation["parameters"]) |
| 121 | + |
| 122 | + # Expand refs in responses |
| 123 | + if "responses" in operation: |
| 124 | + for code, response in operation["responses"].items(): |
| 125 | + if "schema" in response and code not in error_responses: |
| 126 | + response["schema"] = expand_refs(ret_document, response["schema"]) |
| 127 | + |
| 128 | + # Expand refs in top-level parameters |
| 129 | + if "parameters" in ret_document: |
| 130 | + ret_document["parameters"] = expand_refs(ret_document, ret_document["parameters"]) |
| 131 | + |
| 132 | + # Expand refs in top-level responses, ignoring error responses |
| 133 | + if "responses" in ret_document: |
| 134 | + for response_key, response_value in ret_document["responses"].items(): |
| 135 | + if response_key not in error_responses: |
| 136 | + ret_document["responses"][response_key] = expand_refs(ret_document, response_value) |
| 137 | + |
| 138 | + # Expand refs in definitions |
| 139 | + if "definitions" in ret_document: |
| 140 | + ret_document["definitions"] = expand_refs(ret_document, ret_document["definitions"]) |
| 141 | + |
| 142 | + return ret_document |
| 143 | + |
| 144 | + |
| 145 | +def require_swagger(): |
| 146 | + if not (here / "swagger.json").exists(): |
| 147 | + import urllib.request |
| 148 | + |
| 149 | + urllib.request.urlretrieve( |
| 150 | + "https://docs.posit.co/connect/api/swagger.json", |
| 151 | + here / "_swagger.json", |
| 152 | + ) |
| 153 | + |
| 154 | + import json |
| 155 | + |
| 156 | + with open(here / "_swagger.json") as f: |
| 157 | + doc = json.load(f) |
| 158 | + |
| 159 | + swagger = expand_all_references(doc) |
| 160 | + return swagger |
| 161 | + |
| 162 | + |
| 163 | +class OperationDef(TypedDict): |
| 164 | + name: str |
| 165 | + tags: list[str] |
| 166 | + method: str |
| 167 | + route: str |
| 168 | + definition: dict[str, Any] |
| 169 | + |
| 170 | + |
| 171 | +def transform_swagger_to_operation_dict(swagger_dict: SwaggerDocument) -> dict[str, OperationDef]: |
| 172 | + """ |
| 173 | + Swagger to operation dictionary transformation. |
| 174 | +
|
| 175 | + Transforms the structure of a Swagger dictionary to create a dictionary where each entry key is |
| 176 | + the operation ID and the value is the definition for that operation, including the HTTP verb |
| 177 | + and the route. |
| 178 | +
|
| 179 | + Args: |
| 180 | + swagger_dict: The dictionary representing the Swagger document. |
| 181 | +
|
| 182 | + Returns |
| 183 | + ------- |
| 184 | + : |
| 185 | + A dictionary where each key is an operation ID and the value is the operation definition. |
| 186 | + """ |
| 187 | + operation_dict = {} |
| 188 | + |
| 189 | + if "paths" in swagger_dict: |
| 190 | + for route, operations in swagger_dict["paths"].items(): |
| 191 | + for method, operation in operations.items(): |
| 192 | + if not isinstance(operation, dict): |
| 193 | + continue |
| 194 | + if "operationId" in operation: |
| 195 | + operation_id = operation["operationId"] |
| 196 | + tags = operation["tags"] if "tags" in operation else [] |
| 197 | + # response_parent = operation["responses"]["200"] or operation["responses"]["204"] |
| 198 | + # if response_parent and "response" |
| 199 | + operation_dict[operation_id] = { |
| 200 | + "name": operation_id, |
| 201 | + "tags": tags, |
| 202 | + "method": method, |
| 203 | + "route": route, |
| 204 | + "definition": operation, |
| 205 | + } |
| 206 | + |
| 207 | + return operation_dict |
| 208 | + |
| 209 | + |
| 210 | +def main(): |
| 211 | + swagger = require_swagger() |
| 212 | + |
| 213 | + operations = transform_swagger_to_operation_dict(require_swagger()) |
| 214 | + |
| 215 | + with open(here / "_swagger_prompt.md", "w") as f: |
| 216 | + f.write( |
| 217 | + "If an answer can not be resolved, suggest to the user that they can explore calling these API routes themselves. Never produce code that calls these routes as we do not know the return type or successful status codes.\n\nAPI Routes:\n" |
| 218 | + "" |
| 219 | + ) |
| 220 | + |
| 221 | + for operation in operations.values(): |
| 222 | + # print("Operation:", operation["name"]) |
| 223 | + |
| 224 | + # "GET /v1/tasks/{id} Get task details" |
| 225 | + f.write( |
| 226 | + "* " |
| 227 | + + operation["method"].upper() |
| 228 | + + " " |
| 229 | + + operation["route"] |
| 230 | + + " " |
| 231 | + + operation["definition"]["summary"].replace("\n", " ").strip() |
| 232 | + + "\n", |
| 233 | + ) |
| 234 | + |
| 235 | + |
| 236 | +if __name__ == "__main__": |
| 237 | + main() |
0 commit comments