pylapi 0.12.16__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pylapi/__init__.py +1 -0
- pylapi/__version__.py +8 -0
- pylapi/autogen.py +790 -0
- pylapi/autogen_template.py +80 -0
- pylapi/config.py +26 -0
- pylapi/pylapi.py +1022 -0
- pylapi-0.12.16.dist-info/LICENSE +21 -0
- pylapi-0.12.16.dist-info/METADATA +137 -0
- pylapi-0.12.16.dist-info/RECORD +12 -0
- pylapi-0.12.16.dist-info/WHEEL +5 -0
- pylapi-0.12.16.dist-info/entry_points.txt +2 -0
- pylapi-0.12.16.dist-info/top_level.txt +1 -0
pylapi/autogen.py
ADDED
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
# PyLapi API Generator
|
|
2
|
+
#
|
|
3
|
+
# This script generates a PyLapi API from a configuration file.
|
|
4
|
+
#
|
|
5
|
+
# Steps:
|
|
6
|
+
# 1. Create a configuration file from the configuration template.
|
|
7
|
+
# 2. Customise the configuration file by following the instructions in the comment.
|
|
8
|
+
# 3. Run this script with the configuration file as the first argument.
|
|
9
|
+
# - The OpenAPI specification JSON file is specified
|
|
10
|
+
# - as the second argument, or if not specified
|
|
11
|
+
# - in the configuration file `oas` variable, or if not specified
|
|
12
|
+
# - as the configuration file name, with .py replaced by .json
|
|
13
|
+
# - The OpenAPI JSON file path is either absolute or relative to the current directory.
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import yaml
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
import os
|
|
20
|
+
from enum import IntEnum
|
|
21
|
+
from getopt import getopt, GetoptError
|
|
22
|
+
from magico import MagicO
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Defaults
|
|
26
|
+
debug = False
|
|
27
|
+
oas_spec = {}
|
|
28
|
+
output_py = None
|
|
29
|
+
output_py_name = None
|
|
30
|
+
guide_attrs = None
|
|
31
|
+
|
|
32
|
+
all_guide_attrs = {
|
|
33
|
+
"summary",
|
|
34
|
+
"description",
|
|
35
|
+
"parameters",
|
|
36
|
+
"request_body",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
control_guide_attrs = {
|
|
40
|
+
"ref",
|
|
41
|
+
"all",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
config_file = ""
|
|
45
|
+
|
|
46
|
+
main_basename = os.path.basename(sys.argv[0]).rstrip('.py')
|
|
47
|
+
|
|
48
|
+
def usage(exit_code=0):
|
|
49
|
+
print(f"""Usage: {main_basename} [options ...] <config.py> [<openapi.json/yaml]
|
|
50
|
+
\t-h --help Print this help message
|
|
51
|
+
\t-o --output=<file> Output the API SDK to <file> instead of stdout
|
|
52
|
+
\t-g --guide=<list> Include some OpenAI attributes as comments; the list is comma-deliminted list with no spaces.
|
|
53
|
+
\t Valid attributes include: summary,description,parameters,request_body,all,ref
|
|
54
|
+
\t `all` means to include all items on the list
|
|
55
|
+
\t `ref` means to dereference $ref cells in OpenAI attributes
|
|
56
|
+
\t-t --template Print the autogen configuration template to stdout
|
|
57
|
+
\t-d --debug Print all the methods to be generated then stop""", file=sys.stderr)
|
|
58
|
+
exit(exit_code)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
valid_guide_attrs = all_guide_attrs.union(control_guide_attrs)
|
|
62
|
+
|
|
63
|
+
code_rewrite_lines = []
|
|
64
|
+
snippets = MagicO({})
|
|
65
|
+
|
|
66
|
+
# For naming conversion, use MagicWords(name).<conversion>, where <conversion> can be
|
|
67
|
+
# snake, kebab, pascal (upperCamel), camel (lowerCamel), or singular
|
|
68
|
+
# e.g., MagicWords("ThisIs a good_test for magic-words_conversion.").snake
|
|
69
|
+
# Output: this_is_a_good_test_for_magic_words_conversion
|
|
70
|
+
def snake(phrase): return MagicWords(phrase).snake
|
|
71
|
+
def kebab(phrase): return MagicWords(phrase).kebab
|
|
72
|
+
def upperCamel(phrase): return MagicWords(phrase).upperCamel
|
|
73
|
+
def lowerCamel(phrase): return MagicWords(phrase).lowerCamel
|
|
74
|
+
def pascal(phrase): return MagicWords(phrase).pascal
|
|
75
|
+
def camel(phrase): return MagicWords(phrase).camel
|
|
76
|
+
def singular(phrase): return MagicWords(phrase).singular
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Method():
|
|
80
|
+
def __init__(self, config, method):
|
|
81
|
+
self.config = config
|
|
82
|
+
self.method = method
|
|
83
|
+
self.path_segments = self.method["path"].strip("/").split("/")
|
|
84
|
+
|
|
85
|
+
def __repr__(self):
|
|
86
|
+
return str(self.method)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def api_path(self):
|
|
90
|
+
_api_path = "/" + self.class_path if self.class_path else ""
|
|
91
|
+
_api_path += "/" + self.resource_path if self.resource_path else ""
|
|
92
|
+
return _api_path
|
|
93
|
+
|
|
94
|
+
########################################
|
|
95
|
+
#
|
|
96
|
+
# Derived attributes - customisables
|
|
97
|
+
# These attributes are derived from the original OpenAPI JSON
|
|
98
|
+
# attributes as described in the next section.
|
|
99
|
+
#
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def class_name(self):
|
|
103
|
+
# return MagicWords(MagicWords(self.path_segments[0]).singular).upperCamel + "Resource"
|
|
104
|
+
return eval(self.config.naming["class_name"].replace("$", "self"))
|
|
105
|
+
|
|
106
|
+
# Resource Name used to create a resource object:
|
|
107
|
+
# my_resource = MyAPI.resource("example_resource")
|
|
108
|
+
# Example: Singular snake case for resource names
|
|
109
|
+
@property
|
|
110
|
+
def resource_name(self):
|
|
111
|
+
# return MagicWords(MagicWords(self.path_segments[0]).singular).snake
|
|
112
|
+
return eval(self.config.naming["resource_name"].replace("$", "self"))
|
|
113
|
+
|
|
114
|
+
# API path prefix for all methods in the class
|
|
115
|
+
# API full path is {class_path}/{resource_path}
|
|
116
|
+
# Example: resources in /resources/api_method/...
|
|
117
|
+
# IMPORTANT: Make sure the class_path is common to all methods in the class.
|
|
118
|
+
@property
|
|
119
|
+
def class_path(self):
|
|
120
|
+
# return self._path_segments[0]
|
|
121
|
+
# return ""
|
|
122
|
+
return eval(self.config.naming["class_path"].replace("$", "self"))
|
|
123
|
+
|
|
124
|
+
# API path suffix for the method
|
|
125
|
+
# API full path is {class_path}/{resource_path}
|
|
126
|
+
# Example: api_method in /resources/api_method/...
|
|
127
|
+
@property
|
|
128
|
+
def resource_path(self):
|
|
129
|
+
# return "/".join(self._path_segments[1:])
|
|
130
|
+
# return "/".join(self.path_segments)
|
|
131
|
+
return eval(self.config.naming["resource_path"].replace("$", "self"))
|
|
132
|
+
|
|
133
|
+
# Method Name
|
|
134
|
+
# def exampleMethod(self):
|
|
135
|
+
# Example: Lower Camel case for method names
|
|
136
|
+
@property
|
|
137
|
+
def method_name(self):
|
|
138
|
+
# return MagicWords(self.operation_id).lowerCamel
|
|
139
|
+
return eval(self.config.naming["method_name"].replace("$", "self"))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
########################################
|
|
143
|
+
#
|
|
144
|
+
# Initialised attributes - not to be customised
|
|
145
|
+
# These attributes are captured from the
|
|
146
|
+
# OpenAPI JSON file, and returned without
|
|
147
|
+
# conversion (except http_method to uppercase).
|
|
148
|
+
#
|
|
149
|
+
@property
|
|
150
|
+
def path(self):
|
|
151
|
+
return self.method["path"]
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def http_method(self):
|
|
155
|
+
return self.method["http_method"].upper()
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def operation_id(self):
|
|
159
|
+
return self.method["operation_id"]
|
|
160
|
+
|
|
161
|
+
# The followings are used for documentation in the generated file.
|
|
162
|
+
# Comment out, or return blank, None, or False to suppress.
|
|
163
|
+
@property
|
|
164
|
+
def summary(self):
|
|
165
|
+
return self.method["summary"]
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def description(self):
|
|
169
|
+
return self.method["description"]
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def parameters(self):
|
|
173
|
+
return self.method["parameters"]
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def request_body(self):
|
|
177
|
+
return self.method["request_body"]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def dict_checked(dict_data, attr, default=""):
|
|
181
|
+
return dict_data[attr] if attr in dict_data else default
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def print_error(err=""):
|
|
185
|
+
print(err, file=sys.stderr)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def print_line(line=""):
|
|
189
|
+
if line != None:
|
|
190
|
+
print(line.rstrip(), file=output_py)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_oas_spec(oas_file_name):
|
|
194
|
+
global config_file
|
|
195
|
+
_oas_spec = {}
|
|
196
|
+
_oas_type = re.sub(r".+\.(json|yaml)$", "\\1", oas_file_name)
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
if _oas_type == "json":
|
|
200
|
+
_oas_spec = json.load(open(oas_file_name, "r"))
|
|
201
|
+
elif _oas_type == "yaml":
|
|
202
|
+
_oas_spec = yaml.safe_load(open(oas_file_name, "r"))
|
|
203
|
+
else:
|
|
204
|
+
raise Exception(f"Unknown OpenAI specification file type: {oas_file_name}")
|
|
205
|
+
except:
|
|
206
|
+
raise Exception(f"Cannot open OpenAI specification file: {oas_file_name}\nSuggest checking configuration file: {config_file}")
|
|
207
|
+
|
|
208
|
+
for _ in ("openapi", "paths"):
|
|
209
|
+
if _ not in _oas_spec:
|
|
210
|
+
raise Exception(f'OpenAI Error: No "{_}" found in {oas_file_name}')
|
|
211
|
+
|
|
212
|
+
return _oas_spec
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def set_config(config, _oas_spec):
|
|
216
|
+
# Get all config settings defined
|
|
217
|
+
# Missing settings will be taken from _oas_spec
|
|
218
|
+
|
|
219
|
+
# config.api_class_name fallback on oas.info.title
|
|
220
|
+
try:
|
|
221
|
+
_ = config.api_class_name # A trivial check
|
|
222
|
+
except:
|
|
223
|
+
try:
|
|
224
|
+
api_class_name = upperCamel(_oas_spec["info"]["title"])
|
|
225
|
+
except:
|
|
226
|
+
raise Exception(f"API class name cannot be determined: Neither in config and nor in OpenAI specification")
|
|
227
|
+
else:
|
|
228
|
+
config.api_class_name = api_class_name
|
|
229
|
+
|
|
230
|
+
# config.api_url fallback on oas.servers[0].url
|
|
231
|
+
try:
|
|
232
|
+
api_url = config.api_url # A trivial check
|
|
233
|
+
except:
|
|
234
|
+
try:
|
|
235
|
+
api_url = _oas_spec["servers"][0]["url"]
|
|
236
|
+
except:
|
|
237
|
+
raise Exception(f"API URL cannot be determined: Neither in config and nor in OpenAI specification")
|
|
238
|
+
else:
|
|
239
|
+
config.api_url = api_url
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class RewriteExpect(IntEnum):
|
|
243
|
+
CLASS_API = 0
|
|
244
|
+
CLASS_DECO = 1
|
|
245
|
+
CLASS_DEF = 2
|
|
246
|
+
METHOD_DECO = 3
|
|
247
|
+
METHOD_DEF = 4
|
|
248
|
+
|
|
249
|
+
def get_code_rewrite(config, code_rewrite_file_name):
|
|
250
|
+
global snippets
|
|
251
|
+
global code_rewrite_lines
|
|
252
|
+
|
|
253
|
+
if not code_rewrite_file_name:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
code_rewrite_lines = [_.rstrip() for _ in open(code_rewrite_file_name, "r").readlines()]
|
|
257
|
+
# print(len(code_rewrite_lines))
|
|
258
|
+
|
|
259
|
+
# {"resource": (slice_from, slice_to)}
|
|
260
|
+
# where "resource" being one of these:
|
|
261
|
+
# "" - opening lines
|
|
262
|
+
# "{method.class_name}" - a class
|
|
263
|
+
# "{method.class_name}.{method.method_name}" - a method
|
|
264
|
+
# If "resource" is "", expect a class, else except a class or a method
|
|
265
|
+
|
|
266
|
+
this_snippet = "@" # Opening
|
|
267
|
+
this_class = "" # Current class the line is in
|
|
268
|
+
this_slice = [0]
|
|
269
|
+
snippets[this_snippet] = this_slice
|
|
270
|
+
expect = (RewriteExpect.CLASS_API, RewriteExpect.CLASS_DECO)
|
|
271
|
+
line = -1 # In case the file is empty
|
|
272
|
+
for line in range(len(code_rewrite_lines)):
|
|
273
|
+
# print(f"line {line}")
|
|
274
|
+
code_rewrite_line = code_rewrite_lines[line]
|
|
275
|
+
# Strategy: Most frequent cases checked first
|
|
276
|
+
if RewriteExpect.METHOD_DECO in expect:
|
|
277
|
+
if re.findall(rf"^\s*@{config.api_class_name}\.resource_method", code_rewrite_line):
|
|
278
|
+
# print(f"MDC {line+1}: {code_rewrite_line}")
|
|
279
|
+
this_slice.append(line) # Complete `this` first
|
|
280
|
+
snippets[this_snippet] = this_slice
|
|
281
|
+
this_slice = [line] # Mark the new
|
|
282
|
+
expect = (RewriteExpect.METHOD_DEF,)
|
|
283
|
+
# print(f"expect {expect}")
|
|
284
|
+
continue
|
|
285
|
+
if RewriteExpect.METHOD_DEF in expect:
|
|
286
|
+
method_name = re.findall(rf"^\s*def\s+([^(]+)\(", code_rewrite_line)
|
|
287
|
+
if len(method_name):
|
|
288
|
+
# print(f"MDF {line+1}: {code_rewrite_line}")
|
|
289
|
+
method_name = method_name[0]
|
|
290
|
+
this_snippet = f"{this_class}.{method_name}"
|
|
291
|
+
expect = (RewriteExpect.METHOD_DECO, RewriteExpect.CLASS_DECO)
|
|
292
|
+
# print(f"expect {expect}")
|
|
293
|
+
continue
|
|
294
|
+
if RewriteExpect.CLASS_DECO in expect:
|
|
295
|
+
if re.findall(rf"^\s*@{config.api_class_name}\.resource_class", code_rewrite_line):
|
|
296
|
+
# print(f"CDC {line+1}: {code_rewrite_line}")
|
|
297
|
+
this_slice.append(line) # Complete `this` first
|
|
298
|
+
snippets[this_snippet] = this_slice
|
|
299
|
+
this_slice = [line] # Mark the new
|
|
300
|
+
expect = (RewriteExpect.CLASS_DEF,)
|
|
301
|
+
# print(f"expect {expect}")
|
|
302
|
+
continue
|
|
303
|
+
if RewriteExpect.CLASS_DEF in expect:
|
|
304
|
+
class_name = re.findall(rf"^\s*class\s+([^(]+)\(", code_rewrite_line)
|
|
305
|
+
if len(class_name):
|
|
306
|
+
# print(f"CDF {line+1}: {code_rewrite_line}")
|
|
307
|
+
this_class = class_name[0]
|
|
308
|
+
this_snippet = f"{this_class}.@"
|
|
309
|
+
expect = (RewriteExpect.METHOD_DECO, RewriteExpect.CLASS_DECO)
|
|
310
|
+
# print(f"expect {expect}")
|
|
311
|
+
continue
|
|
312
|
+
if RewriteExpect.CLASS_API in expect:
|
|
313
|
+
if re.findall(rf"^\s*class {config.api_class_name}\(", code_rewrite_line):
|
|
314
|
+
# print(f"API {line+1}: {code_rewrite_line}")
|
|
315
|
+
this_slice.append(line) # Complete `this` first
|
|
316
|
+
snippets[this_snippet] = this_slice
|
|
317
|
+
this_slice = [line] # Mark the new
|
|
318
|
+
this_snippet = f"{config.api_class_name}.@"
|
|
319
|
+
expect = (RewriteExpect.CLASS_DECO,)
|
|
320
|
+
# print(f"expect {expect}")
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
# Complete the last snippet
|
|
324
|
+
this_slice.append(line+1)
|
|
325
|
+
snippets[this_snippet] = this_slice
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def rewrite_me(snippet_name, intro_line=None):
|
|
329
|
+
_intro_line = None
|
|
330
|
+
_code_lines = None
|
|
331
|
+
_co = ""
|
|
332
|
+
if snippet_name in snippets:
|
|
333
|
+
_code_lines = "\n".join(code_rewrite_lines[slice(*snippets[snippet_name])])
|
|
334
|
+
if intro_line and _code_lines.rstrip():
|
|
335
|
+
leading_spaces = re.findall(r"^(\s*)\S", _code_lines)
|
|
336
|
+
line_indent = ""
|
|
337
|
+
if len(leading_spaces):
|
|
338
|
+
line_indent = leading_spaces[0]
|
|
339
|
+
# Always a space line before
|
|
340
|
+
_intro_line = f"\n{line_indent}# {intro_line}"
|
|
341
|
+
_co = "# "
|
|
342
|
+
del snippets[snippet_name]
|
|
343
|
+
return _intro_line, _code_lines, _co
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def get_methods(config, oas_paths):
|
|
347
|
+
methods = []
|
|
348
|
+
for oas_path in oas_paths: # Loop through each API path in OAS
|
|
349
|
+
oas_http_methods = oas_paths[oas_path] # A list of HTTP methods, e.g., "get"
|
|
350
|
+
if "parameters" in oas_http_methods: # "parameters" is NOT an HTTP method
|
|
351
|
+
del oas_http_methods["parameters"]
|
|
352
|
+
for oas_http_method in oas_http_methods: # Loop through each HTTP method the path supports
|
|
353
|
+
oas_method = oas_http_methods[oas_http_method] # A specification of the API method
|
|
354
|
+
if "operationId" not in oas_method:
|
|
355
|
+
raise Exception(f"operationId missing in {oas_path}:{oas_http_method}")
|
|
356
|
+
# print(f"{oas_http_method.upper()} {oas_path}")
|
|
357
|
+
# All attributes of `method_to_add` must be defined, even if not in OAS
|
|
358
|
+
method_to_add = Method(config, {
|
|
359
|
+
"path": oas_path,
|
|
360
|
+
"http_method": oas_http_method,
|
|
361
|
+
"operation_id": oas_method["operationId"],
|
|
362
|
+
"summary": dict_checked(oas_method, "summary"),
|
|
363
|
+
"description": dict_checked(oas_method, "description"),
|
|
364
|
+
"parameters": [
|
|
365
|
+
{
|
|
366
|
+
_["name"]: {
|
|
367
|
+
"in": dict_checked(_, "in"),
|
|
368
|
+
"description": dict_checked(_, "description"),
|
|
369
|
+
"required": dict_checked(_, "required", False),
|
|
370
|
+
"example": dict_checked(_, "example"),
|
|
371
|
+
"schema": dict_checked(_, "schema"),
|
|
372
|
+
"style": dict_checked(_, "style"),
|
|
373
|
+
"explode": dict_checked(_, "explode", False),
|
|
374
|
+
} for _ in oas_method["parameters"] if "name" in _
|
|
375
|
+
}
|
|
376
|
+
] if "parameters" in oas_method else [],
|
|
377
|
+
"request_body": oas_method["requestBody"] if "requestBody" in oas_method else {},
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
# print(method_to_add)
|
|
381
|
+
methods.append(method_to_add)
|
|
382
|
+
|
|
383
|
+
methods.sort(key=lambda _: f"{_.class_name}/{_.method_name}")
|
|
384
|
+
|
|
385
|
+
return methods
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def gen_header(config, methods):
|
|
389
|
+
# A set of unique class names
|
|
390
|
+
class_set = sorted(set(_.class_name for _ in methods))
|
|
391
|
+
classes = {}
|
|
392
|
+
invalid_classes = {}
|
|
393
|
+
for class_name in class_set:
|
|
394
|
+
# All methods in the class
|
|
395
|
+
methods_of_class = [_ for _ in methods if _.class_name == class_name]
|
|
396
|
+
if len(methods_of_class): # Show always be True
|
|
397
|
+
path_of_class = methods_of_class[0].class_path
|
|
398
|
+
for _ in methods:
|
|
399
|
+
if _.class_name == class_name and \
|
|
400
|
+
_.class_path != path_of_class:
|
|
401
|
+
print_error(f"{class_name}.{_.method_name}() on \"{_.class_path}\" instead of \"{path_of_class}\"")
|
|
402
|
+
if class_name not in invalid_classes:
|
|
403
|
+
invalid_classes[class_name] = set([path_of_class])
|
|
404
|
+
invalid_classes[class_name].add(_.class_path)
|
|
405
|
+
if invalid_classes == {}:
|
|
406
|
+
method_names_of_class = [_.method_name for _ in methods_of_class]
|
|
407
|
+
classes.update({
|
|
408
|
+
class_name: {
|
|
409
|
+
"methods": method_names_of_class,
|
|
410
|
+
"count": len(methods_of_class),
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
if invalid_classes:
|
|
415
|
+
# classes is empty
|
|
416
|
+
print_error()
|
|
417
|
+
print_error(f"Error:")
|
|
418
|
+
for _ in invalid_classes:
|
|
419
|
+
print_error(f" Multiple class paths in {_}: {', '.join(invalid_classes[_])}")
|
|
420
|
+
print_error()
|
|
421
|
+
print_error(f"Possibly due to invalid resource consolidation in:")
|
|
422
|
+
print_error(f" config.naming[\"class_name\"] == \"{config.naming['class_name']}\"")
|
|
423
|
+
exit(1)
|
|
424
|
+
|
|
425
|
+
print_line(f"# PyLapi API generated by {main_basename}")
|
|
426
|
+
print_line(f"#")
|
|
427
|
+
print_line(f"# API Class: {config.api_class_name}")
|
|
428
|
+
print_line(f"# {len(classes)} Resource Classes:")
|
|
429
|
+
for _ in classes:
|
|
430
|
+
print_line(f"# {_}: {classes[_]['count']} native methods")
|
|
431
|
+
print_line(f"# Total: {len(methods)} native methods")
|
|
432
|
+
print_line()
|
|
433
|
+
|
|
434
|
+
if debug:
|
|
435
|
+
# Output OpenAI analysis then exit
|
|
436
|
+
print_line(f"#")
|
|
437
|
+
ii = 0
|
|
438
|
+
for method in methods:
|
|
439
|
+
ii += 1
|
|
440
|
+
print_line(f"#{ii:4}. {method.class_name}:{method.method_name}(): {method.http_method} {method.path}")
|
|
441
|
+
exit(0)
|
|
442
|
+
|
|
443
|
+
return classes
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def gen_api_class(config):
|
|
447
|
+
global snippets
|
|
448
|
+
global code_rewrite_lines
|
|
449
|
+
|
|
450
|
+
api_auth_type = "Bearer"
|
|
451
|
+
try:
|
|
452
|
+
api_auth_type = config.api_auth_type
|
|
453
|
+
except:
|
|
454
|
+
config.api_auth_type = api_auth_type
|
|
455
|
+
|
|
456
|
+
intro_line, code_lines, co = rewrite_me("@", "Custom opening")
|
|
457
|
+
print_line(intro_line)
|
|
458
|
+
print_line(code_lines)
|
|
459
|
+
|
|
460
|
+
print_line("from pylapi import PyLapi, PyLapiError")
|
|
461
|
+
print_line()
|
|
462
|
+
|
|
463
|
+
intro_line, code_lines, co = rewrite_me(f"{config.api_class_name}.@", f"Custom API class: {config.api_class_name}(PyLapi)")
|
|
464
|
+
print_line(intro_line)
|
|
465
|
+
print_line(f"{co}class {config.api_class_name}(PyLapi):")
|
|
466
|
+
print_line(f"{co} def __init__(self, *args, **kwargs) -> None:")
|
|
467
|
+
print_line(f"{co} super().__init__(*args, **kwargs)")
|
|
468
|
+
print_line(f"{co} self.api_url = \"{config.api_url}\"")
|
|
469
|
+
print_line(f"{co} self.api_auth_type = \"{config.api_auth_type}\"")
|
|
470
|
+
print_line(code_lines)
|
|
471
|
+
print_line()
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def print_class(config, method, classes, resource_class_args_str):
|
|
475
|
+
print_line()
|
|
476
|
+
|
|
477
|
+
intro_line, code_lines, co = rewrite_me(f"{method.class_name}.@", f"Custom resource class: {method.class_name}({config.api_class_name})")
|
|
478
|
+
print_line(intro_line)
|
|
479
|
+
print_line(f"{co}@{config.api_class_name}.resource_class(\"{method.resource_name}\", \"{method.class_path}\"{resource_class_args_str})")
|
|
480
|
+
print_line(f"{co}class {method.class_name}({config.api_class_name}):")
|
|
481
|
+
print_line(code_lines)
|
|
482
|
+
|
|
483
|
+
print_line(f"# To instantiate: {config.api_class_name}.resource(\"{method.resource_name}\")")
|
|
484
|
+
print_line(f"# Number of native methods: {classes[method.class_name]['count']}")
|
|
485
|
+
for _ in classes[method.class_name]["methods"]:
|
|
486
|
+
print_line(f"# {_}")
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def print_method(config, method, resource_method_args_str):
|
|
490
|
+
print_line()
|
|
491
|
+
|
|
492
|
+
route_var_names = re.findall(r"{([a-zA-Z_-]([0-9a-zA-Z_-])*)}", method.resource_path)
|
|
493
|
+
route_var_names = [_[0] + "=..." for _ in route_var_names]
|
|
494
|
+
route_var_names.append("...")
|
|
495
|
+
route_args_text = ", ".join(route_var_names)
|
|
496
|
+
|
|
497
|
+
intro_line, code_lines, co = rewrite_me(f"{method.class_name}.{method.method_name}", f"Custom resource method: {method.class_name}.{method.method_name}()")
|
|
498
|
+
print_line(intro_line)
|
|
499
|
+
print_line(f" {co}@{config.api_class_name}.resource_method(\"{method.resource_path}\", http_method=\"{method.http_method}\"{resource_method_args_str})")
|
|
500
|
+
print_line(f" {co}def {method.method_name}(self): pass")
|
|
501
|
+
print_line(code_lines)
|
|
502
|
+
print_line(f" # To call: {config.api_class_name}.resource(\"{method.resource_name}\").{method.method_name}({route_args_text})")
|
|
503
|
+
print_line(f" # Request: {method.http_method} {config.api_url}{method.api_path}")
|
|
504
|
+
print_line()
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def print_guide(method):
|
|
508
|
+
def _multiline_trim(ml, n):
|
|
509
|
+
_ml = ml
|
|
510
|
+
_ml = re.sub(r"\n+$", "", _ml)
|
|
511
|
+
_ml = re.sub(r"\n", "\n #" + " " * n, _ml)
|
|
512
|
+
_ml = re.sub(r" +\n", "\n", _ml)
|
|
513
|
+
return _ml
|
|
514
|
+
|
|
515
|
+
def _deref(data: dict, oas_spec: dict):
|
|
516
|
+
if type(data) == dict:
|
|
517
|
+
if len(data) != 1 or list(data.keys())[0] != "$ref":
|
|
518
|
+
return {_: _deref(data[_], oas_spec) for _ in data}
|
|
519
|
+
else:
|
|
520
|
+
ref = data[list(data.keys())[0]]
|
|
521
|
+
ref = re.sub(r"^#", "$", ref)
|
|
522
|
+
ref = re.sub(r"/", ".", ref)
|
|
523
|
+
return _deref(MagicO(oas_spec)[ref], oas_spec)
|
|
524
|
+
elif type(data) == list:
|
|
525
|
+
return [_deref(_, oas_spec) for _ in data]
|
|
526
|
+
else:
|
|
527
|
+
return data
|
|
528
|
+
|
|
529
|
+
if "summary" in guide_attrs:
|
|
530
|
+
try:
|
|
531
|
+
summary = None
|
|
532
|
+
if method.summary:
|
|
533
|
+
summary = f"Summary: {method.summary}"
|
|
534
|
+
summary = _multiline_trim(summary, 3)
|
|
535
|
+
except:
|
|
536
|
+
pass
|
|
537
|
+
else:
|
|
538
|
+
if summary != None:
|
|
539
|
+
print_line(f" # {summary}")
|
|
540
|
+
|
|
541
|
+
if "description" in guide_attrs:
|
|
542
|
+
try:
|
|
543
|
+
description = None
|
|
544
|
+
if method.description:
|
|
545
|
+
description = f"Description:\n{method.description}"
|
|
546
|
+
description = _multiline_trim(description, 3)
|
|
547
|
+
except:
|
|
548
|
+
pass
|
|
549
|
+
else:
|
|
550
|
+
if description != None:
|
|
551
|
+
print_line(f" # {description}")
|
|
552
|
+
|
|
553
|
+
if "parameters" in guide_attrs:
|
|
554
|
+
try:
|
|
555
|
+
parameters = None
|
|
556
|
+
if len(method.parameters) > 0 and method.parameters != [{}]:
|
|
557
|
+
parameters = method.parameters.copy()
|
|
558
|
+
if "ref" in guide_attrs:
|
|
559
|
+
parameters = _deref(parameters, oas_spec)
|
|
560
|
+
parameters = "Parameters:\n" + re.sub(r"^-", " ", yaml.dump(parameters, indent=2, sort_keys=False))
|
|
561
|
+
parameters = _multiline_trim(parameters, 1)
|
|
562
|
+
except:
|
|
563
|
+
pass
|
|
564
|
+
else:
|
|
565
|
+
if parameters != None:
|
|
566
|
+
print_line(f" #")
|
|
567
|
+
print_line(f" # {parameters}")
|
|
568
|
+
|
|
569
|
+
if "request_body" in guide_attrs:
|
|
570
|
+
try:
|
|
571
|
+
rb_desc = None
|
|
572
|
+
rb_content = None
|
|
573
|
+
if method.request_body != {}:
|
|
574
|
+
if "description" in method.request_body:
|
|
575
|
+
rb_desc = " description: " + method.request_body['description']
|
|
576
|
+
rb_desc = _multiline_trim(rb_desc, 5)
|
|
577
|
+
if "content" in method.request_body:
|
|
578
|
+
rb_content = method.request_body['content'].copy()
|
|
579
|
+
if "ref" in guide_attrs:
|
|
580
|
+
rb_content = _deref(rb_content, oas_spec)
|
|
581
|
+
rb_content = " content:\n" + yaml.dump(rb_content, indent=2, sort_keys=False)
|
|
582
|
+
rb_content = _multiline_trim(rb_content, 5)
|
|
583
|
+
except:
|
|
584
|
+
pass
|
|
585
|
+
else:
|
|
586
|
+
if rb_desc != None or rb_content != None:
|
|
587
|
+
print_line(f" #")
|
|
588
|
+
print_line(f" # Request Body:")
|
|
589
|
+
if rb_desc != None:
|
|
590
|
+
print_line(f" # {rb_desc}")
|
|
591
|
+
if rb_content != None:
|
|
592
|
+
print_line(f" # {rb_content}")
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def gen_resource_classes(config, methods, classes):
|
|
596
|
+
resource_class_args_str = ""
|
|
597
|
+
try:
|
|
598
|
+
for resource_class_arg in config.resource_class_args:
|
|
599
|
+
val = config.resource_class_args[resource_class_arg]
|
|
600
|
+
if type(val) == str:
|
|
601
|
+
val = f'"{val}"'
|
|
602
|
+
resource_class_args_str += f", {resource_class_arg}={val}"
|
|
603
|
+
except:
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
resource_method_args_str = ""
|
|
607
|
+
try:
|
|
608
|
+
for resource_method_arg in config.resource_method_args:
|
|
609
|
+
val = config.resource_method_args[resource_method_arg]
|
|
610
|
+
if type(val) == str:
|
|
611
|
+
val = f'"{val}"'
|
|
612
|
+
resource_method_args_str += f", {resource_method_arg}={val}"
|
|
613
|
+
except:
|
|
614
|
+
pass
|
|
615
|
+
|
|
616
|
+
# `resource_class` - change of first path item
|
|
617
|
+
# `resource_method` - on every path, method being operation_id
|
|
618
|
+
last_class_name = None
|
|
619
|
+
num_methods = 0
|
|
620
|
+
for method in methods:
|
|
621
|
+
this_class_name = method.class_name
|
|
622
|
+
if this_class_name != last_class_name:
|
|
623
|
+
if last_class_name != None:
|
|
624
|
+
# New custom methods of the class
|
|
625
|
+
if snippets[last_class_name]:
|
|
626
|
+
# `snippets` can be changed in rewrite_me()
|
|
627
|
+
for new_method in snippets[last_class_name].copy():
|
|
628
|
+
intro_line, code_lines, co = rewrite_me(f"{last_class_name}.{new_method}", f"Custom new resource method: {last_class_name}.{new_method}()")
|
|
629
|
+
print_line(intro_line)
|
|
630
|
+
print_line(code_lines)
|
|
631
|
+
num_methods += 1
|
|
632
|
+
if num_methods == 0:
|
|
633
|
+
print_line(" pass")
|
|
634
|
+
print_line()
|
|
635
|
+
last_class_name = this_class_name
|
|
636
|
+
num_methods = 0
|
|
637
|
+
print_class(config, method, classes, resource_class_args_str)
|
|
638
|
+
num_methods += 1
|
|
639
|
+
print_method(config, method, resource_method_args_str)
|
|
640
|
+
print_guide(method)
|
|
641
|
+
# print_line()
|
|
642
|
+
|
|
643
|
+
if last_class_name and num_methods == 0:
|
|
644
|
+
print_line(" pass")
|
|
645
|
+
|
|
646
|
+
# Handle left-over snippet classes
|
|
647
|
+
snippets_flats = []
|
|
648
|
+
snippets_dict = snippets.to_dict()
|
|
649
|
+
for snippet_class in snippets_dict:
|
|
650
|
+
for snippet_code in snippets_dict[snippet_class]:
|
|
651
|
+
snippets_flats.append(f"{snippet_class}.{snippet_code}")
|
|
652
|
+
# print(snippets_flats)
|
|
653
|
+
|
|
654
|
+
if len(snippets_flats):
|
|
655
|
+
print_line()
|
|
656
|
+
print_line()
|
|
657
|
+
print_line("# Custom new resource classes")
|
|
658
|
+
print_line()
|
|
659
|
+
for snippets_name in snippets_flats:
|
|
660
|
+
print_line("\n".join(code_rewrite_lines[slice(*snippets[snippets_name])]))
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def main():
|
|
664
|
+
global debug
|
|
665
|
+
global output_py
|
|
666
|
+
global oas_spec
|
|
667
|
+
global guide_attrs
|
|
668
|
+
global config_file
|
|
669
|
+
|
|
670
|
+
############################################################
|
|
671
|
+
#
|
|
672
|
+
# Arguments and configuration
|
|
673
|
+
#
|
|
674
|
+
|
|
675
|
+
# Getopt - cannot load config for defaults until after getopt
|
|
676
|
+
try:
|
|
677
|
+
opts, args = getopt(sys.argv[1:], "ho:g:td", ["help", "output=", "guide=", "template", "debug"])
|
|
678
|
+
except GetoptError as err:
|
|
679
|
+
print_error(err)
|
|
680
|
+
usage(2)
|
|
681
|
+
|
|
682
|
+
for _o, _a in opts:
|
|
683
|
+
if _o in ("-h", "--help"):
|
|
684
|
+
usage()
|
|
685
|
+
elif _o in ("-o", "--output"):
|
|
686
|
+
if _a == "-":
|
|
687
|
+
output_py = sys.stdout
|
|
688
|
+
else:
|
|
689
|
+
output_py_name = _a
|
|
690
|
+
output_py = open(_a, "w")
|
|
691
|
+
elif _o in ("-g", "--guide"):
|
|
692
|
+
guide_attrs = set(_a.split(","))
|
|
693
|
+
unknown_specs = []
|
|
694
|
+
for _ in guide_attrs:
|
|
695
|
+
if _ not in valid_guide_attrs:
|
|
696
|
+
unknown_specs.append(_)
|
|
697
|
+
if unknown_specs != []:
|
|
698
|
+
print_error(f"Unknown guide options: {', '.join(unknown_specs)}")
|
|
699
|
+
usage(2)
|
|
700
|
+
if "all" in guide_attrs:
|
|
701
|
+
# Cannot copy over guide_attrs as "ref" may be in
|
|
702
|
+
guide_attrs = guide_attrs.union(all_guide_attrs)
|
|
703
|
+
guide_attrs.remove("all")
|
|
704
|
+
elif _o in ("-t", "--template"):
|
|
705
|
+
import inspect
|
|
706
|
+
import autogen_template
|
|
707
|
+
print(inspect.getsource(autogen_template), end='')
|
|
708
|
+
exit(0)
|
|
709
|
+
elif _o in ("-d", "--debug"):
|
|
710
|
+
debug = True
|
|
711
|
+
else:
|
|
712
|
+
print_error("Invalid option: {_o}")
|
|
713
|
+
usage(2)
|
|
714
|
+
|
|
715
|
+
if len(args) < 1 or len(args) > 2:
|
|
716
|
+
usage(1)
|
|
717
|
+
|
|
718
|
+
# Load config file
|
|
719
|
+
config_file = args[0]
|
|
720
|
+
config_dirname = os.path.dirname(config_file)
|
|
721
|
+
config_module_name = re.sub(r"\.py$", "", os.path.basename(config_file))
|
|
722
|
+
sys.path.append(config_dirname)
|
|
723
|
+
import importlib
|
|
724
|
+
config = importlib.import_module(config_module_name)
|
|
725
|
+
|
|
726
|
+
# Process defaults
|
|
727
|
+
if output_py == None:
|
|
728
|
+
try:
|
|
729
|
+
output_py_name = config.output_py_name
|
|
730
|
+
if output_py_name[0] == "." and config_dirname:
|
|
731
|
+
# Relative path
|
|
732
|
+
output_py_name = f"{config_dirname}/{output_py_name}"
|
|
733
|
+
output_py = open(output_py_name, "w")
|
|
734
|
+
except:
|
|
735
|
+
output_py = sys.stdout
|
|
736
|
+
|
|
737
|
+
if guide_attrs == None:
|
|
738
|
+
try:
|
|
739
|
+
guide_attrs = config.guide_attrs
|
|
740
|
+
except:
|
|
741
|
+
guide_attrs = set()
|
|
742
|
+
|
|
743
|
+
# Process OAS file
|
|
744
|
+
oas_file_name = f"{config_module_name}.json" # Default OpenAI definition
|
|
745
|
+
if len(args) >= 2:
|
|
746
|
+
# openapi.json specified
|
|
747
|
+
oas_file_name = args[0]
|
|
748
|
+
else:
|
|
749
|
+
try:
|
|
750
|
+
# Use config.oas if defined
|
|
751
|
+
oas_file_name = config.oas_file_name
|
|
752
|
+
if oas_file_name[0] == "." and config_dirname:
|
|
753
|
+
# Relative path
|
|
754
|
+
oas_file_name = f"{config_dirname}/{oas_file_name}"
|
|
755
|
+
except:
|
|
756
|
+
# Use default
|
|
757
|
+
pass
|
|
758
|
+
|
|
759
|
+
# Process Code Rewrite file
|
|
760
|
+
code_rewrite_file_name = ""
|
|
761
|
+
try:
|
|
762
|
+
# Use config.oas if defined
|
|
763
|
+
code_rewrite_file_name = config.code_rewrite_file_name
|
|
764
|
+
if code_rewrite_file_name[0] == "." and config_dirname:
|
|
765
|
+
# Relative path
|
|
766
|
+
code_rewrite_file_name = f"{code_rewrite_file_name}/{oas_file_name}"
|
|
767
|
+
except:
|
|
768
|
+
# Use default
|
|
769
|
+
pass
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
############################################################
|
|
773
|
+
#
|
|
774
|
+
# Generate the API SDK
|
|
775
|
+
#
|
|
776
|
+
|
|
777
|
+
oas_spec = get_oas_spec(oas_file_name)
|
|
778
|
+
set_config(config, oas_spec)
|
|
779
|
+
get_code_rewrite(config, code_rewrite_file_name)
|
|
780
|
+
methods = get_methods(config, oas_spec["paths"])
|
|
781
|
+
classes = gen_header(config, methods)
|
|
782
|
+
gen_api_class(config)
|
|
783
|
+
gen_resource_classes(config, methods, classes)
|
|
784
|
+
|
|
785
|
+
if output_py_name:
|
|
786
|
+
code_rewrite_text = f", merged with {code_rewrite_file_name}," if code_rewrite_file_name else ""
|
|
787
|
+
print(f"{config.api_class_name} generated{code_rewrite_text} and saved in {output_py_name}")
|
|
788
|
+
|
|
789
|
+
if __name__ == "__main__":
|
|
790
|
+
main()
|