codecraftai-restclient-gitlab 1.2.0__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.
- ccai/gitlab/py.typed +0 -0
- ccai/gitlab/rest/aspectsrest.py +120 -0
- ccai/gitlab/rest/clients/__init__.py +0 -0
- ccai/gitlab/rest/clients/gitlabrestclient.py +118 -0
- ccai/gitlab/rest/clients/groups/__init__.py +0 -0
- ccai/gitlab/rest/clients/groups/accessrequests.py +230 -0
- ccai/gitlab/rest/clients/groups/admin.py +41 -0
- ccai/gitlab/rest/clients/groups/alertmanagement.py +149 -0
- ccai/gitlab/rest/clients/groups/application.py +68 -0
- ccai/gitlab/rest/clients/groups/applications.py +95 -0
- ccai/gitlab/rest/clients/groups/avatar.py +41 -0
- ccai/gitlab/rest/clients/groups/badges.py +338 -0
- ccai/gitlab/rest/clients/groups/batchedbackgroundmigrations.py +122 -0
- ccai/gitlab/rest/clients/groups/branches.py +230 -0
- ccai/gitlab/rest/clients/groups/broadcastmessages.py +149 -0
- ccai/gitlab/rest/clients/groups/bulkimports.py +176 -0
- ccai/gitlab/rest/clients/groups/clusters.py +149 -0
- ccai/gitlab/rest/clients/groups/jobs.py +95 -0
- ccai/gitlab/rest/clients/groups/metadata.py +68 -0
- ccai/gitlab/rest/clients/groups/migrations.py +41 -0
- ccai/gitlab/rest/clients/groups/pipelinecomposition.py +146 -0
- ccai/gitlab/rest/clients/groups/planlimits.py +68 -0
- ccai/gitlab/rest/clients/parameters/__init__.py +0 -0
- ccai/gitlab/rest/clients/parameters/accessrequests.py +222 -0
- ccai/gitlab/rest/clients/parameters/admin.py +31 -0
- ccai/gitlab/rest/clients/parameters/alertmanagement.py +147 -0
- ccai/gitlab/rest/clients/parameters/application.py +27 -0
- ccai/gitlab/rest/clients/parameters/applications.py +51 -0
- ccai/gitlab/rest/clients/parameters/avatar.py +31 -0
- ccai/gitlab/rest/clients/parameters/badges.py +344 -0
- ccai/gitlab/rest/clients/parameters/batchedbackgroundmigrations.py +105 -0
- ccai/gitlab/rest/clients/parameters/branches.py +236 -0
- ccai/gitlab/rest/clients/parameters/broadcastmessages.py +123 -0
- ccai/gitlab/rest/clients/parameters/bulkimports.py +173 -0
- ccai/gitlab/rest/clients/parameters/clusters.py +100 -0
- ccai/gitlab/rest/clients/parameters/jobs.py +88 -0
- ccai/gitlab/rest/clients/parameters/metadata.py +11 -0
- ccai/gitlab/rest/clients/parameters/migrations.py +29 -0
- ccai/gitlab/rest/clients/parameters/pipelinecomposition.py +123 -0
- ccai/gitlab/rest/clients/parameters/planlimits.py +47 -0
- ccai/gitlab/rest/clients/responses/__init__.py +0 -0
- ccai/gitlab/rest/clients/responses/accessrequests.py +157 -0
- ccai/gitlab/rest/clients/responses/admin.py +24 -0
- ccai/gitlab/rest/clients/responses/alertmanagement.py +100 -0
- ccai/gitlab/rest/clients/responses/application.py +43 -0
- ccai/gitlab/rest/clients/responses/applications.py +62 -0
- ccai/gitlab/rest/clients/responses/avatar.py +24 -0
- ccai/gitlab/rest/clients/responses/badges.py +233 -0
- ccai/gitlab/rest/clients/responses/batchedbackgroundmigrations.py +81 -0
- ccai/gitlab/rest/clients/responses/branches.py +157 -0
- ccai/gitlab/rest/clients/responses/broadcastmessages.py +100 -0
- ccai/gitlab/rest/clients/responses/bulkimports.py +119 -0
- ccai/gitlab/rest/clients/responses/clusters.py +100 -0
- ccai/gitlab/rest/clients/responses/jobs.py +62 -0
- ccai/gitlab/rest/clients/responses/metadata.py +43 -0
- ccai/gitlab/rest/clients/responses/migrations.py +24 -0
- ccai/gitlab/rest/clients/responses/pipelinecomposition.py +100 -0
- ccai/gitlab/rest/clients/responses/planlimits.py +43 -0
- ccai/gitlab/rest/descriptions/gitlab-openapi-hints.yaml +195 -0
- ccai/gitlab/rest/descriptions/gitlab-openapi-spec.yaml +3635 -0
- ccai/gitlab/rest/generation/__init__.py +0 -0
- ccai/gitlab/rest/generation/restclientgenerator.py +581 -0
- ccai/gitlab/rest/itemsresult.py +274 -0
- ccai/gitlab/rest/restapigroup.py +17 -0
- ccai/gitlab/rest/restclientbase.py +676 -0
- ccai/gitlab/rest/restclientutils.py +0 -0
- codecraftai_restclient_gitlab-1.2.0.dist-info/METADATA +90 -0
- codecraftai_restclient_gitlab-1.2.0.dist-info/RECORD +70 -0
- codecraftai_restclient_gitlab-1.2.0.dist-info/WHEEL +4 -0
- codecraftai_restclient_gitlab-1.2.0.dist-info/licenses/LICENSE.md +204 -0
|
File without changes
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.. module:: restclientgenerator
|
|
3
|
+
:platform: Darwin, Linux, Unix, Windows
|
|
4
|
+
:synopsis: Generates REST client source from the GitHub OpenAPI description.
|
|
5
|
+
|
|
6
|
+
.. moduleauthor:: Myron Walker <myron.walker@gmail.com>
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import ast
|
|
16
|
+
import json
|
|
17
|
+
import keyword
|
|
18
|
+
import re
|
|
19
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import yaml
|
|
23
|
+
except ImportError:
|
|
24
|
+
yaml = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
AUTO_GENERATED_BANNER = "# Auto-generated by RestClientGenerator. DO NOT EDIT.\n"
|
|
28
|
+
|
|
29
|
+
SUPPORTED_HTTP_METHODS = {"get", "post", "put", "delete", "patch", "head", "options"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ParameterSpec:
|
|
34
|
+
"""
|
|
35
|
+
Specification for a parameter from the OpenAPI description.
|
|
36
|
+
"""
|
|
37
|
+
name: str
|
|
38
|
+
location: str
|
|
39
|
+
required: bool
|
|
40
|
+
schema: Dict[str, Any]
|
|
41
|
+
description: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class OperationSpec:
|
|
46
|
+
"""
|
|
47
|
+
Container holding details about an API operation.
|
|
48
|
+
"""
|
|
49
|
+
path: str
|
|
50
|
+
http_method: str
|
|
51
|
+
group: str
|
|
52
|
+
operation_id: str
|
|
53
|
+
method_name: str
|
|
54
|
+
summary: str
|
|
55
|
+
description: str
|
|
56
|
+
parameters: List[ParameterSpec]
|
|
57
|
+
request_body: Optional[Dict[str, Any]]
|
|
58
|
+
responses: Dict[str, Any]
|
|
59
|
+
external_docs: Optional[Dict[str, Any]] = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class RestClientGenerator:
|
|
63
|
+
"""
|
|
64
|
+
Shapes the patterns for the REST API generation.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, openapi_spec_file: str, clients_root: str, client_class_name: str = "GithubRestClient"):
|
|
68
|
+
self._spec_path = Path(openapi_spec_file).resolve()
|
|
69
|
+
self._clients_root = Path(clients_root).resolve()
|
|
70
|
+
self._client_class_name = client_class_name
|
|
71
|
+
self._spec = self._load_spec(self._spec_path)
|
|
72
|
+
|
|
73
|
+
self._components = self._spec.get("components", {})
|
|
74
|
+
self._tag_catalog: Dict[str, str] = {}
|
|
75
|
+
self._groups: Dict[str, List[OperationSpec]] = {}
|
|
76
|
+
|
|
77
|
+
self._process_spec()
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
def generate(self):
|
|
81
|
+
"""
|
|
82
|
+
Generate all client, group, parameter, and response modules.
|
|
83
|
+
"""
|
|
84
|
+
clients_root = self._clients_root
|
|
85
|
+
groups_dir = clients_root / "groups"
|
|
86
|
+
params_dir = clients_root / "parameters"
|
|
87
|
+
responses_dir = clients_root / "responses"
|
|
88
|
+
|
|
89
|
+
for directory in (groups_dir, params_dir, responses_dir):
|
|
90
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
|
|
92
|
+
sorted_groups = sorted(self._groups.keys())
|
|
93
|
+
|
|
94
|
+
for group in sorted_groups:
|
|
95
|
+
ops = sorted(self._groups[group], key=lambda o: o.method_name)
|
|
96
|
+
self._generate_parameter_module(group, ops, params_dir)
|
|
97
|
+
self._generate_response_module(group, ops, responses_dir)
|
|
98
|
+
self._generate_group_module(group, ops, groups_dir)
|
|
99
|
+
|
|
100
|
+
self._generate_client_module(sorted_groups, clients_root)
|
|
101
|
+
self._write_simple_init(groups_dir)
|
|
102
|
+
self._write_simple_init(params_dir)
|
|
103
|
+
self._write_simple_init(responses_dir)
|
|
104
|
+
self._write_simple_init(clients_root)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
def _load_spec(self, spec_path: Path) -> Dict[str, Any]:
|
|
108
|
+
"""
|
|
109
|
+
Load the OpenAPI specification from JSON or YAML.
|
|
110
|
+
"""
|
|
111
|
+
suffix = spec_path.suffix.lower()
|
|
112
|
+
with spec_path.open("r", encoding="utf-8") as sf:
|
|
113
|
+
if suffix in (".yaml", ".yml"):
|
|
114
|
+
if yaml is None:
|
|
115
|
+
errmsg = "PyYAML is required to load YAML specifications. Install with 'pip install pyyaml'."
|
|
116
|
+
raise ImportError(errmsg)
|
|
117
|
+
return yaml.safe_load(sf)
|
|
118
|
+
return json.load(sf)
|
|
119
|
+
|
|
120
|
+
def _process_spec(self):
|
|
121
|
+
spec = self._spec
|
|
122
|
+
|
|
123
|
+
for item in spec.get("tags", []):
|
|
124
|
+
name = item["name"]
|
|
125
|
+
desc = item.get("description", "")
|
|
126
|
+
self._tag_catalog[self._safe_snake(name)] = desc
|
|
127
|
+
|
|
128
|
+
path_catalog: Dict[str, dict] = spec["paths"]
|
|
129
|
+
for path_leaf, method_catalog in path_catalog.items():
|
|
130
|
+
path_level_params = method_catalog.get("parameters", [])
|
|
131
|
+
for call_method, op_desc in method_catalog.items():
|
|
132
|
+
if call_method not in SUPPORTED_HTTP_METHODS:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
operation_id = op_desc.get("operationId", "")
|
|
136
|
+
tags = op_desc.get("tags", [])
|
|
137
|
+
summary = op_desc.get("summary", "")
|
|
138
|
+
description = op_desc.get("description", "")
|
|
139
|
+
external_docs = op_desc.get("externalDocs")
|
|
140
|
+
responses = op_desc.get("responses", {})
|
|
141
|
+
request_body = op_desc.get("requestBody")
|
|
142
|
+
|
|
143
|
+
group_name, method_name = self._derive_group_and_method(operation_id, tags)
|
|
144
|
+
|
|
145
|
+
parameters = self._collect_parameters(
|
|
146
|
+
path_level_params, op_desc.get("parameters", [])
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
op = OperationSpec(
|
|
150
|
+
path=path_leaf,
|
|
151
|
+
http_method=call_method.upper(),
|
|
152
|
+
group=group_name,
|
|
153
|
+
operation_id=operation_id,
|
|
154
|
+
method_name=method_name,
|
|
155
|
+
summary=summary,
|
|
156
|
+
description=description,
|
|
157
|
+
parameters=parameters,
|
|
158
|
+
request_body=request_body,
|
|
159
|
+
responses=responses,
|
|
160
|
+
external_docs=external_docs,
|
|
161
|
+
)
|
|
162
|
+
self._groups.setdefault(group_name, []).append(op)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
def _collect_parameters(self, path_level_params: List[dict], op_level_params: List[dict]) -> List[ParameterSpec]:
|
|
166
|
+
parameters: List[ParameterSpec] = []
|
|
167
|
+
for pentry in path_level_params + op_level_params:
|
|
168
|
+
param_def = self._dereference_parameter(pentry)
|
|
169
|
+
name = param_def.get("name", "unnamed")
|
|
170
|
+
location = param_def.get("in", "query")
|
|
171
|
+
required = bool(param_def.get("required", False))
|
|
172
|
+
schema = param_def.get("schema", {})
|
|
173
|
+
description = param_def.get("description", "")
|
|
174
|
+
parameters.append(
|
|
175
|
+
ParameterSpec(
|
|
176
|
+
name=name,
|
|
177
|
+
location=location,
|
|
178
|
+
required=required,
|
|
179
|
+
schema=schema,
|
|
180
|
+
description=description,
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
return parameters
|
|
184
|
+
|
|
185
|
+
def _dereference_parameter(self, param_entry: dict) -> dict:
|
|
186
|
+
if "$ref" in param_entry:
|
|
187
|
+
ref_path = param_entry["$ref"]
|
|
188
|
+
resolved = self._resolve_ref(ref_path)
|
|
189
|
+
if not isinstance(resolved, dict):
|
|
190
|
+
errmsg = f"Reference '{ref_path}' did not resolve to a parameter dictionary."
|
|
191
|
+
raise ValueError(errmsg)
|
|
192
|
+
return resolved
|
|
193
|
+
return param_entry
|
|
194
|
+
|
|
195
|
+
def _resolve_ref(self, ref_path: str):
|
|
196
|
+
if not ref_path.startswith("#/"):
|
|
197
|
+
errmsg = f"Unsupported reference format '{ref_path}'."
|
|
198
|
+
raise ValueError(errmsg)
|
|
199
|
+
|
|
200
|
+
segments = ref_path.lstrip("#/").split("/")
|
|
201
|
+
obj: Any = self._spec
|
|
202
|
+
for segment in segments:
|
|
203
|
+
obj = obj[segment]
|
|
204
|
+
return obj
|
|
205
|
+
|
|
206
|
+
def _derive_group_and_method(self, operation_id: str, tags: List[str]) -> Tuple[str, str]:
|
|
207
|
+
group_name = tags[0] if tags else "default"
|
|
208
|
+
method_name = operation_id
|
|
209
|
+
|
|
210
|
+
if operation_id and "/" in operation_id:
|
|
211
|
+
parts = operation_id.split("/", 1)
|
|
212
|
+
group_name = parts[0]
|
|
213
|
+
method_name = parts[1]
|
|
214
|
+
elif not tags and operation_id:
|
|
215
|
+
group_name = operation_id
|
|
216
|
+
method_name = operation_id
|
|
217
|
+
|
|
218
|
+
group_name = self._safe_snake(group_name)
|
|
219
|
+
method_name = self._safe_snake(method_name)
|
|
220
|
+
return group_name, method_name
|
|
221
|
+
|
|
222
|
+
def _safe_snake(self, name: str) -> str:
|
|
223
|
+
snake = re.sub(r"[^0-9a-zA-Z]+", "_", name).strip("_").lower()
|
|
224
|
+
if snake == "" or snake[0].isdigit() or keyword.iskeyword(snake):
|
|
225
|
+
snake = f"op_{snake}"
|
|
226
|
+
return snake
|
|
227
|
+
|
|
228
|
+
def _safe_pascal(self, name: str) -> str:
|
|
229
|
+
parts = re.split(r"[^0-9a-zA-Z]+", name)
|
|
230
|
+
pascal = "".join([p.capitalize() for p in parts if p])
|
|
231
|
+
if pascal == "":
|
|
232
|
+
pascal = "Generated"
|
|
233
|
+
if pascal[0].isdigit():
|
|
234
|
+
pascal = f"Op{pascal}"
|
|
235
|
+
return pascal
|
|
236
|
+
|
|
237
|
+
def _file_slug(self, name: str) -> str:
|
|
238
|
+
"""
|
|
239
|
+
Creates a filesystem-friendly slug without underscores.
|
|
240
|
+
"""
|
|
241
|
+
slug = self._safe_snake(name).replace("_", "")
|
|
242
|
+
if slug == "":
|
|
243
|
+
slug = "generated"
|
|
244
|
+
if slug[0].isdigit():
|
|
245
|
+
slug = f"op{slug}"
|
|
246
|
+
return slug
|
|
247
|
+
|
|
248
|
+
def _field_default_literal(self, schema: Dict[str, Any], required: bool) -> str:
|
|
249
|
+
if required:
|
|
250
|
+
return ""
|
|
251
|
+
|
|
252
|
+
if "default" in schema:
|
|
253
|
+
dval = schema["default"]
|
|
254
|
+
if isinstance(dval, str):
|
|
255
|
+
return f'"{dval}"'
|
|
256
|
+
return str(dval)
|
|
257
|
+
|
|
258
|
+
return "None"
|
|
259
|
+
|
|
260
|
+
def _schema_to_type(self, schema: Dict[str, Any]) -> str:
|
|
261
|
+
if "$ref" in schema:
|
|
262
|
+
return "Any"
|
|
263
|
+
|
|
264
|
+
schema_type = schema.get("type", "any")
|
|
265
|
+
if schema_type == "integer":
|
|
266
|
+
return "int"
|
|
267
|
+
if schema_type == "number":
|
|
268
|
+
return "float"
|
|
269
|
+
if schema_type == "boolean":
|
|
270
|
+
return "bool"
|
|
271
|
+
if schema_type == "array":
|
|
272
|
+
item_schema = schema.get("items", {})
|
|
273
|
+
item_type = self._schema_to_type(item_schema)
|
|
274
|
+
return f"List[{item_type}]"
|
|
275
|
+
if schema_type == "object":
|
|
276
|
+
return "Dict[str, Any]"
|
|
277
|
+
return "str"
|
|
278
|
+
|
|
279
|
+
def _expected_status_codes(self, responses: Dict[str, Any]) -> List[int]:
|
|
280
|
+
expected: List[int] = []
|
|
281
|
+
|
|
282
|
+
for code_val in sorted(responses.keys()):
|
|
283
|
+
code_int = None
|
|
284
|
+
if isinstance(code_val, str) and code_val.isdigit():
|
|
285
|
+
code_int = int(code_val)
|
|
286
|
+
if isinstance(code_val, int):
|
|
287
|
+
code_int = code_val
|
|
288
|
+
if code_int is not None:
|
|
289
|
+
if 200 <= code_int < 300:
|
|
290
|
+
expected.append(code_int)
|
|
291
|
+
|
|
292
|
+
if not expected:
|
|
293
|
+
expected.append(200)
|
|
294
|
+
|
|
295
|
+
return expected
|
|
296
|
+
|
|
297
|
+
def _generate_parameter_module(self, group: str, ops: List[OperationSpec], params_dir: Path):
|
|
298
|
+
slug = self._file_slug(group)
|
|
299
|
+
|
|
300
|
+
# Generate the dataclasses first so the typing import can be limited to the names they reference.
|
|
301
|
+
module_lines: List[str] = []
|
|
302
|
+
|
|
303
|
+
for op in ops:
|
|
304
|
+
class_name = f"{self._safe_pascal(group)}{self._safe_pascal(op.method_name)}Params"
|
|
305
|
+
module_lines.append(f"@dataclass")
|
|
306
|
+
module_lines.append(f"class {class_name}:")
|
|
307
|
+
if op.parameters:
|
|
308
|
+
module_lines.append(f" \"\"\"")
|
|
309
|
+
module_lines.append(f" Parameters for the '{op.operation_id}' operation.")
|
|
310
|
+
module_lines.append(f" \"\"\"")
|
|
311
|
+
required_fields = [p for p in op.parameters if p.required]
|
|
312
|
+
optional_fields = [p for p in op.parameters if not p.required]
|
|
313
|
+
|
|
314
|
+
for param in required_fields + optional_fields:
|
|
315
|
+
attr_name = self._safe_snake(param.name)
|
|
316
|
+
ptype = self._schema_to_type(param.schema)
|
|
317
|
+
if not param.required:
|
|
318
|
+
ptype = f"Optional[{ptype}]"
|
|
319
|
+
default_lit = self._field_default_literal(param.schema, param.required)
|
|
320
|
+
if default_lit:
|
|
321
|
+
module_lines.append(f" {attr_name}: {ptype} = {default_lit}")
|
|
322
|
+
else:
|
|
323
|
+
module_lines.append(f" {attr_name}: {ptype}")
|
|
324
|
+
|
|
325
|
+
if op.request_body is not None:
|
|
326
|
+
module_lines.append(" body: Optional[Any] = None")
|
|
327
|
+
|
|
328
|
+
if not op.parameters and op.request_body is None:
|
|
329
|
+
module_lines.append(" pass")
|
|
330
|
+
else:
|
|
331
|
+
module_lines.append("")
|
|
332
|
+
module_lines.append(" def path_params(self) -> Dict[str, Any]:")
|
|
333
|
+
module_lines.append(" params: Dict[str, Any] = {}")
|
|
334
|
+
for param in op.parameters:
|
|
335
|
+
if param.location == "path":
|
|
336
|
+
attr_name = self._safe_snake(param.name)
|
|
337
|
+
module_lines.append(f" if self.{attr_name} is not None:")
|
|
338
|
+
module_lines.append(f" params['{param.name}'] = self.{attr_name}")
|
|
339
|
+
module_lines.append(" return params")
|
|
340
|
+
module_lines.append("")
|
|
341
|
+
module_lines.append(" def query_params(self) -> Dict[str, Any]:")
|
|
342
|
+
module_lines.append(" params: Dict[str, Any] = {}")
|
|
343
|
+
for param in op.parameters:
|
|
344
|
+
if param.location == "query":
|
|
345
|
+
attr_name = self._safe_snake(param.name)
|
|
346
|
+
module_lines.append(f" if self.{attr_name} is not None:")
|
|
347
|
+
module_lines.append(f" params['{param.name}'] = self.{attr_name}")
|
|
348
|
+
module_lines.append(" return params")
|
|
349
|
+
module_lines.append("")
|
|
350
|
+
module_lines.append(" def header_params(self) -> Dict[str, Any]:")
|
|
351
|
+
module_lines.append(" params: Dict[str, Any] = {}")
|
|
352
|
+
for param in op.parameters:
|
|
353
|
+
if param.location == "header":
|
|
354
|
+
attr_name = self._safe_snake(param.name)
|
|
355
|
+
module_lines.append(f" if self.{attr_name} is not None:")
|
|
356
|
+
module_lines.append(f" params['{param.name}'] = self.{attr_name}")
|
|
357
|
+
module_lines.append(" return params")
|
|
358
|
+
module_lines.append("")
|
|
359
|
+
module_lines.append(" def body_params(self) -> Any:")
|
|
360
|
+
if op.request_body is not None:
|
|
361
|
+
module_lines.append(" return self.body")
|
|
362
|
+
else:
|
|
363
|
+
module_lines.append(" return None")
|
|
364
|
+
module_lines.append("")
|
|
365
|
+
|
|
366
|
+
body_text = "\n".join(module_lines)
|
|
367
|
+
header_lines: List[str] = [AUTO_GENERATED_BANNER]
|
|
368
|
+
header_lines.append("from dataclasses import dataclass")
|
|
369
|
+
typing_statement = self._typing_import_statement(["Any", "Dict", "List", "Optional"], body_text)
|
|
370
|
+
if typing_statement is not None:
|
|
371
|
+
header_lines.append(typing_statement)
|
|
372
|
+
header_lines.append("")
|
|
373
|
+
module_lines = header_lines + module_lines
|
|
374
|
+
|
|
375
|
+
content = "\n".join(module_lines).rstrip() + "\n"
|
|
376
|
+
out_path = params_dir / f"{slug}.py"
|
|
377
|
+
self._write_file(out_path, content)
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
def _generate_response_module(self, group: str, ops: List[OperationSpec], responses_dir: Path):
|
|
381
|
+
slug = self._file_slug(group)
|
|
382
|
+
module_lines: List[str] = [AUTO_GENERATED_BANNER]
|
|
383
|
+
module_lines.append("from typing import Any, Dict")
|
|
384
|
+
module_lines.append("")
|
|
385
|
+
module_lines.append("from requests import Response")
|
|
386
|
+
module_lines.append("")
|
|
387
|
+
|
|
388
|
+
for op in ops:
|
|
389
|
+
class_name = f"{self._safe_pascal(group)}{self._safe_pascal(op.method_name)}Response"
|
|
390
|
+
module_lines.append(f"class {class_name}:")
|
|
391
|
+
module_lines.append(" \"\"\"")
|
|
392
|
+
module_lines.append(f" Response wrapper for the '{op.operation_id}' operation.")
|
|
393
|
+
module_lines.append(" \"\"\"")
|
|
394
|
+
module_lines.append(" def __init__(self, response: Response):")
|
|
395
|
+
module_lines.append(" self.response = response")
|
|
396
|
+
module_lines.append(" self.status_code = response.status_code")
|
|
397
|
+
module_lines.append(" self.headers: Dict[str, Any] = dict(response.headers)")
|
|
398
|
+
module_lines.append(" try:")
|
|
399
|
+
module_lines.append(" self.data = response.json()")
|
|
400
|
+
module_lines.append(" except Exception:")
|
|
401
|
+
module_lines.append(" self.data = response.content")
|
|
402
|
+
module_lines.append(" return")
|
|
403
|
+
module_lines.append("")
|
|
404
|
+
module_lines.append(" @property")
|
|
405
|
+
module_lines.append(" def ok(self) -> bool:")
|
|
406
|
+
module_lines.append(" rtnval = True if 200 <= self.status_code < 300 else False")
|
|
407
|
+
module_lines.append(" return rtnval")
|
|
408
|
+
module_lines.append("")
|
|
409
|
+
|
|
410
|
+
content = "\n".join(module_lines).rstrip() + "\n"
|
|
411
|
+
out_path = responses_dir / f"{slug}.py"
|
|
412
|
+
self._write_file(out_path, content)
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
def _generate_group_module(self, group: str, ops: List[OperationSpec], groups_dir: Path):
|
|
416
|
+
slug = self._file_slug(group)
|
|
417
|
+
class_name = f"{self._safe_pascal(group)}ApiGroup"
|
|
418
|
+
module_lines: List[str] = [AUTO_GENERATED_BANNER]
|
|
419
|
+
module_lines.append("from typing import Optional")
|
|
420
|
+
module_lines.append("")
|
|
421
|
+
module_lines.append("from ccai.gitlab.rest.aspectsrest import AspectsRest, DEFAULT_REST_ASPECTS")
|
|
422
|
+
module_lines.append("from ccai.gitlab.rest.restapigroup import RestApiGroup")
|
|
423
|
+
module_lines.append(f"from ccai.gitlab.rest.clients.parameters import {slug} as parameters")
|
|
424
|
+
module_lines.append(f"from ccai.gitlab.rest.clients.responses import {slug} as responses")
|
|
425
|
+
module_lines.append("")
|
|
426
|
+
module_lines.append("")
|
|
427
|
+
module_lines.append(f"class {class_name}(RestApiGroup):")
|
|
428
|
+
tag_desc = self._tag_catalog.get(group, "")
|
|
429
|
+
if tag_desc:
|
|
430
|
+
module_lines.append(" \"\"\"")
|
|
431
|
+
module_lines.append(f" {tag_desc}")
|
|
432
|
+
module_lines.append(" \"\"\"")
|
|
433
|
+
|
|
434
|
+
for op in ops:
|
|
435
|
+
method_name = self._safe_snake(op.method_name)
|
|
436
|
+
params_class = f"{self._safe_pascal(group)}{self._safe_pascal(op.method_name)}Params"
|
|
437
|
+
resp_class = f"{self._safe_pascal(group)}{self._safe_pascal(op.method_name)}Response"
|
|
438
|
+
module_lines.append("")
|
|
439
|
+
module_lines.append(f" def {method_name}(self, params: Optional[parameters.{params_class}] = None, *, aspects: Optional[AspectsRest] = DEFAULT_REST_ASPECTS) -> responses.{resp_class}:")
|
|
440
|
+
module_lines.append(" \"\"\"")
|
|
441
|
+
if op.summary:
|
|
442
|
+
module_lines.append(f" {op.summary}")
|
|
443
|
+
else:
|
|
444
|
+
module_lines.append(f" {op.description}")
|
|
445
|
+
module_lines.append(" \"\"\"")
|
|
446
|
+
module_lines.append(f" if params is None:")
|
|
447
|
+
module_lines.append(f" params = parameters.{params_class}()")
|
|
448
|
+
module_lines.append(f" call_leaf = \"{op.path}\"")
|
|
449
|
+
module_lines.append(" path_params = params.path_params()")
|
|
450
|
+
module_lines.append(" if path_params:")
|
|
451
|
+
module_lines.append(" call_leaf = self.client.format_path(call_leaf, path_params)")
|
|
452
|
+
module_lines.append(" query_params = params.query_params()")
|
|
453
|
+
module_lines.append(" header_params = params.header_params()")
|
|
454
|
+
module_lines.append(" body_params = params.body_params()")
|
|
455
|
+
exp_status = self._expected_status_codes(op.responses)
|
|
456
|
+
module_lines.append(f" exp_status = {exp_status}")
|
|
457
|
+
module_lines.append(" resp = self.client.perform_call(")
|
|
458
|
+
module_lines.append(" leaf=call_leaf,")
|
|
459
|
+
module_lines.append(f" method=\"{op.http_method}\",")
|
|
460
|
+
module_lines.append(" params=query_params if query_params else None,")
|
|
461
|
+
module_lines.append(" body=body_params,")
|
|
462
|
+
module_lines.append(" exp_status=exp_status,")
|
|
463
|
+
module_lines.append(" headers=header_params if header_params else None,")
|
|
464
|
+
module_lines.append(" context_msg=None,")
|
|
465
|
+
module_lines.append(" aspects=aspects,")
|
|
466
|
+
module_lines.append(" )")
|
|
467
|
+
module_lines.append(f" result = responses.{resp_class}(resp)")
|
|
468
|
+
module_lines.append(" return result")
|
|
469
|
+
|
|
470
|
+
content = "\n".join(module_lines).rstrip() + "\n"
|
|
471
|
+
out_path = groups_dir / f"{slug}.py"
|
|
472
|
+
self._write_file(out_path, content)
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
def _generate_client_module(self, sorted_groups: List[str], clients_root: Path):
|
|
476
|
+
client_filename = f"{self._client_class_name.lower()}.py"
|
|
477
|
+
client_file = clients_root / client_filename
|
|
478
|
+
imports: List[str] = [AUTO_GENERATED_BANNER]
|
|
479
|
+
imports.append("from typing import Optional")
|
|
480
|
+
imports.append("")
|
|
481
|
+
imports.append("from ccai.gitlab.rest.aspectsrest import AspectsRest, DEFAULT_REST_ASPECTS")
|
|
482
|
+
imports.append("from ccai.gitlab.rest.restclientbase import RestClientBase")
|
|
483
|
+
imports.append("from ccai.credentials.basecredential import BaseCredential")
|
|
484
|
+
|
|
485
|
+
for group in sorted_groups:
|
|
486
|
+
gclass = f"{self._safe_pascal(group)}ApiGroup"
|
|
487
|
+
gmodule = f"ccai.gitlab.rest.clients.groups.{self._file_slug(group)}"
|
|
488
|
+
imports.append(f"from {gmodule} import {gclass}")
|
|
489
|
+
|
|
490
|
+
imports.append("")
|
|
491
|
+
|
|
492
|
+
lines = imports
|
|
493
|
+
lines.append("")
|
|
494
|
+
lines.append(f"class {self._client_class_name}(RestClientBase):")
|
|
495
|
+
lines.append(' """')
|
|
496
|
+
lines.append(" GitHub public REST API client generated from the OpenAPI specification.")
|
|
497
|
+
lines.append(" \"\"\"")
|
|
498
|
+
lines.append(" def __init__(self, host: str, credential: BaseCredential, port: Optional[int] = None, scheme: Optional[str] = \"https\", aspects: AspectsRest = DEFAULT_REST_ASPECTS):")
|
|
499
|
+
lines.append(" super().__init__(host, credential, port, scheme=scheme, aspects=aspects)")
|
|
500
|
+
|
|
501
|
+
for group in sorted_groups:
|
|
502
|
+
gclass = f"{self._safe_pascal(group)}ApiGroup"
|
|
503
|
+
attr_name = f"_grp_{self._safe_snake(group)}"
|
|
504
|
+
lines.append(f" self.{attr_name} = {gclass}(self)")
|
|
505
|
+
lines.append(" return")
|
|
506
|
+
lines.append("")
|
|
507
|
+
|
|
508
|
+
for group in sorted_groups:
|
|
509
|
+
gclass = f"{self._safe_pascal(group)}ApiGroup"
|
|
510
|
+
attr_name = f"_grp_{self._safe_snake(group)}"
|
|
511
|
+
prop_name = self._safe_snake(group)
|
|
512
|
+
lines.append(" @property")
|
|
513
|
+
lines.append(f" def {prop_name}(self) -> {gclass}:")
|
|
514
|
+
lines.append(f" return self.{attr_name}")
|
|
515
|
+
lines.append("")
|
|
516
|
+
|
|
517
|
+
content = "\n".join(lines).rstrip() + "\n"
|
|
518
|
+
self._write_file(client_file, content)
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
def _write_simple_init(self, target_dir: Path):
|
|
522
|
+
init_file = target_dir / "__init__.py"
|
|
523
|
+
if init_file.exists():
|
|
524
|
+
return
|
|
525
|
+
init_file.write_text("", encoding="utf-8")
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
def _typing_import_statement(self, candidate_names: List[str], body_text: str) -> Optional[str]:
|
|
529
|
+
"""
|
|
530
|
+
Creates a 'from typing import ...' statement containing only the candidate names that the generated
|
|
531
|
+
module body references.
|
|
532
|
+
|
|
533
|
+
:param candidate_names: The typing names the body might use.
|
|
534
|
+
:param body_text: The generated module body.
|
|
535
|
+
|
|
536
|
+
:returns: The import statement, or None when the body references none of the candidate names.
|
|
537
|
+
"""
|
|
538
|
+
referenced_names = set()
|
|
539
|
+
tree = ast.parse(body_text)
|
|
540
|
+
for node in ast.walk(tree):
|
|
541
|
+
if isinstance(node, ast.Name):
|
|
542
|
+
referenced_names.add(node.id)
|
|
543
|
+
|
|
544
|
+
used_names = []
|
|
545
|
+
for name in candidate_names:
|
|
546
|
+
if name in referenced_names:
|
|
547
|
+
used_names.append(name)
|
|
548
|
+
|
|
549
|
+
statement = None
|
|
550
|
+
if len(used_names) > 0:
|
|
551
|
+
statement = "from typing import {}".format(", ".join(used_names))
|
|
552
|
+
|
|
553
|
+
return statement
|
|
554
|
+
|
|
555
|
+
def _write_file(self, path: Path, content: str):
|
|
556
|
+
path.write_text(content, encoding="utf-8")
|
|
557
|
+
return
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def rest_client_generator_main():
|
|
561
|
+
"""
|
|
562
|
+
Entry point used to generate REST client code from the OpenAPI specification.
|
|
563
|
+
"""
|
|
564
|
+
parser = argparse.ArgumentParser(description="Generate GitHub REST client code from an OpenAPI specification.")
|
|
565
|
+
parser.add_argument("--client-root", required=True, help="Root directory where the generated client package (public) resides.")
|
|
566
|
+
parser.add_argument("--client-classname", default="GithubRestClient", help="Class name to use for the generated root client.")
|
|
567
|
+
parser.add_argument("--specfile", required=True, help="Path to the OpenAPI specification JSON or YAML file.")
|
|
568
|
+
|
|
569
|
+
args = parser.parse_args()
|
|
570
|
+
|
|
571
|
+
generator = RestClientGenerator(
|
|
572
|
+
openapi_spec_file=args.specfile,
|
|
573
|
+
clients_root=args.client_root,
|
|
574
|
+
client_class_name=args.client_classname
|
|
575
|
+
)
|
|
576
|
+
generator.generate()
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
if __name__ == "__main__":
|
|
581
|
+
rest_client_generator_main()
|