ominfra 0.0.0.dev199__py3-none-any.whl → 0.0.0.dev201__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,497 @@
1
+ """
2
+ TODO:
3
+ - default values? nullability? maybe a new_default helper?
4
+ - relative import base
5
+ """
6
+ import builtins
7
+ import dataclasses as dc
8
+ import io
9
+ import typing as ta
10
+
11
+ from omlish import check
12
+ from omlish import collections as col
13
+ from omlish import lang
14
+
15
+
16
+ if ta.TYPE_CHECKING:
17
+ import botocore.loaders
18
+ import botocore.model
19
+ import botocore.session
20
+ else:
21
+ botocore = lang.proxy_import('botocore', extras=[
22
+ 'loaders',
23
+ 'model',
24
+ 'session',
25
+ ])
26
+
27
+
28
+ ##
29
+
30
+
31
+ ServiceTypeName: ta.TypeAlias = ta.Literal[
32
+ 'service-2',
33
+ 'paginators-1',
34
+ 'waiters-2',
35
+ ]
36
+
37
+
38
+ class ModelGen:
39
+ def __init__(
40
+ self,
41
+ service_model: 'botocore.model.ServiceModel',
42
+ *,
43
+ shape_names: ta.Iterable[str] = (),
44
+ operation_names: ta.Iterable[str] = (),
45
+ ) -> None:
46
+ super().__init__()
47
+
48
+ self._service_model = service_model
49
+ self._shape_names = list(check.not_isinstance(shape_names, str))
50
+ self._operation_names = list(check.not_isinstance(operation_names, str))
51
+
52
+ @property
53
+ def service_model(self) -> 'botocore.model.ServiceModel':
54
+ return self._service_model
55
+
56
+ @property
57
+ def shape_names(self) -> ta.Sequence[str]:
58
+ return self._shape_names
59
+
60
+ @property
61
+ def operation_names(self) -> ta.Sequence[str]:
62
+ return self._operation_names
63
+
64
+ #
65
+
66
+ @classmethod
67
+ def create_data_loader(cls) -> 'botocore.loaders.Loader':
68
+ session = botocore.session.get_session()
69
+ return session.get_component('data_loader')
70
+
71
+ @classmethod
72
+ def list_available_services(
73
+ cls,
74
+ *,
75
+ type_name: ServiceTypeName = 'service-2',
76
+ ) -> list[str]:
77
+ loader = cls.create_data_loader()
78
+ return list(loader.list_available_services(type_name))
79
+
80
+ @classmethod
81
+ def load_service_model(
82
+ cls,
83
+ service_name: str,
84
+ *,
85
+ type_name: ServiceTypeName = 'service-2',
86
+ api_version: str | None = None,
87
+ ) -> 'botocore.model.ServiceModel':
88
+ loader = cls.create_data_loader()
89
+ json_model = loader.load_service_model(service_name, type_name, api_version=api_version)
90
+ return botocore.model.ServiceModel(json_model, service_name=service_name)
91
+
92
+ @classmethod
93
+ def get_referenced_shape_names(
94
+ cls,
95
+ service_model: 'botocore.model.ServiceModel',
96
+ *,
97
+ shape_names: ta.Iterable[str] = (),
98
+ operation_names: ta.Iterable[str] = (),
99
+ ) -> list[str]:
100
+ todo = set(check.not_isinstance(shape_names, str))
101
+
102
+ for on in operation_names:
103
+ op = service_model.operation_model(on)
104
+ for osh in [
105
+ op.input_shape,
106
+ op.output_shape,
107
+ *op.error_shapes,
108
+ ]:
109
+ if osh is not None:
110
+ todo.add(osh.name)
111
+
112
+ seen = set(cls.BASE_TYPE_ANNS)
113
+
114
+ dct: dict[str, set[str]] = {}
115
+ while todo:
116
+ cur = todo.pop()
117
+ seen.add(cur)
118
+
119
+ shape: botocore.model.Shape = service_model.shape_for(cur)
120
+
121
+ if isinstance(shape, botocore.model.StructureShape):
122
+ deps = {m.name for m in shape.members.values()}
123
+
124
+ elif isinstance(shape, botocore.model.MapShape):
125
+ deps = {shape.key.name, shape.value.name}
126
+
127
+ elif isinstance(shape, botocore.model.ListShape):
128
+ deps = {shape.member.name}
129
+
130
+ else:
131
+ deps = set()
132
+
133
+ dct[shape.name] = deps
134
+ todo.update(deps - seen)
135
+
136
+ return list(lang.flatten(sorted(s - cls.BASE_SHAPE_NAMES) for s in col.mut_toposort(dct)))
137
+
138
+ #
139
+
140
+ BASE_TYPE_ANNS: ta.ClassVar[ta.Mapping[str, str]] = {
141
+ 'Boolean': 'bool',
142
+ 'Integer': 'int',
143
+ 'String': 'str',
144
+ 'DateTime': '_base.DateTime',
145
+ 'MillisecondDateTime': '_base.MillisecondDateTime',
146
+ 'TagList': '_base.TagList',
147
+ }
148
+
149
+ BASE_SHAPE_NAMES: ta.ClassVar[ta.AbstractSet[str]] = set(BASE_TYPE_ANNS)
150
+
151
+ def get_type_ann(
152
+ self,
153
+ name: str,
154
+ *,
155
+ unquoted_names: bool = False,
156
+ ) -> str | None:
157
+ try:
158
+ return self.BASE_TYPE_ANNS[name]
159
+ except KeyError:
160
+ pass
161
+
162
+ if name in self._shape_names:
163
+ name = self.sanitize_class_name(name)
164
+ if not unquoted_names:
165
+ return f"'{name}'"
166
+ else:
167
+ return name
168
+
169
+ return None
170
+
171
+ #
172
+
173
+ DEMANGLE_PREFIXES: ta.ClassVar[ta.Sequence[str]] = [
174
+ 'AAAA',
175
+ 'ACL',
176
+ 'ACP',
177
+ 'AES',
178
+ 'AES256',
179
+ 'AZ',
180
+ 'CA',
181
+ 'CRC32',
182
+ 'CRC32C',
183
+ 'DB',
184
+ 'EFS',
185
+ 'ETag',
186
+ 'IAM',
187
+ 'IO',
188
+ 'IP',
189
+ 'JSON',
190
+ 'KMS',
191
+ 'MD5',
192
+ 'MFA',
193
+ 'SHA1',
194
+ 'SHA256',
195
+ 'SSE',
196
+ 'TTL',
197
+ ]
198
+
199
+ def demangle_name(self, n: str) -> str:
200
+ ps: list[str] = []
201
+ while n:
202
+ ms: list[tuple[str, int]] = []
203
+
204
+ for pfx in self.DEMANGLE_PREFIXES:
205
+ if (i := n.find(pfx)) >= 0:
206
+ ms.append((pfx, i))
207
+
208
+ if not ms:
209
+ ps.append(n)
210
+ break
211
+
212
+ if len(ms) > 1:
213
+ m = sorted(ms, key=lambda t: (t[1], -len(t[0])))[0]
214
+ else:
215
+ m = ms[0]
216
+
217
+ pfx, i = m
218
+ l, r = n[:i], n[i + len(pfx):]
219
+
220
+ if l:
221
+ ps.append(l)
222
+ ps.append(pfx.lower())
223
+
224
+ n = r
225
+
226
+ return '_'.join(lang.snake_case(p) for p in ps)
227
+
228
+ #
229
+
230
+ def sanitize_class_name(self, n: str) -> str:
231
+ if hasattr(builtins, n):
232
+ n += '_'
233
+ return n
234
+
235
+ #
236
+
237
+ PREAMBLE_LINES: ta.Sequence[str] = [
238
+ '# flake8: noqa: E501',
239
+ '# ruff: noqa: N801 S105',
240
+ '# fmt: off',
241
+ 'import dataclasses as _dc # noqa',
242
+ 'import enum as _enum # noqa',
243
+ 'import typing as _ta # noqa',
244
+ '',
245
+ 'from .. import base as _base # noqa',
246
+ '',
247
+ '',
248
+ '##',
249
+ '',
250
+ '',
251
+ ]
252
+
253
+ def gen_preamble(self) -> str:
254
+ return '\n'.join(self.PREAMBLE_LINES)
255
+
256
+ #
257
+
258
+ PRIMITIVE_SHAPE_TYPES: ta.ClassVar[ta.Mapping[str, str]] = {
259
+ 'integer': 'int',
260
+ 'long': 'int',
261
+ 'blob': 'bytes',
262
+ 'boolean': 'bool',
263
+ 'timestamp': '_base.Timestamp',
264
+ }
265
+
266
+ @dc.dataclass(frozen=True)
267
+ class ShapeSrc:
268
+ src: str
269
+
270
+ class_name: str | None = dc.field(default=None, kw_only=True)
271
+ double_space: bool = False
272
+
273
+ def gen_shape(
274
+ self,
275
+ name: str,
276
+ *,
277
+ unquoted_names: bool = False,
278
+ ) -> ShapeSrc:
279
+ shape: botocore.model.Shape = self._service_model.shape_for(name)
280
+
281
+ san_name = self.sanitize_class_name(shape.name)
282
+
283
+ if isinstance(shape, botocore.model.StructureShape):
284
+ lines: list[str] = []
285
+
286
+ mds = [
287
+ f'shape_name={shape.name!r}',
288
+ ]
289
+
290
+ lines.extend([
291
+ '@_dc.dataclass(frozen=True)',
292
+ f'class {san_name}(',
293
+ ' _base.Shape,',
294
+ *[f' {dl},' for dl in mds],
295
+ '):',
296
+ ])
297
+
298
+ if not shape.members:
299
+ lines.append(' pass')
300
+
301
+ for i, (mn, ms) in enumerate(shape.members.items()):
302
+ if i:
303
+ lines.append('')
304
+ fn = self.demangle_name(mn)
305
+ mds = [
306
+ f'member_name={mn!r}',
307
+ f'shape_name={ms.name!r}',
308
+ ]
309
+ ma = self.get_type_ann(
310
+ ms.name,
311
+ unquoted_names=unquoted_names,
312
+ )
313
+ fls = [
314
+ f'{fn}: {ma or ms.name} = _dc.field(metadata=_base.field_metadata(',
315
+ *[f' {dl},' for dl in mds],
316
+ '))',
317
+ ]
318
+ if ma is None:
319
+ fls = ['# ' + fl for fl in fls]
320
+ lines.append('\n'.join(' ' + fl for fl in fls))
321
+
322
+ return self.ShapeSrc(
323
+ '\n'.join(lines),
324
+ class_name=san_name,
325
+ double_space=True,
326
+ )
327
+
328
+ elif isinstance(shape, botocore.model.ListShape):
329
+ mn = shape.member.name
330
+ ma = self.get_type_ann(
331
+ mn,
332
+ unquoted_names=unquoted_names,
333
+ )
334
+ l = f'{san_name}: _ta.TypeAlias = _ta.Sequence[{ma or mn}]'
335
+ if ma is None:
336
+ l = '# ' + l
337
+ return self.ShapeSrc(l)
338
+
339
+ elif isinstance(shape, botocore.model.MapShape):
340
+ # shape.key, shape.value
341
+ kn = shape.key.name
342
+ ka = self.get_type_ann(
343
+ kn,
344
+ unquoted_names=unquoted_names,
345
+ )
346
+ vn = shape.key.name
347
+ va = self.get_type_ann(
348
+ vn,
349
+ unquoted_names=unquoted_names,
350
+ )
351
+ l = f'{san_name}: _ta.TypeAlias = _ta.Mapping[{ka or kn}, {va or vn}]'
352
+ if ka is None or va is None:
353
+ l = '# ' + l
354
+ return self.ShapeSrc(l)
355
+
356
+ elif isinstance(shape, botocore.model.StringShape):
357
+ if shape.enum:
358
+ ls = [
359
+ f'class {san_name}(_enum.Enum):',
360
+ ]
361
+ all_caps = all(v == v.upper() for v in shape.enum)
362
+ for v in shape.enum:
363
+ n = v
364
+ if not all_caps:
365
+ n = self.demangle_name(n)
366
+ n = n.upper()
367
+ for c in '.-:':
368
+ n = n.replace(c, '_')
369
+ ls.append(f' {n} = {v!r}')
370
+ return self.ShapeSrc(
371
+ '\n'.join(ls),
372
+ double_space=True,
373
+ )
374
+
375
+ else:
376
+ return self.ShapeSrc(f"{san_name} = _ta.NewType('{san_name}', str)")
377
+
378
+ elif (pt := self.PRIMITIVE_SHAPE_TYPES.get(shape.type_name)) is not None:
379
+ return self.ShapeSrc(f'{san_name} = _ta.NewType({san_name!r}, {pt})')
380
+
381
+ else:
382
+ raise TypeError(shape.type_name)
383
+
384
+ def gen_all_shapes(
385
+ self,
386
+ out: ta.TextIO,
387
+ *,
388
+ unquoted_names: bool = False,
389
+ ) -> None:
390
+ shape_srcs = [
391
+ self.gen_shape(
392
+ shape_name,
393
+ unquoted_names=unquoted_names,
394
+ )
395
+ for shape_name in self.shape_names
396
+ ]
397
+
398
+ all_shapes: list[str] = []
399
+
400
+ prev_shape_src: ModelGen.ShapeSrc | None = None
401
+ for shape_src in shape_srcs:
402
+ if prev_shape_src is not None:
403
+ out.write('\n')
404
+ if shape_src.double_space or prev_shape_src.double_space:
405
+ out.write('\n')
406
+ out.write(shape_src.src)
407
+ out.write('\n')
408
+ if shape_src.class_name is not None:
409
+ all_shapes.append(shape_src.class_name)
410
+ prev_shape_src = shape_src
411
+
412
+ out.write('\n\n')
413
+ out.write('ALL_SHAPES: frozenset[type[_base.Shape]] = frozenset([\n')
414
+ for n in sorted(all_shapes):
415
+ out.write(f' {n},\n')
416
+ out.write('])\n')
417
+
418
+ #
419
+
420
+ @dc.dataclass(frozen=True)
421
+ class OperationSrc:
422
+ src: str
423
+ name: str
424
+
425
+ def gen_operation(
426
+ self,
427
+ name: str,
428
+ ) -> OperationSrc:
429
+ operation: botocore.model.OperationModel = self._service_model.operation_model(name)
430
+
431
+ dcn = self.demangle_name(operation.name).upper()
432
+
433
+ fls = [
434
+ f'name={operation.name!r},',
435
+ ]
436
+
437
+ if operation.input_shape is not None:
438
+ fls.append(f'input={operation.input_shape.name},')
439
+ if operation.output_shape is not None:
440
+ fls.append(f'output={operation.output_shape.name},')
441
+
442
+ if operation.error_shapes:
443
+ fls.append('errors=[')
444
+ for osn in [es.name for es in operation.error_shapes]:
445
+ fls.append(f' {osn},')
446
+ fls.append('],')
447
+
448
+ lines = [
449
+ f'{dcn} = _base.Operation(',
450
+ *[f' {fl}' for fl in fls],
451
+ ')',
452
+ ]
453
+
454
+ return self.OperationSrc('\n'.join(lines), dcn)
455
+
456
+ def gen_all_operations(
457
+ self,
458
+ out: ta.TextIO,
459
+ ) -> None:
460
+ all_operations: list[str] = []
461
+
462
+ for i, name in enumerate(sorted(self._operation_names)):
463
+ if i:
464
+ out.write('\n')
465
+ ops = self.gen_operation(
466
+ name,
467
+ )
468
+ all_operations.append(ops.name)
469
+ out.write(ops.src)
470
+ out.write('\n')
471
+ out.write('\n')
472
+
473
+ out.write('\n')
474
+ out.write('ALL_OPERATIONS: frozenset[_base.Operation] = frozenset([\n')
475
+ for n in sorted(all_operations):
476
+ out.write(f' {n},\n')
477
+ out.write('])\n')
478
+
479
+ #
480
+
481
+ def gen_module(self) -> str:
482
+ out = io.StringIO()
483
+
484
+ out.write(self.gen_preamble())
485
+ out.write('\n')
486
+
487
+ self.gen_all_shapes(
488
+ out,
489
+ unquoted_names=True,
490
+ )
491
+
492
+ if self._operation_names:
493
+ out.write('\n\n##\n\n\n')
494
+
495
+ self.gen_all_operations(out)
496
+
497
+ return out.getvalue()
File without changes