Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-site/docs/guides/coverage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This page maps major OpenAPI areas to SDK resources and methods. All requests us
- Destinations (Data Sinks): `client.destinations`
- CRUD/activate/pause/copy
- Nexsets (Data Sets): `client.nexsets`
- CRUD/activate/pause/samples/copy/docs_recommendation
- CRUD/activate/pause/samples/copy/docs_recommendation/list_docs/update_docs/copy_docs
- Credentials: `client.credentials`
- CRUD/probe/probe_tree/probe_sample (async/request_id)
- Data Maps (Lookups): `client.lookups`
Expand Down
3 changes: 2 additions & 1 deletion nexla_sdk/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
DestinationType,
DestinationUpdate,
)
from nexla_sdk.models.doc_containers import DocContainer
from nexla_sdk.models.doc_containers import DocContainer, DocContainerInput
from nexla_sdk.models.enums import (
AccessRole,
ConnectorCategory,
Expand Down Expand Up @@ -377,6 +377,7 @@
"BlockedDomain",
# Doc containers / Data schemas
"DocContainer",
"DocContainerInput",
"DataSchema",
# Webhooks
"WebhookSendOptions",
Expand Down
2 changes: 2 additions & 0 deletions nexla_sdk/models/doc_containers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .requests import DocContainerInput
from .responses import DocContainer

__all__ = [
"DocContainer",
"DocContainerInput",
]
24 changes: 24 additions & 0 deletions nexla_sdk/models/doc_containers/requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Request models for doc containers."""

from typing import Optional

from pydantic import ConfigDict

from nexla_sdk.models.base import BaseModel


class DocContainerInput(BaseModel):
"""Writable fields for creating or replacing a documentation entry.

Server-owned and read-only fields (``id``, ``owner``, ``org``,
``public``, ``tags``, ``created_at`` etc.) are silently ignored on
construction so a full ``DocContainer`` response can be round-tripped
through this model to drop fields the API does not accept on input.
"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Docstring doesn't explain why repo_type / repo_config are excluded

Issue: The class docstring lists id, owner, org, public, tags, and created_at as intentionally excluded, but doesn't mention repo_type or repo_config. The PR description documents the reasoning for public and tags specifically; a future maintainer seeing a 400 from a POST with repo_type included will have no breadcrumb here.

Suggestion: Add a brief note (matching the existing PR description rationale) about repo_type / repo_config:

    Server-owned and read-only fields (``id``, ``owner``, ``org``,
    ``public``, ``tags``, ``created_at`` etc.) are silently ignored on
    construction so a full ``DocContainer`` response can be round-tripped
    through this model to drop fields the API does not accept on input.
    ``repo_type`` and ``repo_config`` are also excluded pending confirmation
    that the API accepts them on write.

Generated by Claude Code


model_config = ConfigDict(extra="ignore")

name: str
description: Optional[str] = None
doc_type: str = "md"
text: str
21 changes: 20 additions & 1 deletion nexla_sdk/models/doc_containers/responses.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
from typing import Optional
from datetime import datetime
from typing import Any, Dict, List, Optional

from pydantic import Field

from nexla_sdk.models.base import BaseModel
from nexla_sdk.models.common import Organization, Owner


class DocContainer(BaseModel):
"""Documentation container attached to a Nexla resource (e.g. a nexset)."""

id: int
owner: Optional[Owner] = None
org: Optional[Organization] = None
name: Optional[str] = None
description: Optional[str] = None
doc_type: Optional[str] = None
public: Optional[bool] = None
repo_type: Optional[str] = None
repo_config: Optional[Dict[str, Any]] = None
text: Optional[str] = None
access_roles: List[str] = Field(default_factory=list)
tags: List[str] = Field(default_factory=list)
copied_from_id: Optional[int] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
119 changes: 118 additions & 1 deletion nexla_sdk/resources/nexsets.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union

from nexla_sdk.models.doc_containers import DocContainer, DocContainerInput
from nexla_sdk.models.nexsets.requests import (
NexsetCopyOptions,
NexsetCreate,
Expand Down Expand Up @@ -162,3 +163,119 @@ def docs_recommendation(self, set_id: int) -> Dict[str, Any]:
"""Generate AI suggestion for Nexset documentation."""
path = f"{self._path}/{set_id}/docs/recommendation"
return self._make_request("POST", path)

def list_docs(self, set_id: int, expand: bool = True) -> List[DocContainer]:
"""List documentation entries attached to a nexset.

Each entry is a ``DocContainer`` carrying the rich documentation body
in its ``text`` field (typically markdown), along with metadata such
as owner, org, doc type, and timestamps.

Args:
set_id: Nexset ID.
expand: When ``True`` (default), pass ``expand=1`` to the API to
include nested ``owner`` and ``org`` details in each entry.

Returns:
List of ``DocContainer`` instances. Empty list if the nexset has
no documentation.

Examples:
Read the markdown body of the first doc on a nexset::

docs = client.nexsets.list_docs(419706)
if docs:
print(docs[0].text)
"""
path = f"{self._path}/{set_id}/docs"
params = {"expand": 1} if expand else {}
response = self._make_request("GET", path, params=params)
if isinstance(response, list):
return [DocContainer.model_validate(item) for item in response]
return []
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Silent return [] on non-list response (same applies to update_docs at line 242)

Issue: If the API ever returns a non-list shape (e.g. a dict or null) for these endpoints — say, during a server-side change or transient error that slips past HTTP-level checking — both list_docs and update_docs silently return []. The caller receives an empty result and has no way to distinguish "no docs" from "unexpected response".

This pattern is consistent with the rest of the SDK (get_accessors, etc.), so it's not blocking. Worth tracking as a broader SDK concern: consider adding a warning log or raising NexlaError when isinstance(response, list) is unexpectedly False.


Generated by Claude Code


def update_docs(
self,
set_id: int,
docs: List[Union[DocContainerInput, Dict[str, Any]]],
) -> List[DocContainer]:
"""Replace all documentation entries on a nexset.

This call uses **replace-all** semantics: the provided list becomes
the entire set of docs for the nexset. Any existing entries that are
not in the list are removed.

Args:
set_id: Nexset ID.
docs: New documentation entries. Each item may be a
``DocContainerInput`` instance or a plain dict with the same
shape (``name``, ``text``, ``doc_type``, optional
``description``). Plain dicts are accepted to support
generic pass-through callers (e.g. the MCP server, raw
scripts).

Returns:
The new list of ``DocContainer`` entries as parsed from the
server response.
Comment on lines +217 to +219
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Returns: section is mis-indented inside Args:

Issue: The Returns: block (lines 217-219) is indented at 12 spaces — the same level as individual parameter descriptions — rather than 8 spaces (same level as Args:). Sphinx/autodoc and most docstring parsers treat it as a continuation of the docs parameter description rather than a top-level section, so rendered API docs will look broken.

Suggestion: Dedent Returns: and its body by one level:

        Args:
            set_id: Nexset ID.
            docs: ...

        Returns:
            The new list of ``DocContainer`` entries as parsed from the
            server response.

Generated by Claude Code


Examples:
Replace a nexset's docs with a single markdown entry::

from nexla_sdk.models import DocContainerInput

client.nexsets.update_docs(
419706,
[DocContainerInput(
name="Overview",
description="High-level description",
text="# Overview\\n\\n...",
)],
)
"""
path = f"{self._path}/{set_id}/docs"
serialized = [self._serialize_data(d) for d in docs]
response = self._make_request(
"POST", path, json={"docs": serialized}
)
if isinstance(response, list):
return [DocContainer.model_validate(item) for item in response]
return []

def copy_docs(self, src_id: int, dst_id: int) -> List[DocContainer]:
"""Copy all documentation entries from one nexset to another.

Reads the source's docs, strips server-owned fields (id, owner, org,
access_roles, copied_from_id, created_at, updated_at), and writes
them to the destination using ``update_docs`` (replace-all
semantics). Existing docs on the destination are overwritten.

If the source has no docs, this is a no-op: the destination is left
unchanged and an empty list is returned.

Note: only fields present on ``DocContainerInput`` are carried over.
Any new server-side doc fields will be ignored until
``DocContainerInput`` is extended.

Args:
src_id: Source nexset ID (docs are read from here).
dst_id: Destination nexset ID (docs are written here).

Returns:
The destination's new list of ``DocContainer`` entries, or an
empty list if the source had no docs.

Examples:
Copy docs from one nexset to another::

client.nexsets.copy_docs(src_id=419706, dst_id=419800)
"""
source_docs = self.list_docs(src_id)
if not source_docs:
return []
payload = [
DocContainerInput.model_validate(
doc.model_dump(exclude_none=True)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] copy_docs raises ValidationError when a source doc has text=None

Issue: DocContainer.text is Optional[str], so the API can return null for text. When that happens, model_dump(exclude_none=True) omits text from the dict entirely. DocContainerInput.model_validate(...) then raises a ValidationError ("field required") because DocContainerInput.text: str has no default. The caller gets an unhandled exception mid-copy with no indication of which doc caused it.

Suggestion: Drop exclude_none=True from the model_dump call here. With DocContainerInput's extra="ignore" policy, server-owned fields are still stripped regardless. text=None will then cause a type-validation failure (str doesn't accept None), which is arguably correct — but at least the error message will be clearer. Alternatively, pre-filter docs with None text and document the behaviour:

valid_docs = [doc for doc in source_docs if doc.text is not None]
if not valid_docs:
    return []
payload = [
    DocContainerInput.model_validate(doc.model_dump())
    for doc in valid_docs
]

Generated by Claude Code

)
for doc in source_docs
]
return self.update_docs(dst_id, payload)
Loading
Loading