66# ///
77from __future__ import annotations
88
9- from copy import deepcopy
109import pathlib
1110import os
1211import json
1817T = TypeVar ("T" )
1918
2019
21- def find_value (root , path ):
22- """
23- Find a value in an object graph.
24-
25- This function is used to follow the specified path through the object graph at root
26- and return the item in the graph, if any, that the path refers to.
27-
28- :param root: the root of the object graph to traverse.
29- :param path: the path through the graph to take.
30- :return: the resulting value or None.
31- """
32- if isinstance (path , str ):
33- path = path .split ("/" )
34- parent = root
35- for part in path :
36- if part in parent :
37- parent = parent [part ]
38- else :
39- return None
40- return parent
41-
42-
43- def expand_refs (
44- document , obj
45- ) -> Any : # Use `Any` for return type to hack around typing requirement
46- """
47- Expands `ref`s in the given object.
48-
49- Returns an object semantically equivalent to the original but with references expanded.
50-
51- Parameters
52- ----------
53- document
54- the master swagger document containing the responses and definitions.
55- obj
56- is either a normal swagger object, a ref object, or a swagger object with a schema.
57- """
58- if isinstance (obj , list ):
59- return [expand_refs (document , item ) for item in obj ]
60- elif isinstance (obj , dict ):
61- if "$ref" in obj :
62- ref_path = obj ["$ref" ].strip ("#/" ).split ("/" )
63- ref_value = find_value (document , ref_path )
64- if ref_value is None :
65- raise RuntimeError (
66- f"Reference { obj ['$ref' ]} not found in the document."
67- )
68- return expand_refs (document , ref_value )
69- else :
70- return {key : expand_refs (document , value ) for key , value in obj .items ()}
71- else :
72- return obj
73-
74-
7520class SwaggerOperation (TypedDict , total = False ):
76- operationId : str
77- tags : list [str ]
7821 summary : str
79- description : str
80- parameters : list [dict [str , Any ]]
81- responses : dict [str , Any ]
8222
8323
8424class SwaggerDocument (TypedDict ):
85- paths : NotRequired [dict [str , SwaggerOperation ]]
86- parameters : NotRequired [dict [str , Any ]]
87- responses : NotRequired [dict [str , Any ]]
88- definitions : NotRequired [dict [str , Any ]]
89-
90-
91- def expand_all_references (document : SwaggerDocument ) -> SwaggerDocument :
92- """
93- Expands all JSON references.
94-
95- Expands all references ($ref) in the merged swagger document by replacing them with
96- their full definitions.
97-
98- This returns a new document with all references expanded.
99-
100- Arguments
101- ---------
102- document
103- The dictionary representing the Swagger document to process
104-
105- Returns
106- -------
107- :
108- The processed Swagger document with all references expanded.
109- """
110- ret_document = deepcopy (document )
111- # List of error response keys to ignore
112- error_responses = [
113- "BadRequest" ,
114- "Unauthorized" ,
115- "PaymentRequired" ,
116- "Forbidden" ,
117- "NotFound" ,
118- "Conflict" ,
119- "APIError" ,
120- "InternalServerError" ,
121- ]
122-
123- # We need to expand refs in paths
124- if "paths" in ret_document :
125- for _path , operations in ret_document ["paths" ].items ():
126- for _method , operation in operations .items ():
127- if not isinstance (operation , dict ):
128- continue
129- # Expand refs in parameters
130- if "parameters" in operation :
131- operation ["parameters" ] = expand_refs (
132- ret_document , operation ["parameters" ]
133- )
134-
135- # Expand refs in responses
136- if "responses" in operation :
137- for code , response in operation ["responses" ].items ():
138- if "schema" in response and code not in error_responses :
139- response ["schema" ] = expand_refs (
140- ret_document , response ["schema" ]
141- )
142-
143- # Expand refs in top-level parameters
144- if "parameters" in ret_document :
145- ret_document ["parameters" ] = expand_refs (
146- ret_document , ret_document ["parameters" ]
147- )
148-
149- # Expand refs in top-level responses, ignoring error responses
150- if "responses" in ret_document :
151- for response_key , response_value in ret_document ["responses" ].items ():
152- if response_key not in error_responses :
153- ret_document ["responses" ][response_key ] = expand_refs (
154- ret_document , response_value
155- )
156-
157- # Expand refs in definitions
158- if "definitions" in ret_document :
159- ret_document ["definitions" ] = expand_refs (
160- ret_document , ret_document ["definitions" ]
161- )
162-
163- return ret_document
164-
165-
166- def require_swagger ():
167- if not (here / "_swagger.json" ).exists ():
168- import urllib .request
169-
170- urllib .request .urlretrieve (
171- "https://docs.posit.co/connect/api/swagger.json" ,
172- here / "_swagger.json" ,
173- )
174-
175- doc = json .load (here / "_swagger.json" )
176-
177- swagger = expand_all_references (doc )
178- return swagger
25+ paths : dict [str , SwaggerOperation ]
17926
18027
18128class OperationDef (TypedDict ):
182- name : str
183- tags : list [str ]
18429 method : str
18530 route : str
186- definition : dict [ str , Any ]
31+ summary : str
18732
18833
18934def transform_swagger_to_operation_dict (
19035 swagger_dict : SwaggerDocument ,
191- ) -> dict [ str , OperationDef ]:
36+ ) -> list [ OperationDef ]:
19237 """
19338 Swagger to operation dictionary transformation.
19439
@@ -204,49 +49,78 @@ def transform_swagger_to_operation_dict(
20449 :
20550 A dictionary where each key is an operation ID and the value is the operation definition.
20651 """
207- operation_dict = {}
52+ operation_list : list [OperationDef ] = []
53+
54+ if "paths" not in swagger_dict :
55+ raise ValueError (
56+ "The Swagger document `swagger_dict=` does not contain a 'paths' key."
57+ )
20858
20959 if "paths" in swagger_dict :
21060 for route , operations in swagger_dict ["paths" ].items ():
61+ if not isinstance (route , str ):
62+ raise ValueError (
63+ f"Expected route to be a string, but got { type (route )} ."
64+ )
21165 for method , operation in operations .items ():
66+ if not isinstance (method , str ):
67+ raise ValueError (
68+ f"Expected method to be a string, but got { type (method )} ."
69+ )
21270 if not isinstance (operation , dict ):
213- continue
214- if "operationId" in operation :
215- operation_id = operation ["operationId" ]
216- tags = operation ["tags" ] if "tags" in operation else []
217- # response_parent = operation["responses"]["200"] or operation["responses"]["204"]
218- # if response_parent and "response"
219- operation_dict [operation_id ] = {
220- "name" : operation_id ,
221- "tags" : tags ,
71+ raise ValueError (
72+ f"Expected operation to be a dictionary, but got { type (operation )} ."
73+ )
74+ if "summary" not in operation :
75+ raise ValueError (
76+ f"Expected operation to have a 'summary' key, but got { operation } ."
77+ )
78+ summary = operation ["summary" ]
79+ if not isinstance (summary , str ):
80+ raise ValueError (
81+ f"Expected summary to be a string, but got { type (summary )} ."
82+ )
83+ operation_list .append (
84+ {
22285 "method" : method ,
22386 "route" : route ,
224- "definition " : operation ,
87+ "summary " : summary ,
22588 }
89+ )
22690
227- return operation_dict
91+ return operation_list
22892
22993
23094def main ():
231- swagger = require_swagger ()
95+ if not (here / "_swagger.json" ).exists ():
96+ import urllib .request
97+
98+ urllib .request .urlretrieve (
99+ "https://docs.posit.co/connect/api/swagger.json" ,
100+ here / "_swagger.json" ,
101+ )
102+
103+ swagger = json .loads ((here / "_swagger.json" ).read_text ())
232104
233105 operations = transform_swagger_to_operation_dict (swagger )
234106
107+ # Write out the swagger portion of the instructions with a preamble and a
108+ # list of all the API routes and their short summaries.
235109 with open (here / "_swagger_prompt.md" , "w" ) as f :
236110 f .write (
237111 "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 \n API Routes:\n "
238112 ""
239113 )
240114
241- for operation in operations . values () :
242- # `"GET /v1/tasks/{id} Get task details"`
115+ for operation in operations :
116+ # `"* GET /v1/tasks/{id} Get task details"`
243117 f .write (
244118 "* "
245119 + operation ["method" ].upper ()
246120 + " "
247121 + operation ["route" ]
248122 + " "
249- + operation ["definition" ][ " summary" ].replace ("\n " , " " ).strip ()
123+ + operation ["summary" ].replace ("\n " , " " ).strip ()
250124 + "\n " ,
251125 )
252126
0 commit comments