1+ #!python
2+
3+ import click
4+ # from prettytable import PrettyTable
5+ import json
6+ import requests
7+ import os
8+ import shutil
9+ from string import Template
10+ import re
11+
12+
13+ METAURL = 'https://api.softlayer.com/metadata/v3.1'
14+
15+
16+ class OpenAPIGen ():
17+
18+ def __init__ (self , outdir : str ) -> None :
19+ self .outdir = outdir
20+ if not os .path .isdir (self .outdir ):
21+ print (f"Creating directory { self .outdir } " )
22+ os .mkdir (self .outdir )
23+ self .metajson = None
24+ self .metapath = f'{ self .outdir } /sldn_metadata.json'
25+ self .openapi = {
26+ "openapi" : '3.0.3' ,
27+ "info" : {
28+ "title" : "SoftLayer API - OpenAPI 3.0" ,
29+ "description" : "SoftLayer API Definitions in a swagger format" ,
30+ "termsOfService" : "https://cloud.ibm.com/docs/overview?topic=overview-terms" ,
31+ "version" : "1.0.0"
32+ },
33+ "externalDocs" : {
34+ "description" : "SLDN" ,
35+ "url" : "https://sldn.softlayer.com"
36+ },
37+ "servers" : [
38+ {"url" : "https://api.softlayer.com" },
39+ {"url" : "https://api.service.softlayer.com" }
40+ ],
41+ "paths" : {},
42+ "components" : {
43+ "schemas" : {},
44+ "requestBodies" : {},
45+ "securitySchemes" : {
46+ "api_key" : {
47+ "type" : "apiKey" ,
48+ "name" : "api_key" ,
49+ "in" : "header"
50+ }
51+ }
52+ }
53+ }
54+
55+ def getMetadata (self , url : str ) -> dict :
56+ """Downloads metadata from SLDN"""
57+ response = requests .get (url )
58+ if response .status_code != 200 :
59+ raise Exception (f"{ url } returned \n { response .text } \n HTTP CODE: { response .status_code } " )
60+
61+ self .metajson = response .json ()
62+ return self .metajson
63+
64+ def saveMetadata (self ) -> None :
65+ """Saves metadata to a file"""
66+ print (f"Writing SLDN Metadata to { self .metapath } " )
67+ with open (self .metapath , 'w' ) as f :
68+ json .dump (self .metajson , f , indent = 4 )
69+
70+ def getLocalMetadata (self ) -> dict :
71+ """Loads metadata from local data folder"""
72+ with open (self .metapath , "r" , encoding = "utf-8" ) as f :
73+ metadata = f .read ()
74+ self .metajson = json .loads (metadata )
75+ return self .metajson
76+
77+ def addInORMMethods (self ):
78+ for serviceName , service in self .metajson .items ():
79+ # noservice means datatype only.
80+ if service .get ('noservice' , False ) == False :
81+ for propName , prop in service .get ('properties' , {}).items ():
82+ if prop .get ('form' , '' ) == 'relational' :
83+ # capitlize() sadly lowercases the other letters in the string
84+ ormName = f"get{ propName [0 ].upper ()} { propName [1 :]} "
85+ ormMethod = {
86+ 'doc' : prop .get ('doc' , '' ),
87+ 'docOverview' : "" ,
88+ 'name' : ormName ,
89+ 'type' : prop .get ('type' ),
90+ 'typeArray' : prop .get ('typeArray' , None ),
91+ 'ormMethod' : True ,
92+ 'maskable' : True ,
93+ 'filterable' : True ,
94+ 'deprecated' : prop .get ('deprecated' , False )
95+ }
96+ if ormMethod ['typeArray' ]:
97+ ormMethod ['limitable' ] = True
98+ self .metajson [serviceName ]['methods' ][ormName ] = ormMethod
99+ return self .metajson
100+
101+ def addInChildMethods (self ):
102+ for serviceName , service in self .metajson .items ():
103+ self .metajson [serviceName ]['methods' ] = self .getBaseMethods (serviceName , 'methods' )
104+ self .metajson [serviceName ]['properties' ] = self .getBaseMethods (serviceName , 'properties' )
105+
106+
107+ def getBaseMethods (self , serviceName , objectType ):
108+ """Responsible for pulling in properties or methods from the base class of the service requested"""
109+ service = self .metajson [serviceName ]
110+ methods = service .get (objectType , {})
111+ if service .get ('base' , "SoftLayer_Entity" ) != "SoftLayer_Entity" :
112+
113+ baseMethods = self .getBaseMethods (service .get ('base' ), objectType )
114+ for bName , bMethod in baseMethods .items ():
115+ if not methods .get (bName , False ):
116+ methods [bName ] = bMethod
117+ return methods
118+
119+ def generate (self ) -> None :
120+ print ("OK" )
121+ for serviceName , service in self .metajson .items ():
122+ print (f"Working on { serviceName } " )
123+ # Writing the check this way to be more clear to myself when reading it
124+ # This service has methods
125+ if service .get ('noservice' , False ) == False :
126+ for methodName , method in service .get ('methods' , {}).items ():
127+ self .openapi ['paths' ].update (self .genPath (serviceName , methodName , method ))
128+
129+
130+ with open (f"{ self .outdir } /sl_openapi.json" , "w" ) as outfile :
131+ json .dump (self .openapi , outfile , indent = 4 )
132+
133+ def genPath (self , serviceName : str , methodName : str , method : dict ) -> dict :
134+ http_method = "get"
135+ if method .get ('parameters' , False ):
136+ http_method = "post"
137+ init_param = ''
138+ if not method .get ('static' , False ):
139+ init_param = f"{{{ serviceName } ID}}/"
140+
141+ schema = method .get ('type' )
142+ new_path = {
143+ f"{ serviceName } /{ init_param } { methodName } " : {
144+ http_method : {
145+ "description" : method .get ('doc' ),
146+ "summary" : method .get ('docOverview' , '' ),
147+ "externalDocs" : f"https://sldn.softlayer.com/reference/services/{ serviceName } /{ methodName } /" ,
148+ "operationId" : f"{ serviceName } ::{ methodName } " ,
149+ "responses" : {
150+ "200" : {
151+ "description" : "Successful operation" ,
152+ "content" : {
153+ "application/json" : {
154+ "schema" : self .getSchema (method )
155+ }
156+ }
157+ }
158+ },
159+ "security" : [
160+ {"api_key" : []}
161+ ]
162+ }
163+ }
164+ }
165+ return new_path
166+
167+ def getSchema (self , method : dict ) -> dict :
168+ """Gets a formatted schema object from a method"""
169+ is_array = method .get ('typeArray' , False )
170+ sl_type = method .get ('type' , "null" )
171+ ref = {}
172+ if sl_type .startswith ("SoftLayer_" ):
173+ ref = {"$ref" : f"#/components/schemas/{ sl_type } " }
174+ else :
175+ ref = {"type" : sl_type }
176+ if is_array :
177+ schema = {"type" : "array" , "items" : ref }
178+ else :
179+ schema = ref
180+ return schema
181+
182+
183+
184+ @click .command ()
185+ @click .option ('--download' , default = False , is_flag = True )
186+ @click .option ('--clean' , default = False , is_flag = True , help = "Removes the services and datatypes directories so they can be built from scratch" )
187+ def main (download : bool , clean : bool ):
188+ cwd = os .getcwd ()
189+ outdir = f'{ cwd } /openapi'
190+ if not cwd .endswith ('githubio_source' ):
191+ raise Exception (f"Working Directory should be githubio_source, is currently { cwd } " )
192+
193+ if clean :
194+ print (f"Removing { outdir } " )
195+ try :
196+ shutil .rmtree (f'{ outdir } ' )
197+ except FileNotFoundError :
198+ print ("Directory doesnt exist..." )
199+
200+ generator = OpenAPIGen (outdir )
201+ if download :
202+ try :
203+ metajson = generator .getMetadata (url = METAURL )
204+ generator .addInChildMethods ()
205+ generator .addInORMMethods ()
206+ generator .saveMetadata ()
207+ except Exception as e :
208+ print ("========== ERROR ==========" )
209+ print (f"{ e } " )
210+ print ("========== ERROR ==========" )
211+ else :
212+ metajson = generator .getLocalMetadata ()
213+
214+ print ("Generating OpenAPI...." )
215+ generator .generate ()
216+
217+
218+ if __name__ == "__main__" :
219+ main ()
0 commit comments