API surface

Signatures, documentation and grouped namespaces for generated API clients

apisurface builds signatures, docstrings and grouped namespaces from API operation metadata. Generated clients can use it to support tab completion and documentation through ? and doc(). fastspec uses it for HTTP clients generated from specifications.

An operation record can be any object with these attributes:

mk_sig creates an inspect.Signature with Python parameter names. mk_doc creates a docstring. OpGroup and mk_groups organize operations into namespaces with attribute access. full_docs combines their overviews into Markdown.

Names


source

snake

def snake(
    s:str
):

Convert an identifier-ish string to snake_case.

snake converts names such as tunedModels.generateContent, max-tokens and networkAccessAllowed to snake case. Unlike camel2snake, it also replaces runs of non-alphanumeric characters with underscores:

test_eq(snake('tunedModels.generateContent'), 'tuned_models_generate_content')
test_eq(snake('max-tokens'), 'max_tokens')
test_eq(snake('networkAccessAllowed'), 'network_access_allowed')
snake('HTMLParser v2')
'html_parser_v2'

sanitize_param_name replaces non-word characters before applying snake.


source

sanitize_param_name

def sanitize_param_name(
    p
):

Signatures

The examples use a chat-message operation. Its parameters include a hyphenated name and a Python keyword:

send = SimpleNamespace(
    group='messages', name='send', summary='Send a chat message',
    docs_url='https://api.example.com/docs#send',
    params=['model', 'input', 'max-tokens', 'for', 'stream'],
    required_params=['model', 'input'],
    param_types=dict(model=str, input=str, stream=bool, **{'max-tokens':int, 'for':str}),
    param_defaults=dict(stream=False),
    param_docs={'model':'Model id', 'input':'Prompt text', 'max-tokens':'Cap on generated tokens', 'for':'End-user id'})

source

sanitized_params

def sanitized_params(
    ps
):

Mapping from spec param names ps to valid Python identifiers; exact names win collisions.

sanitized_params converts max-tokens to max_tokens and appends an underscore to the keyword for. It adds further underscores to resolve collisions. An unchanged name takes precedence over a converted one.

sparams = sanitized_params(send.params)
test_eq(sparams['max-tokens'], 'max_tokens')
test_eq(sparams['for'], 'for_')
sparams
{'model': 'model',
 'input': 'input',
 'max-tokens': 'max_tokens',
 'for': 'for_',
 'stream': 'stream'}

source

mk_sig

def mk_sig(
    op, sparams:NoneType=None, defaults:NoneType=None
):

An operation signature with parameter descriptions in Annotated; defaults makes those params optional

mk_sig places required parameters before optional parameters. An optional parameter without a default uses UNSET to represent omission. None remains a separate value for clients that send JSON null.

Parameter descriptions use Annotated metadata. docments reads them without source code, including when delegates copies them to a wrapper.

sig = mk_sig(send)
test_eq(typing.get_args(sig.parameters['model'].annotation), (str, 'Model id'))
sig
<Signature (model: Annotated[str, 'Model id'], input: Annotated[str, 'Prompt text'], max_tokens: Annotated[int, 'Cap on generated tokens'] = UNSET, for_: Annotated[str, 'End-user id'] = UNSET, stream: bool = False)>

Passing defaults makes those parameters optional with the given values. This supports client-level binding without changing the spec. An explicit None remains distinct from UNSET.

bound = mk_sig(send, defaults={'model':'sonnet-4', 'for':None})
test_eq(bound.parameters['model'].default, 'sonnet-4')
test_is(bound.parameters['for_'].default, None)
test_is(mk_sig(send, defaults={'model':None}).parameters['model'].default, None)
bound
<Signature (input: Annotated[str, 'Prompt text'], max_tokens: Annotated[int, 'Cap on generated tokens'] = UNSET, model: Annotated[str, 'Model id'] = 'sonnet-4', for_: Annotated[str, 'End-user id'] = None, stream: bool = False)>

Docstrings

_op_summary uses the operation name when a summary is absent. Whitespace in spec summaries collapses to one space.

TestOp = namedtuple('TestOp', 'summary name docs_url')
test_eq(_op_summary(TestOp("List models", "list", "")), "List models")
test_eq(_op_summary(TestOp("", "list_models", "")), "list_models")
test_eq(_op_summary(TestOp(None, "list_models", "")), "list_models")

test_eq(_op_summary(TestOp("List  all\n models", "list", "")), "List all models")

Root-relative links resolve against docs_url. Anchor-only links and absolute URLs remain unchanged.

test_eq(_op_summary(TestOp("[details](/docs#rate)", "x", "https://api.example.com/docs")),
    "[details](https://api.example.com/docs#rate)")
test_eq(_op_summary(TestOp("[see](#limits)", "x", "https://api.example.com/docs")), "[see](#limits)")
test_eq(_op_summary(TestOp("[info](https://other.com/x)", "x", "https://api.example.com/docs")),
    "[info](https://other.com/x)")

source

mk_doc

def mk_doc(
    op, sig, sparams
):

Render operation docstring with summary, docs URL, and parameter hints.

mk_doc includes the summary, documentation link and parameter details. Each parameter uses its Python name, with a type, required status or default, and description:

send.__signature__ = sig
send.__doc__ = mk_doc(send, sig, sparams)
PrettyString(send.__doc__)
Send a chat message

Docs: https://api.example.com/docs#send

Parameters:
- model (str, required): Model id
- input (str, required): Prompt text
- max_tokens (int, optional): Cap on generated tokens
- for_ (str, optional): End-user id
- stream (bool, default: False)

Groups


source

OpGroup

def OpGroup(
    name:str, ops
):

OpGroup exposes each operation as an attribute. Its overview lists operations with signatures; each operation retains its own parameter documentation.

messages = OpGroup('messages', [send])
assert 'messages.send' in messages.__doc__
messages
  • messages.send(model, input, max_tokens, for_, stream): Send a chat message

Overview only. Read doc(group.operation) for parameter details and doc(group.subgroup) to descend.


source

mk_groups

def mk_groups(
    ops
):

Nested tree of OpGroups from ops, following each op’s group path.

mk_groups builds a tree from each operation’s group: a string for a flat namespace, a list for nesting. Here a second copy of send belongs to the messages.batches subgroup.

batch_send = SimpleNamespace(**vars(send))
batch_send.group = ['messages', 'batches']
groups = mk_groups([send, batch_send])
test_is(groups['messages'].batches.send, batch_send)
groups['messages']
  • messages.send(model, input, max_tokens, for_, stream): Send a chat message
  • batches/

Overview only. Read doc(group.operation) for parameter details and doc(group.subgroup) to descend.

Attribute access descends through subgroups to the operation, which retains its parameter documentation:

PrettyString(groups['messages'].batches.send.__doc__)
Send a chat message

Docs: https://api.example.com/docs#send

Parameters:
- model (str, required): Model id
- input (str, required): Prompt text
- max_tokens (int, optional): Cap on generated tokens
- for_ (str, optional): End-user id
- stream (bool, default: False)

OpGroup.__allow__ returns the group’s operations and immediate subgroups. Hosts whose allow mechanism follows __allow__ recursively can register the whole group in one call.

Reference docs


source

full_docs

def full_docs(
    groups
):

Markdown overview of every group and operation in a mk_groups tree.

full_docs combines the group overviews into one Markdown document, including nested groups. It lists operation names, parameter names, and summaries. Parameter types, defaults, and descriptions remain in each operation’s documentation.

PrettyString(full_docs(groups))
## messages

- [messages.send](https://api.example.com/docs#send)(model, input, max_tokens, for_, stream): *Send a chat message*
- batches/

### messages.batches

- [messages.batches.send](https://api.example.com/docs#send)(model, input, max_tokens, for_, stream): *Send a chat message*