#!/usr/bin/env python3 """Working helper (tmp, not shipped): query the OpenAI OpenAPI spec offline. dump.py list REGEX -> schema names matching regex dump.py schema NAME [DEPTH] -> resolved schema as JSON (refs inlined to DEPTH) dump.py flatten NAME [DEPTH] -> dotted-path parameter rows (JSON list) dump.py events UNION_NAME -> compact list of event schemas from a oneOf/anyOf union dump.py paths PATH [PATH...] -> operations for these paths (params, body ref, response ref) """ from __future__ import annotations import json, re, sys from pathlib import Path ROOT = Path('/Users/simon-pierreboucher/Desktop/doc-api') SPEC = ROOT / 'sources/openai/openapi/openapi-master.yaml' CACHE = ROOT / 'tmp/openai-core/spec.json' def load(): if CACHE.exists(): return json.loads(CACHE.read_text()) import yaml try: from yaml import CSafeLoader as L except ImportError: from yaml import SafeLoader as L spec = yaml.load(SPEC.read_text(), Loader=L) CACHE.parent.mkdir(parents=True, exist_ok=True) CACHE.write_text(json.dumps(spec, default=str)) return json.loads(CACHE.read_text()) SPEC_OBJ = load() SCHEMAS = SPEC_OBJ['components']['schemas'] def ref_name(node): return node['$ref'].split('/')[-1] if isinstance(node, dict) and '$ref' in node else None def deref(node): """Return (resolved_node, ref_name). Sibling keys of a $ref (description etc.) override.""" name = None while isinstance(node, dict) and '$ref' in node: name = ref_name(node) target = SCHEMAS.get(name) if target is None: return {'x-missing-ref': name}, name merged = dict(target) for k, v in node.items(): if k != '$ref': merged[k] = v node = merged return node, name def clean(desc): if not desc: return None d = re.sub(r'\s+', ' ', str(desc)).strip() return d[:900] def split_nullable(node): """anyOf/oneOf with a {"type":"null"} member -> (other_members, nullable)""" for key in ('anyOf', 'oneOf'): if key in node and isinstance(node[key], list): members = [m for m in node[key] if not (isinstance(m, dict) and m.get('type') == 'null')] nullable = len(members) != len(node[key]) return key, members, nullable return None, None, bool(node.get('nullable')) def merge_allof(node): if 'allOf' not in node: return node out = {k: v for k, v in node.items() if k != 'allOf'} props, req = {}, [] for m in node['allOf']: r, _ = deref(m) r = merge_allof(r) props.update(r.get('properties', {})) req += r.get('required', []) for k in ('description', 'title'): if k in r and k not in out: out[k] = r[k] out.setdefault('type', 'object') out['properties'] = {**props, **out.get('properties', {})} out['required'] = sorted(set(req + out.get('required', []))) return out def type_label(node): if isinstance(node, bool): return 'any' if node else 'never' if not isinstance(node, dict): return 'any' node, name = deref(node) node = merge_allof(node) key, members, nullable = split_nullable(node) if members is not None: if len(members) == 1: t = type_label(members[0]) else: labels = [] for m in members: r, n = deref(m) r = merge_allof(r) if r.get('type') == 'object' and 'type' in r.get('properties', {}): e = r['properties']['type'].get('enum') or [] labels.append(f"object<{e[0]}>" if len(e) == 1 else (n or 'object')) else: labels.append(type_label(m)) t = ' | '.join(dict.fromkeys(labels)) return t + (' | null' if nullable else '') t = node.get('type') if t == 'array': return f"array<{type_label(node.get('items', {}))}>" if t is None: return name or 'any' if t == 'object' and node.get('additionalProperties') and not node.get('properties'): return 'map' if nullable: t = f"{t} | null" return t def const_type(node): p = node.get('properties', {}) if 'type' in p: e = p['type'].get('enum') if e and len(e) == 1: return e[0] return None def flatten(node, path, required, out, depth, stack, variant=None): if depth < 0: return node, name = deref(node) if name and name in stack: return stack = stack + ([name] if name else []) node = merge_allof(node) key, members, nullable = split_nullable(node) if members is not None and len(members) == 1: inner, _ = deref(members[0]) inner = merge_allof(inner) merged = {**inner, **{k: v for k, v in node.items() if k not in (key,)}} if nullable: merged['nullable'] = True node = merged key, members, nullable = split_nullable(node) row = { 'parameter': path, 'type': type_label(node), 'required': required, 'default': node.get('default'), 'minimum': node.get('minimum'), 'maximum': node.get('maximum'), 'enum': node.get('enum'), 'description': clean(node.get('description')), 'deprecated': node.get('deprecated', False), 'nullable': nullable or bool(node.get('nullable')), 'variant': variant, 'schema_ref': name, } if path: out.append(row) if members is not None and len(members) > 1: for m in members: r, n = deref(m) r = merge_allof(r) ct = const_type(r) if r.get('type') == 'object' or 'properties' in r: sub = f"{path}({ct or n or 'object'})" flatten(m, sub, required, out, depth - 1, stack, variant=ct or n) elif r.get('type') == 'array': flatten(m, path + '[]', False, out, depth - 1, stack) return if node.get('type') == 'array' or 'items' in node: flatten(node.get('items', {}), path + '[]', False, out, depth - 1, stack) return props = node.get('properties') or {} req = set(node.get('required', [])) for pname, pnode in props.items(): flatten(pnode, f"{path}.{pname}" if path else pname, pname in req, out, depth - 1, stack) def show_schema(name, depth=3): def walk(node, d): node, n = deref(node) if d <= 0: return {'$schema': n} if n else {k: v for k, v in node.items() if k in ('type', 'enum')} if isinstance(node, dict): return {k: (walk(v, d - 1) if k in ('properties', 'items', 'allOf', 'anyOf', 'oneOf', 'additionalProperties') or isinstance(v, (dict, list)) and k not in ('enum', 'required', 'example', 'x-oaiMeta') else v) for k, v in node.items()} if isinstance(node, list): return [walk(x, d) for x in node] return node return walk(SCHEMAS[name], depth) def events(union): node = SCHEMAS[union] members = node.get('anyOf') or node.get('oneOf') or [] out = [] for m in members: r, n = deref(m) r = merge_allof(r) ct = const_type(r) props = {} for pn, pv in (r.get('properties') or {}).items(): props[pn] = {'type': type_label(pv), 'description': clean(deref(pv)[0].get('description'))} out.append({'schema': n, 'type': ct, 'description': clean(r.get('description')), 'properties': props, 'required': r.get('required', []), 'example': (r.get('x-oaiMeta') or {}).get('example')}) return out def paths(plist): out = {} for p in plist: item = SPEC_OBJ['paths'].get(p) if not item: out[p] = 'MISSING' continue for method, op in item.items(): if method not in ('get', 'post', 'delete', 'patch', 'put'): continue params = [] for prm in op.get('parameters', []): prm, _ = deref(prm) if '$ref' in prm else (prm, None) sch = prm.get('schema', {}) params.append({'name': prm.get('name'), 'in': prm.get('in'), 'required': prm.get('required', False), 'type': type_label(sch), 'enum': deref(sch)[0].get('enum'), 'default': deref(sch)[0].get('default'), 'description': clean(prm.get('description'))}) body = op.get('requestBody', {}).get('content', {}) body_ref = {ct: ref_name(v.get('schema', {})) or type_label(v.get('schema', {})) for ct, v in body.items()} resp = {} for code, r in op.get('responses', {}).items(): for ct, v in (r.get('content') or {}).items(): resp[f"{code} {ct}"] = ref_name(v.get('schema', {})) or type_label(v.get('schema', {})) meta = op.get('x-oaiMeta', {}) out[f"{method.upper()} {p}"] = {'operationId': op.get('operationId'), 'summary': clean(op.get('summary')), 'description': clean(op.get('description')), 'parameters': params, 'requestBody': body_ref, 'responses': resp, 'deprecated': op.get('deprecated', False), 'meta': {k: (clean(v) if isinstance(v, str) else v) for k, v in meta.items() if k in ('name', 'group', 'returns', 'beta', 'path')}} return out if __name__ == '__main__': cmd = sys.argv[1] if cmd == 'list': rx = re.compile(sys.argv[2]) print('\n'.join(n for n in SCHEMAS if rx.search(n))) elif cmd == 'schema': print(json.dumps(show_schema(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 3), indent=1)) elif cmd == 'flatten': rows = [] flatten({'$ref': f'#/components/schemas/{sys.argv[2]}'}, '', False, rows, int(sys.argv[3]) if len(sys.argv) > 3 else 6, []) print(json.dumps(rows, indent=None)) elif cmd == 'events': print(json.dumps(events(sys.argv[2]), indent=1)) elif cmd == 'paths': print(json.dumps(paths(sys.argv[2:]), indent=1))