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