geniespec 0.1.1__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.
geniespec/__init__.py ADDED
File without changes
@@ -0,0 +1,372 @@
1
+ $schema: 'http://json-schema.org/draft-07/schema#'
2
+ $id: kibaRestApiSchema
3
+ type: object
4
+ additionalProperties: false
5
+ required:
6
+ - version
7
+ - serviceName
8
+ - pythonClientPackageName
9
+ - typescriptClientPackageName
10
+ properties:
11
+ version:
12
+ type: string
13
+ pattern: '^([0-9]|[1-9][0-9]*)\.([0-9]|[1-9][0-9]*)\.([0-9]|[1-9][0-9]*)$'
14
+ serviceName:
15
+ type: string
16
+ pattern: '^[a-z][a-z-]*$'
17
+ pythonClientPackageName:
18
+ type: string
19
+ pattern: '^[a-z][-a-z]*$'
20
+ typescriptClientPackageName:
21
+ type: string
22
+ pattern: '[@/-a-z]*$'
23
+ transitions:
24
+ type: array
25
+ items:
26
+ $ref: '#/definitions/RootTransition'
27
+ resources:
28
+ type: array
29
+ items:
30
+ $ref: '#/definitions/Resource'
31
+ exceptions:
32
+ type: array
33
+ items:
34
+ $ref: '#/definitions/Exception'
35
+ enums:
36
+ type: array
37
+ items:
38
+ $ref: '#/definitions/Enum'
39
+ oneOfs:
40
+ type: array
41
+ items:
42
+ $ref: '#/definitions/OneOf'
43
+ imports:
44
+ type: array
45
+ items:
46
+ type: string
47
+ defaultAuthentication:
48
+ type:
49
+ - "null"
50
+ - string
51
+ pattern: '^[a-z][a-z0-9-]*$'
52
+ definitions:
53
+ EnumValues:
54
+ type: array
55
+ minItems: 1
56
+ uniqueItems: true
57
+ items:
58
+ type: string
59
+ pattern: '^[A-Za-z0-9][A-Za-z0-9_-]*$'
60
+ Enum:
61
+ type: object
62
+ additionalProperties: false
63
+ required:
64
+ - name
65
+ - values
66
+ properties:
67
+ name:
68
+ type: string
69
+ pattern: '^[A-Z][A-Za-z]*$'
70
+ values:
71
+ $ref: '#/definitions/EnumValues'
72
+ OneOf:
73
+ type: object
74
+ additionalProperties: false
75
+ required:
76
+ - name
77
+ - discriminator
78
+ - types
79
+ properties:
80
+ name:
81
+ type: string
82
+ pattern: '^[A-Z][A-Za-z]*$'
83
+ discriminator:
84
+ type: string
85
+ pattern: '^[a-z][A-Za-z0-9-]*$'
86
+ types:
87
+ type: array
88
+ minItems: 2
89
+ uniqueItems: true
90
+ items:
91
+ type: string
92
+ pattern: '^[A-Z][A-Za-z-]*$'
93
+ Attribute:
94
+ type: object
95
+ additionalProperties: false
96
+ required:
97
+ - name
98
+ - type
99
+ properties:
100
+ name:
101
+ type: string
102
+ pattern: '^[a-z][A-Za-z0-9-]*$'
103
+ type:
104
+ type: string
105
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
106
+ listValueTypes:
107
+ type: array
108
+ minItems: 1
109
+ items:
110
+ type: string
111
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
112
+ isPrimary:
113
+ type: boolean
114
+ isUnique:
115
+ type: boolean
116
+ shouldDisableGetEndpoint:
117
+ type: boolean
118
+ isUnfilterable:
119
+ type: boolean
120
+ isUpdatable:
121
+ type: boolean
122
+ isNullable:
123
+ type: boolean
124
+ isDeprecated:
125
+ type: boolean
126
+ defaultValue:
127
+ type:
128
+ - string
129
+ - number
130
+ - boolean
131
+ - 'null'
132
+ isSettable:
133
+ type: boolean
134
+ isAccessibleIndependently:
135
+ type: boolean
136
+ mappingKeyType:
137
+ type: string
138
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
139
+ mappingValueTypes:
140
+ type: array
141
+ items:
142
+ type: string
143
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
144
+ enumValues:
145
+ $ref: '#/definitions/EnumValues'
146
+ Resource:
147
+ type: object
148
+ additionalProperties: false
149
+ required:
150
+ - singleName
151
+ - collectionName
152
+ properties:
153
+ singleName:
154
+ type: string
155
+ pattern: '^[A-Z][A-Za-z-]*$'
156
+ collectionName:
157
+ type: string
158
+ pattern: '^[A-Z][A-Za-z-]*$'
159
+ parentResource:
160
+ type: string
161
+ pattern: '^[A-Z][A-Za-z-]*$'
162
+ attributes:
163
+ type: array
164
+ items:
165
+ $ref: '#/definitions/Attribute'
166
+ transitions:
167
+ type: array
168
+ items:
169
+ $ref: '#/definitions/Transition'
170
+ accessibleFrom:
171
+ type: array
172
+ items:
173
+ type: string
174
+ BaseTransition:
175
+ type: object
176
+ additionalProperties: false
177
+ required:
178
+ - action
179
+ properties:
180
+ action:
181
+ type: string
182
+ pattern: '^[A-Z][A-Z-]*$'
183
+ method:
184
+ type: string
185
+ enum:
186
+ - GET
187
+ - POST
188
+ requestFields:
189
+ type: array
190
+ items:
191
+ $ref: '#/definitions/RequestField'
192
+ responseField:
193
+ $ref: '#/definitions/ResponseField'
194
+ authentication:
195
+ type:
196
+ - "null"
197
+ - string
198
+ pattern: '^[a-z][a-z0-9-]*$'
199
+ responseType:
200
+ type: string
201
+ enum:
202
+ - json
203
+ - stream
204
+ - redirect
205
+ Transition:
206
+ type: object
207
+ additionalProperties: false
208
+ required:
209
+ - action
210
+ properties:
211
+ action:
212
+ type: string
213
+ pattern: '^[A-Z][A-Z-]*$'
214
+ method:
215
+ type: string
216
+ enum:
217
+ - GET
218
+ - POST
219
+ requestFields:
220
+ type: array
221
+ items:
222
+ $ref: '#/definitions/RequestField'
223
+ responseField:
224
+ $ref: '#/definitions/ResponseField'
225
+ authentication:
226
+ type:
227
+ - "null"
228
+ - string
229
+ pattern: '^[a-z][a-z0-9-]*$'
230
+ responseType:
231
+ type: string
232
+ enum:
233
+ - json
234
+ - stream
235
+ - redirect
236
+ operatesOn:
237
+ type: string
238
+ enum:
239
+ - individual
240
+ - collection
241
+ isIndirectAction:
242
+ type: boolean
243
+ RootTransition:
244
+ type: object
245
+ allOf:
246
+ - $ref: '#/definitions/BaseTransition'
247
+ RequestField:
248
+ type: object
249
+ additionalProperties: false
250
+ required:
251
+ - name
252
+ - type
253
+ properties:
254
+ name:
255
+ type: string
256
+ pattern: '^[a-z][A-Za-z0-9-]*$'
257
+ type:
258
+ type: string
259
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
260
+ listValueTypes:
261
+ type: array
262
+ minItems: 1
263
+ items:
264
+ type: string
265
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
266
+ mappingKeyType:
267
+ type: string
268
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
269
+ mappingValueTypes:
270
+ type: array
271
+ items:
272
+ type: string
273
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
274
+ enumValues:
275
+ $ref: '#/definitions/EnumValues'
276
+ isRequired:
277
+ type: boolean
278
+ defaultValue:
279
+ type:
280
+ - string
281
+ - number
282
+ - boolean
283
+ - 'null'
284
+ isDeprecated:
285
+ type: boolean
286
+ maxValue:
287
+ type:
288
+ - string
289
+ - number
290
+ minValue:
291
+ type:
292
+ - string
293
+ - number
294
+ source:
295
+ type: string
296
+ enum:
297
+ - request
298
+ - query-parameters
299
+ ResponseField:
300
+ type: object
301
+ additionalProperties: false
302
+ required:
303
+ - name
304
+ - type
305
+ properties:
306
+ name:
307
+ type: string
308
+ pattern: '^[a-z][A-Za-z-]*$'
309
+ type:
310
+ type: string
311
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
312
+ listValueTypes:
313
+ type: array
314
+ minItems: 1
315
+ items:
316
+ type: string
317
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
318
+ mappingKeyType:
319
+ type: string
320
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
321
+ mappingValueTypes:
322
+ type: array
323
+ items:
324
+ type: string
325
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
326
+ enumValues:
327
+ $ref: '#/definitions/EnumValues'
328
+ isNullable:
329
+ type: boolean
330
+ isList:
331
+ type: boolean
332
+ Exception:
333
+ type: object
334
+ additionalProperties: false
335
+ required:
336
+ - name
337
+ properties:
338
+ name:
339
+ type: string
340
+ pattern: '^[A-Z][A-Za-z-]*Exception$'
341
+ message:
342
+ type: string
343
+ extends:
344
+ type: string
345
+ pattern: '^([a-z-]+.exceptions.[A-Za-z-]*)|([A-Za-z-]*)'
346
+ statusCode:
347
+ type: integer
348
+ fields:
349
+ type: array
350
+ items:
351
+ $ref: '#/definitions/ExceptionField'
352
+ ExceptionField:
353
+ type: object
354
+ additionalProperties: false
355
+ required:
356
+ - name
357
+ - type
358
+ properties:
359
+ name:
360
+ type: string
361
+ pattern: '^[a-z][A-Za-z-]*$'
362
+ type:
363
+ type: string
364
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
365
+ listValueTypes:
366
+ type: array
367
+ minItems: 1
368
+ items:
369
+ type: string
370
+ pattern: '^[A-Z][A-Za-z-\[\]_, ]*$'
371
+ isRequired:
372
+ type: boolean
geniespec/cli.py ADDED
@@ -0,0 +1,123 @@
1
+ import argparse
2
+ import pathlib
3
+ import signal
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ import time
8
+ import types
9
+
10
+ import yaml # type: ignore[import-untyped]
11
+ from core import logging
12
+
13
+ from geniespec.genie import TARGET_LANGUAGE_CODES
14
+ from geniespec.genie import Renderer
15
+ from geniespec.genie import SyntacticApiValidator
16
+ from geniespec.types import KibaApi
17
+
18
+ WATCH_INTERVAL_SECONDS = 0.5
19
+ COMMAND_STOP_TIMEOUT_SECONDS = 10
20
+
21
+
22
+ def _sync_directory(sourceDirectoryPath: pathlib.Path, targetDirectoryPath: pathlib.Path) -> list[pathlib.Path]:
23
+ changedPaths: list[pathlib.Path] = []
24
+ sourceRelativePaths = {path.relative_to(sourceDirectoryPath) for path in sourceDirectoryPath.rglob('*') if path.is_file()}
25
+ for relativePath in sorted(sourceRelativePaths):
26
+ content = (sourceDirectoryPath / relativePath).read_bytes()
27
+ targetPath = targetDirectoryPath / relativePath
28
+ if targetPath.is_file() and targetPath.read_bytes() == content:
29
+ continue
30
+ targetPath.parent.mkdir(parents=True, exist_ok=True)
31
+ temporaryPath = targetPath.with_name(f'.{targetPath.name}.tmp')
32
+ temporaryPath.write_bytes(content)
33
+ temporaryPath.replace(targetPath)
34
+ changedPaths.append(targetPath)
35
+ stalePaths = [path for path in targetDirectoryPath.rglob('*') if path.is_file() and '__pycache__' not in path.parts and path.relative_to(targetDirectoryPath) not in sourceRelativePaths]
36
+ for stalePath in stalePaths:
37
+ stalePath.unlink()
38
+ return changedPaths + stalePaths
39
+
40
+
41
+ def build(specPath: pathlib.Path, target: str, outputDirectoryPath: pathlib.Path) -> list[pathlib.Path]:
42
+ with open(specPath) as specFile:
43
+ apiYaml = yaml.safe_load(specFile)
44
+ SyntacticApiValidator().validate(apiYaml)
45
+ api = KibaApi(**apiYaml)
46
+ with tempfile.TemporaryDirectory() as renderDirectory:
47
+ Renderer(target=target).render_directory(api=api, outputDirectoryPath=renderDirectory)
48
+ changedPaths: list[pathlib.Path] = []
49
+ # NOTE(krishan711): only sync the packages the target generates so other files in the output directory are never touched
50
+ for packagePath in sorted(pathlib.Path(renderDirectory).iterdir()):
51
+ changedPaths += _sync_directory(sourceDirectoryPath=packagePath, targetDirectoryPath=outputDirectoryPath / packagePath.name)
52
+ return changedPaths
53
+
54
+
55
+ def _build_and_log(specPath: pathlib.Path, target: str, outputDirectoryPath: pathlib.Path) -> None:
56
+ changedPaths = build(specPath=specPath, target=target, outputDirectoryPath=outputDirectoryPath)
57
+ logging.info(f'geniespec: built {specPath} into {outputDirectoryPath} ({len(changedPaths)} files changed)')
58
+
59
+
60
+ def _stop_process(process: subprocess.Popen[bytes]) -> None:
61
+ if process.poll() is not None:
62
+ return
63
+ process.terminate()
64
+ try:
65
+ process.wait(timeout=COMMAND_STOP_TIMEOUT_SECONDS)
66
+ except subprocess.TimeoutExpired:
67
+ process.kill()
68
+ process.wait()
69
+
70
+
71
+ def watch(specPath: pathlib.Path, target: str, outputDirectoryPath: pathlib.Path, command: list[str]) -> int:
72
+ lastModifiedTime: int | None = None
73
+ process: subprocess.Popen[bytes] | None = None
74
+ try:
75
+ while True:
76
+ try:
77
+ modifiedTime: int | None = specPath.stat().st_mtime_ns
78
+ except FileNotFoundError:
79
+ modifiedTime = lastModifiedTime
80
+ if modifiedTime != lastModifiedTime:
81
+ lastModifiedTime = modifiedTime
82
+ try:
83
+ _build_and_log(specPath=specPath, target=target, outputDirectoryPath=outputDirectoryPath)
84
+ except Exception as exception: # noqa: BLE001
85
+ logging.error(f'geniespec: failed to build {specPath}, keeping the previous output: {exception}')
86
+ if command and process is None:
87
+ process = subprocess.Popen(command) # noqa: S603
88
+ if process is not None and process.poll() is not None:
89
+ return process.returncode
90
+ time.sleep(WATCH_INTERVAL_SECONDS)
91
+ finally:
92
+ if process is not None:
93
+ _stop_process(process=process)
94
+
95
+
96
+ def _exit_on_terminate(signalNumber: int, frame: types.FrameType | None) -> None: # noqa: ARG001
97
+ sys.exit(128 + signalNumber)
98
+
99
+
100
+ def main() -> None:
101
+ parser = argparse.ArgumentParser(prog='geniespec')
102
+ subparsers = parser.add_subparsers(dest='subcommand', required=True)
103
+ for subcommand in ('build', 'watch'):
104
+ subparser = subparsers.add_parser(subcommand)
105
+ subparser.add_argument('--spec', type=pathlib.Path, required=True)
106
+ subparser.add_argument('--target', choices=sorted(TARGET_LANGUAGE_CODES), required=True)
107
+ subparser.add_argument('--output', type=pathlib.Path, default=pathlib.Path())
108
+ subparsers.choices['watch'].add_argument('command', nargs=argparse.REMAINDER, help='command to run while watching, after --')
109
+ args = parser.parse_args()
110
+ logging.basicConfig(level=logging.INFO)
111
+ if args.subcommand == 'build':
112
+ _build_and_log(specPath=args.spec, target=args.target, outputDirectoryPath=args.output)
113
+ return
114
+ command = args.command[1:] if args.command[:1] == ['--'] else args.command
115
+ signal.signal(signal.SIGTERM, _exit_on_terminate)
116
+ try:
117
+ sys.exit(watch(specPath=args.spec, target=args.target, outputDirectoryPath=args.output, command=command))
118
+ except KeyboardInterrupt:
119
+ return
120
+
121
+
122
+ if __name__ == '__main__':
123
+ main()