SPB Git forge

spb/doc-api

Public
2commits 1branches 0releases
15.7 MBsize
maindefault branch
13 days agolast push
Python 88.3% TypeScript 7.6% Shell 4.1%
10.1 KB · 267 lines python
Raw Blame History
1#!/usr/bin/env python32"""Working helper (tmp, not shipped): query the OpenAI OpenAPI spec offline.34  dump.py list REGEX                 -> schema names matching regex5  dump.py schema NAME [DEPTH]        -> resolved schema as JSON (refs inlined to DEPTH)6  dump.py flatten NAME [DEPTH]       -> dotted-path parameter rows (JSON list)7  dump.py events UNION_NAME          -> compact list of event schemas from a oneOf/anyOf union8  dump.py paths PATH [PATH...]       -> operations for these paths (params, body ref, response ref)9"""10from __future__ import annotations11import json, re, sys12from pathlib import Path1314ROOT = Path('/Users/simon-pierreboucher/Desktop/doc-api')15SPEC = ROOT / 'sources/openai/openapi/openapi-master.yaml'16CACHE = ROOT / 'tmp/openai-core/spec.json'171819def load():20    if CACHE.exists():21        return json.loads(CACHE.read_text())22    import yaml23    try:24        from yaml import CSafeLoader as L25    except ImportError:26        from yaml import SafeLoader as L27    spec = yaml.load(SPEC.read_text(), Loader=L)28    CACHE.parent.mkdir(parents=True, exist_ok=True)29    CACHE.write_text(json.dumps(spec, default=str))30    return json.loads(CACHE.read_text())313233SPEC_OBJ = load()34SCHEMAS = SPEC_OBJ['components']['schemas']353637def ref_name(node):38    return node['$ref'].split('/')[-1] if isinstance(node, dict) and '$ref' in node else None394041def deref(node):42    """Return (resolved_node, ref_name). Sibling keys of a $ref (description etc.) override."""43    name = None44    while isinstance(node, dict) and '$ref' in node:45        name = ref_name(node)46        target = SCHEMAS.get(name)47        if target is None:48            return {'x-missing-ref': name}, name49        merged = dict(target)50        for k, v in node.items():51            if k != '$ref':52                merged[k] = v53        node = merged54    return node, name555657def clean(desc):58    if not desc:59        return None60    d = re.sub(r'\s+', ' ', str(desc)).strip()61    return d[:900]626364def split_nullable(node):65    """anyOf/oneOf with a {"type":"null"} member -> (other_members, nullable)"""66    for key in ('anyOf', 'oneOf'):67        if key in node and isinstance(node[key], list):68            members = [m for m in node[key] if not (isinstance(m, dict) and m.get('type') == 'null')]69            nullable = len(members) != len(node[key])70            return key, members, nullable71    return None, None, bool(node.get('nullable'))727374def merge_allof(node):75    if 'allOf' not in node:76        return node77    out = {k: v for k, v in node.items() if k != 'allOf'}78    props, req = {}, []79    for m in node['allOf']:80        r, _ = deref(m)81        r = merge_allof(r)82        props.update(r.get('properties', {}))83        req += r.get('required', [])84        for k in ('description', 'title'):85            if k in r and k not in out:86                out[k] = r[k]87    out.setdefault('type', 'object')88    out['properties'] = {**props, **out.get('properties', {})}89    out['required'] = sorted(set(req + out.get('required', [])))90    return out919293def type_label(node):94    if isinstance(node, bool):95        return 'any' if node else 'never'96    if not isinstance(node, dict):97        return 'any'98    node, name = deref(node)99    node = merge_allof(node)100    key, members, nullable = split_nullable(node)101    if members is not None:102        if len(members) == 1:103            t = type_label(members[0])104        else:105            labels = []106            for m in members:107                r, n = deref(m)108                r = merge_allof(r)109                if r.get('type') == 'object' and 'type' in r.get('properties', {}):110                    e = r['properties']['type'].get('enum') or []111                    labels.append(f"object<{e[0]}>" if len(e) == 1 else (n or 'object'))112                else:113                    labels.append(type_label(m))114            t = ' | '.join(dict.fromkeys(labels))115        return t + (' | null' if nullable else '')116    t = node.get('type')117    if t == 'array':118        return f"array<{type_label(node.get('items', {}))}>"119    if t is None:120        return name or 'any'121    if t == 'object' and node.get('additionalProperties') and not node.get('properties'):122        return 'map<string,' + type_label(node['additionalProperties']) + '>'123    if nullable:124        t = f"{t} | null"125    return t126127128def const_type(node):129    p = node.get('properties', {})130    if 'type' in p:131        e = p['type'].get('enum')132        if e and len(e) == 1:133            return e[0]134    return None135136137def flatten(node, path, required, out, depth, stack, variant=None):138    if depth < 0:139        return140    node, name = deref(node)141    if name and name in stack:142        return143    stack = stack + ([name] if name else [])144    node = merge_allof(node)145    key, members, nullable = split_nullable(node)146    if members is not None and len(members) == 1:147        inner, _ = deref(members[0])148        inner = merge_allof(inner)149        merged = {**inner, **{k: v for k, v in node.items() if k not in (key,)}}150        if nullable:151            merged['nullable'] = True152        node = merged153        key, members, nullable = split_nullable(node)154    row = {155        'parameter': path,156        'type': type_label(node),157        'required': required,158        'default': node.get('default'),159        'minimum': node.get('minimum'),160        'maximum': node.get('maximum'),161        'enum': node.get('enum'),162        'description': clean(node.get('description')),163        'deprecated': node.get('deprecated', False),164        'nullable': nullable or bool(node.get('nullable')),165        'variant': variant,166        'schema_ref': name,167    }168    if path:169        out.append(row)170    if members is not None and len(members) > 1:171        for m in members:172            r, n = deref(m)173            r = merge_allof(r)174            ct = const_type(r)175            if r.get('type') == 'object' or 'properties' in r:176                sub = f"{path}({ct or n or 'object'})"177                flatten(m, sub, required, out, depth - 1, stack, variant=ct or n)178            elif r.get('type') == 'array':179                flatten(m, path + '[]', False, out, depth - 1, stack)180        return181    if node.get('type') == 'array' or 'items' in node:182        flatten(node.get('items', {}), path + '[]', False, out, depth - 1, stack)183        return184    props = node.get('properties') or {}185    req = set(node.get('required', []))186    for pname, pnode in props.items():187        flatten(pnode, f"{path}.{pname}" if path else pname, pname in req, out, depth - 1, stack)188189190def show_schema(name, depth=3):191    def walk(node, d):192        node, n = deref(node)193        if d <= 0:194            return {'$schema': n} if n else {k: v for k, v in node.items() if k in ('type', 'enum')}195        if isinstance(node, dict):196            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)197                    for k, v in node.items()}198        if isinstance(node, list):199            return [walk(x, d) for x in node]200        return node201    return walk(SCHEMAS[name], depth)202203204def events(union):205    node = SCHEMAS[union]206    members = node.get('anyOf') or node.get('oneOf') or []207    out = []208    for m in members:209        r, n = deref(m)210        r = merge_allof(r)211        ct = const_type(r)212        props = {}213        for pn, pv in (r.get('properties') or {}).items():214            props[pn] = {'type': type_label(pv), 'description': clean(deref(pv)[0].get('description'))}215        out.append({'schema': n, 'type': ct, 'description': clean(r.get('description')),216                    'properties': props, 'required': r.get('required', []),217                    'example': (r.get('x-oaiMeta') or {}).get('example')})218    return out219220221def paths(plist):222    out = {}223    for p in plist:224        item = SPEC_OBJ['paths'].get(p)225        if not item:226            out[p] = 'MISSING'227            continue228        for method, op in item.items():229            if method not in ('get', 'post', 'delete', 'patch', 'put'):230                continue231            params = []232            for prm in op.get('parameters', []):233                prm, _ = deref(prm) if '$ref' in prm else (prm, None)234                sch = prm.get('schema', {})235                params.append({'name': prm.get('name'), 'in': prm.get('in'), 'required': prm.get('required', False),236                               'type': type_label(sch), 'enum': deref(sch)[0].get('enum'), 'default': deref(sch)[0].get('default'),237                               'description': clean(prm.get('description'))})238            body = op.get('requestBody', {}).get('content', {})239            body_ref = {ct: ref_name(v.get('schema', {})) or type_label(v.get('schema', {})) for ct, v in body.items()}240            resp = {}241            for code, r in op.get('responses', {}).items():242                for ct, v in (r.get('content') or {}).items():243                    resp[f"{code} {ct}"] = ref_name(v.get('schema', {})) or type_label(v.get('schema', {}))244            meta = op.get('x-oaiMeta', {})245            out[f"{method.upper()} {p}"] = {'operationId': op.get('operationId'), 'summary': clean(op.get('summary')),246                                            'description': clean(op.get('description')), 'parameters': params,247                                            'requestBody': body_ref, 'responses': resp, 'deprecated': op.get('deprecated', False),248                                            'meta': {k: (clean(v) if isinstance(v, str) else v) for k, v in meta.items() if k in ('name', 'group', 'returns', 'beta', 'path')}}249    return out250251252if __name__ == '__main__':253    cmd = sys.argv[1]254    if cmd == 'list':255        rx = re.compile(sys.argv[2])256        print('\n'.join(n for n in SCHEMAS if rx.search(n)))257    elif cmd == 'schema':258        print(json.dumps(show_schema(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 3), indent=1))259    elif cmd == 'flatten':260        rows = []261        flatten({'$ref': f'#/components/schemas/{sys.argv[2]}'}, '', False, rows, int(sys.argv[3]) if len(sys.argv) > 3 else 6, [])262        print(json.dumps(rows, indent=None))263    elif cmd == 'events':264        print(json.dumps(events(sys.argv[2]), indent=1))265    elif cmd == 'paths':266        print(json.dumps(paths(sys.argv[2:]), indent=1))267