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/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()