# API surface


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

A client generated from a machine-readable spec (an OpenAPI document, a
Google Discovery document, an SDK symbol graph) is only pleasant to use
if its runtime-created callables behave like hand-written ones: tab
completion shows real parameters, `?` and `doc()` show real docs, and
related operations sit together in namespaces you can browse. This
module builds those pieces from plain *operation records*, ducks with
these attributes: `name`, `group` (a nesting path: string or list),
`summary`, `docs_url`, `params` (ordered parameter names),
`required_params`, `param_types`, `param_defaults`, and `param_docs`.
[`mk_sig`](https://fastcore.fast.ai/apisurface.html#mk_sig) turns a
record into an `inspect.Signature` (spec names sanitized to Python
identifiers via
[`sanitized_params`](https://fastcore.fast.ai/apisurface.html#sanitized_params)),
[`mk_doc`](https://fastcore.fast.ai/apisurface.html#mk_doc) renders a
docstring, and
[`OpGroup`](https://fastcore.fast.ai/apisurface.html#opgroup)/[`mk_groups`](https://fastcore.fast.ai/apisurface.html#mk_groups)/[`full_docs`](https://fastcore.fast.ai/apisurface.html#full_docs)
assemble named callables into an attribute-chained tree with markdown
summaries at every level.

`fastspec` builds its HTTP clients on this layer.

## Names

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/apisurface.py#L25"
target="_blank" style="float:right; font-size:smaller">source</a>

### snake

``` python
def snake(
    s:str
):
```

*Convert an identifier-ish string to snake_case.*

Spec names arrive in every convention at once: OpenAPI operation ids
like `tunedModels.generateContent`, header-ish parameter names like
`max-tokens`, Swift argument labels like `networkAccessAllowed`.
[`snake`](https://fastcore.fast.ai/apisurface.html#snake) normalizes any
of them to one Python style. It differs from
[`camel2snake`](https://fastcore.fast.ai/basics.html#camel2snake) by
also folding runs of non-alphanumeric characters into underscores, so
dotted and hyphenated names come out as identifiers too.

``` python
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`](https://fastcore.fast.ai/apisurface.html#sanitize_param_name)
is the stricter form for parameter names, which must be assignable:
anything non-identifier becomes `_` before snaking.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/apisurface.py#L33"
target="_blank" style="float:right; font-size:smaller">source</a>

### sanitize_param_name

``` python
def sanitize_param_name(
    p
):
```

*Call self as a function.*

## Signatures

The running example throughout: one operation record for a chat-message
API, with a hyphenated name and a Python keyword among its parameters.
Any object with these attributes works.

``` python
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'})
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/apisurface.py#L43"
target="_blank" style="float:right; font-size:smaller">source</a>

### sanitized_params

``` python
def sanitized_params(
    ps
):
```

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

Two of those parameter names can’t be Python parameters as-is:
`max-tokens` isn’t an identifier, and `for` is a keyword.
[`sanitized_params`](https://fastcore.fast.ai/apisurface.html#sanitized_params)
maps every spec name to a usable identifier, leaving good names alone; a
name that’s already exact always keeps its spelling, and invented names
grow underscores until they’re unique.

``` python
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'}

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/apisurface.py#L65"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_sig

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

*An `inspect.Signature` for operation record `op`; `defaults` values
make their params optional.*

[`mk_sig`](https://fastcore.fast.ai/apisurface.html#mk_sig) builds the
real `inspect.Signature`: required parameters first, then optional ones.
An optional parameter with no spec default gets the `UNSET` sentinel, so
a wrapper can distinguish “caller omitted this” from any real value.

``` python
sig = mk_sig(send)
sig
```

    <Signature (model: str, input: str, max_tokens: int = UNSET, for_: str = UNSET, stream: bool = False)>

Passing `defaults` makes those parameters optional with the given values
— useful for client-level binding (an API version, a repo name) without
touching the spec.

``` python
mk_sig(send, defaults={'model':'sonnet-4'})
```

    <Signature (input: str, max_tokens: int = UNSET, for_: str = UNSET, model: str = 'sonnet-4', stream: bool = False)>

## Docstrings

Summaries come from spec prose, so
[`_op_summary`](https://fastcore.fast.ai/apisurface.html#_op_summary)
flattens whitespace and rewrites relative markdown links against
`docs_url` — a summary that reads correctly in isolation is what group
listings and docstrings both need.

``` python
TestOp = namedtuple('TestOp', 'summary name docs_url')

# Summary passthrough, and fallback to the op name
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")

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

# Relative link rewriting against docs_url; anchor-only and absolute links untouched
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)")
```

[`_op_line`](https://fastcore.fast.ai/apisurface.html#_op_line) renders
one op as a markdown bullet for group listings: dotted group path,
linked to its docs page, with the signature’s parameters inline.

``` python
_op_line(send, sig)
```

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

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/apisurface.py#L93"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_doc

``` python
def mk_doc(
    op, sig, sparams
):
```

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

[`mk_doc`](https://fastcore.fast.ai/apisurface.html#mk_doc) renders the
docstring a generated callable should carry: summary, docs link, and one
line per parameter with its type, requiredness or default, and spec
description — under the *sanitized* name the caller will actually type.

``` python
print(mk_doc(send, sig, sparams))
```

    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

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/apisurface.py#L111"
target="_blank" style="float:right; font-size:smaller">source</a>

### OpGroup

``` python
def OpGroup(
    name:str, ops
):
```

*Namespace for grouped operations: each op is an attribute, and the repr
lists them all*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/apisurface.py#L129"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_groups

``` python
def mk_groups(
    ops
):
```

*Nested tree of
[`OpGroup`](https://fastcore.fast.ai/apisurface.html#opgroup)s from ops,
following each op’s `group` path.*

Ops declare where they live via `group`: a string for a flat namespace,
a list for nesting.
[`mk_groups`](https://fastcore.fast.ai/apisurface.html#mk_groups) builds
the tree, [`OpGroup`](https://fastcore.fast.ai/apisurface.html#opgroup)
holds one level of it, and attribute chaining walks it. Group names are
snaked, so `tunedModels` arrives as `tuned_models`.

``` python
TestOp = namedtuple('TestOp', 'group name docs_url')

ops = [TestOp("repos", "get", ""), TestOp("repos", "list", ""), TestOp(["tuned_models"], "get", ""),
    TestOp(["tuned_models", "permissions"], "list", ""), TestOp(["tuned_models", "permissions"], "delete", "")]
```

``` python
op_groups = mk_groups(ops)
op_groups
```

    {'repos': , 'tuned_models': }

Nested groups chain as attributes, and each level is itself an
[`OpGroup`](https://fastcore.fast.ai/apisurface.html#opgroup):

``` python
test_eq(isinstance(op_groups['tuned_models'].permissions, OpGroup), True)
```

``` python
op_groups['tuned_models'].get, op_groups['tuned_models'].permissions.delete, op_groups['tuned_models'].permissions.list
```

    (TestOp(group=['tuned_models'], name='get', docs_url=''),
     TestOp(group=['tuned_models', 'permissions'], name='delete', docs_url=''),
     TestOp(group=['tuned_models', 'permissions'], name='list', docs_url=''))

``` python
op_groups['repos'].get, op_groups['repos'].list
```

    (TestOp(group='repos', name='get', docs_url=''),
     TestOp(group='repos', name='list', docs_url=''))

An op that carries a `__signature__` (anything dressed by
[`mk_sig`](https://fastcore.fast.ai/apisurface.html#mk_sig), or a real
callable) contributes an
[`_op_line`](https://fastcore.fast.ai/apisurface.html#_op_line) bullet
to its group’s `__doc__`, so the group’s repr *is* its documentation:

``` python
sendop = SimpleNamespace(**vars(send), __signature__=sig)
groups = mk_groups([sendop])
assert 'messages.send' in groups['messages'].__doc__
groups['messages']
```

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

[`OpGroup.__allow__`](https://fastcore.fast.ai/apisurface.html#opgroup.__allow__)
supports hosts whose `allow` mechanism registers callable surfaces
recursively: allowing a group allows its ops and every nested subgroup
in one call.

## Reference docs

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/fastcore/blob/main/fastcore/apisurface.py#L152"
target="_blank" style="float:right; font-size:smaller">source</a>

### full_docs

``` python
def full_docs(
    groups
):
```

*Complete markdown API reference for a
[`mk_groups`](https://fastcore.fast.ai/apisurface.html#mk_groups) tree:
every group and operation.*

[`full_docs`](https://fastcore.fast.ai/apisurface.html#full_docs) walks
a [`mk_groups`](https://fastcore.fast.ai/apisurface.html#mk_groups) tree
and emits one markdown document covering everything — an always-current
API reference for feeding to a docs page or an LLM.

``` python
print(full_docs(groups))
```

    ## messages

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