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.
Files changed (70) hide show
  1. ccai/gitlab/py.typed +0 -0
  2. ccai/gitlab/rest/aspectsrest.py +120 -0
  3. ccai/gitlab/rest/clients/__init__.py +0 -0
  4. ccai/gitlab/rest/clients/gitlabrestclient.py +118 -0
  5. ccai/gitlab/rest/clients/groups/__init__.py +0 -0
  6. ccai/gitlab/rest/clients/groups/accessrequests.py +230 -0
  7. ccai/gitlab/rest/clients/groups/admin.py +41 -0
  8. ccai/gitlab/rest/clients/groups/alertmanagement.py +149 -0
  9. ccai/gitlab/rest/clients/groups/application.py +68 -0
  10. ccai/gitlab/rest/clients/groups/applications.py +95 -0
  11. ccai/gitlab/rest/clients/groups/avatar.py +41 -0
  12. ccai/gitlab/rest/clients/groups/badges.py +338 -0
  13. ccai/gitlab/rest/clients/groups/batchedbackgroundmigrations.py +122 -0
  14. ccai/gitlab/rest/clients/groups/branches.py +230 -0
  15. ccai/gitlab/rest/clients/groups/broadcastmessages.py +149 -0
  16. ccai/gitlab/rest/clients/groups/bulkimports.py +176 -0
  17. ccai/gitlab/rest/clients/groups/clusters.py +149 -0
  18. ccai/gitlab/rest/clients/groups/jobs.py +95 -0
  19. ccai/gitlab/rest/clients/groups/metadata.py +68 -0
  20. ccai/gitlab/rest/clients/groups/migrations.py +41 -0
  21. ccai/gitlab/rest/clients/groups/pipelinecomposition.py +146 -0
  22. ccai/gitlab/rest/clients/groups/planlimits.py +68 -0
  23. ccai/gitlab/rest/clients/parameters/__init__.py +0 -0
  24. ccai/gitlab/rest/clients/parameters/accessrequests.py +222 -0
  25. ccai/gitlab/rest/clients/parameters/admin.py +31 -0
  26. ccai/gitlab/rest/clients/parameters/alertmanagement.py +147 -0
  27. ccai/gitlab/rest/clients/parameters/application.py +27 -0
  28. ccai/gitlab/rest/clients/parameters/applications.py +51 -0
  29. ccai/gitlab/rest/clients/parameters/avatar.py +31 -0
  30. ccai/gitlab/rest/clients/parameters/badges.py +344 -0
  31. ccai/gitlab/rest/clients/parameters/batchedbackgroundmigrations.py +105 -0
  32. ccai/gitlab/rest/clients/parameters/branches.py +236 -0
  33. ccai/gitlab/rest/clients/parameters/broadcastmessages.py +123 -0
  34. ccai/gitlab/rest/clients/parameters/bulkimports.py +173 -0
  35. ccai/gitlab/rest/clients/parameters/clusters.py +100 -0
  36. ccai/gitlab/rest/clients/parameters/jobs.py +88 -0
  37. ccai/gitlab/rest/clients/parameters/metadata.py +11 -0
  38. ccai/gitlab/rest/clients/parameters/migrations.py +29 -0
  39. ccai/gitlab/rest/clients/parameters/pipelinecomposition.py +123 -0
  40. ccai/gitlab/rest/clients/parameters/planlimits.py +47 -0
  41. ccai/gitlab/rest/clients/responses/__init__.py +0 -0
  42. ccai/gitlab/rest/clients/responses/accessrequests.py +157 -0
  43. ccai/gitlab/rest/clients/responses/admin.py +24 -0
  44. ccai/gitlab/rest/clients/responses/alertmanagement.py +100 -0
  45. ccai/gitlab/rest/clients/responses/application.py +43 -0
  46. ccai/gitlab/rest/clients/responses/applications.py +62 -0
  47. ccai/gitlab/rest/clients/responses/avatar.py +24 -0
  48. ccai/gitlab/rest/clients/responses/badges.py +233 -0
  49. ccai/gitlab/rest/clients/responses/batchedbackgroundmigrations.py +81 -0
  50. ccai/gitlab/rest/clients/responses/branches.py +157 -0
  51. ccai/gitlab/rest/clients/responses/broadcastmessages.py +100 -0
  52. ccai/gitlab/rest/clients/responses/bulkimports.py +119 -0
  53. ccai/gitlab/rest/clients/responses/clusters.py +100 -0
  54. ccai/gitlab/rest/clients/responses/jobs.py +62 -0
  55. ccai/gitlab/rest/clients/responses/metadata.py +43 -0
  56. ccai/gitlab/rest/clients/responses/migrations.py +24 -0
  57. ccai/gitlab/rest/clients/responses/pipelinecomposition.py +100 -0
  58. ccai/gitlab/rest/clients/responses/planlimits.py +43 -0
  59. ccai/gitlab/rest/descriptions/gitlab-openapi-hints.yaml +195 -0
  60. ccai/gitlab/rest/descriptions/gitlab-openapi-spec.yaml +3635 -0
  61. ccai/gitlab/rest/generation/__init__.py +0 -0
  62. ccai/gitlab/rest/generation/restclientgenerator.py +581 -0
  63. ccai/gitlab/rest/itemsresult.py +274 -0
  64. ccai/gitlab/rest/restapigroup.py +17 -0
  65. ccai/gitlab/rest/restclientbase.py +676 -0
  66. ccai/gitlab/rest/restclientutils.py +0 -0
  67. codecraftai_restclient_gitlab-1.2.0.dist-info/METADATA +90 -0
  68. codecraftai_restclient_gitlab-1.2.0.dist-info/RECORD +70 -0
  69. codecraftai_restclient_gitlab-1.2.0.dist-info/WHEEL +4 -0
  70. 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()