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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.prism.log
.vscode
_dev

__pycache__
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "2.2.1"
".": "2.3.0-rc1"
}
8 changes: 4 additions & 4 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 32
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-fab7a71148b6f413a4425ca9f2ce3d42557b65d35ab28c6f64daa7fce6d0ffe2.yml
openapi_spec_hash: 0ead6944545bc40172176e15cc704633
config_hash: c0c9f57ab19252f82cf765939edc61de
configured_endpoints: 33
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-0bbaae2a69204cb3ccf6dc4fbb0cef810a4e8c78f09ac639e253e84df0add53a.yml
openapi_spec_hash: 0a9be554ca3af860e3831bd776e50f56
config_hash: 7a38bab086b53b43d2a719cb4d883264
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"python.analysis.importFormat": "relative",
}
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Changelog

## 2.3.0-rc1 (2025-08-12)

Full Changelog: [v2.2.1...v2.3.0-rc1](https://github.com/writer/writer-python/compare/v2.2.1...v2.3.0-rc1)

### Features

* **api:** add web KG and web search ([127319b](https://github.com/writer/writer-python/commit/127319bf155e738a86e344330e3b59a252ae5bd3))
* **client:** support file upload requests ([aa0d06d](https://github.com/writer/writer-python/commit/aa0d06d2b73d9912a8542de146c8c0d0d0d150b2))


### Bug Fixes

* Add web_search_data to streaming tests. ([73f3db7](https://github.com/writer/writer-python/commit/73f3db7b36bde194d971afc2b16812b88e98e079))
* **parsing:** ignore empty metadata ([8874173](https://github.com/writer/writer-python/commit/8874173e4d0d73c1209bfedee5bb29b5d720c9ec))
* **parsing:** parse extra field types ([d1f5948](https://github.com/writer/writer-python/commit/d1f5948323742a54080dbd008532ce4be17289ba))


### Chores

* **internal:** fix ruff target version ([851522b](https://github.com/writer/writer-python/commit/851522bde50ed18925feb4d07191418be908e6ec))
* **internal:** update comment in script ([279d7df](https://github.com/writer/writer-python/commit/279d7df957f5fd0b75fcd6a4bbae84e79f1f1288))
* **project:** add settings file for vscode ([1c5e20c](https://github.com/writer/writer-python/commit/1c5e20ca653e671b71dc5b8e7d82db9a9dd2a51e))
* update @stainless-api/prism-cli to v5.15.0 ([cdbd4b3](https://github.com/writer/writer-python/commit/cdbd4b3a5551b96ecda64be222ef9ac89e527794))


### Documentation

* **api:** updates to API spec ([4482f85](https://github.com/writer/writer-python/commit/4482f8521bf9e126dbe1f51e298efb1b74b2c100))
* **api:** updates to API spec ([fe26f83](https://github.com/writer/writer-python/commit/fe26f8328201752961a0e1b7d3c418c357b0ccf7))
* **api:** updates to API spec ([a780648](https://github.com/writer/writer-python/commit/a780648ff3063801cdb349ad5cc275139e7726f9))

## 2.2.1 (2025-07-16)

Full Changelog: [v2.2.0...v2.2.1](https://github.com/writer/writer-python/compare/v2.2.0...v2.2.1)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ To install the package from PyPI, use `pip`:

```sh
# install from PyPI
pip install writer-sdk
pip install --pre writer-sdk
```

## Prequisites
Expand Down Expand Up @@ -116,7 +116,7 @@ You can enable this by installing `aiohttp`:

```sh
# install from PyPI
pip install writer-sdk[aiohttp]
pip install --pre writer-sdk[aiohttp]
```

Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
Expand Down
8 changes: 6 additions & 2 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ from writerai.types import (
Question,
QuestionResponseChunk,
GraphCreateResponse,
GraphRetrieveResponse,
GraphUpdateResponse,
GraphListResponse,
GraphDeleteResponse,
GraphRemoveFileFromGraphResponse,
)
Expand All @@ -132,9 +134,9 @@ from writerai.types import (
Methods:

- <code title="post /v1/graphs">client.graphs.<a href="./src/writerai/resources/graphs.py">create</a>(\*\*<a href="src/writerai/types/graph_create_params.py">params</a>) -> <a href="./src/writerai/types/graph_create_response.py">GraphCreateResponse</a></code>
- <code title="get /v1/graphs/{graph_id}">client.graphs.<a href="./src/writerai/resources/graphs.py">retrieve</a>(graph_id) -> <a href="./src/writerai/types/graph.py">Graph</a></code>
- <code title="get /v1/graphs/{graph_id}">client.graphs.<a href="./src/writerai/resources/graphs.py">retrieve</a>(graph_id) -> <a href="./src/writerai/types/graph_retrieve_response.py">GraphRetrieveResponse</a></code>
- <code title="put /v1/graphs/{graph_id}">client.graphs.<a href="./src/writerai/resources/graphs.py">update</a>(graph_id, \*\*<a href="src/writerai/types/graph_update_params.py">params</a>) -> <a href="./src/writerai/types/graph_update_response.py">GraphUpdateResponse</a></code>
- <code title="get /v1/graphs">client.graphs.<a href="./src/writerai/resources/graphs.py">list</a>(\*\*<a href="src/writerai/types/graph_list_params.py">params</a>) -> <a href="./src/writerai/types/graph.py">SyncCursorPage[Graph]</a></code>
- <code title="get /v1/graphs">client.graphs.<a href="./src/writerai/resources/graphs.py">list</a>(\*\*<a href="src/writerai/types/graph_list_params.py">params</a>) -> <a href="./src/writerai/types/graph_list_response.py">SyncCursorPage[GraphListResponse]</a></code>
- <code title="delete /v1/graphs/{graph_id}">client.graphs.<a href="./src/writerai/resources/graphs.py">delete</a>(graph_id) -> <a href="./src/writerai/types/graph_delete_response.py">GraphDeleteResponse</a></code>
- <code title="post /v1/graphs/{graph_id}/file">client.graphs.<a href="./src/writerai/resources/graphs.py">add_file_to_graph</a>(graph_id, \*\*<a href="src/writerai/types/graph_add_file_to_graph_params.py">params</a>) -> <a href="./src/writerai/types/file.py">File</a></code>
- <code title="post /v1/graphs/question">client.graphs.<a href="./src/writerai/resources/graphs.py">question</a>(\*\*<a href="src/writerai/types/graph_question_params.py">params</a>) -> <a href="./src/writerai/types/question.py">Question</a></code>
Expand Down Expand Up @@ -166,6 +168,7 @@ from writerai.types import (
ToolAIDetectResponse,
ToolContextAwareSplittingResponse,
ToolParsePdfResponse,
ToolWebSearchResponse,
)
```

Expand All @@ -174,6 +177,7 @@ Methods:
- <code title="post /v1/tools/ai-detect">client.tools.<a href="./src/writerai/resources/tools/tools.py">ai_detect</a>(\*\*<a href="src/writerai/types/tool_ai_detect_params.py">params</a>) -> <a href="./src/writerai/types/tool_ai_detect_response.py">ToolAIDetectResponse</a></code>
- <code title="post /v1/tools/context-aware-splitting">client.tools.<a href="./src/writerai/resources/tools/tools.py">context_aware_splitting</a>(\*\*<a href="src/writerai/types/tool_context_aware_splitting_params.py">params</a>) -> <a href="./src/writerai/types/tool_context_aware_splitting_response.py">ToolContextAwareSplittingResponse</a></code>
- <code title="post /v1/tools/pdf-parser/{file_id}">client.tools.<a href="./src/writerai/resources/tools/tools.py">parse_pdf</a>(file_id, \*\*<a href="src/writerai/types/tool_parse_pdf_params.py">params</a>) -> <a href="./src/writerai/types/tool_parse_pdf_response.py">ToolParsePdfResponse</a></code>
- <code title="post /v1/tools/web-search">client.tools.<a href="./src/writerai/resources/tools/tools.py">web_search</a>(\*\*<a href="src/writerai/types/tool_web_search_params.py">params</a>) -> <a href="./src/writerai/types/tool_web_search_response.py">ToolWebSearchResponse</a></code>

## Comprehend

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "writer-sdk"
version = "2.2.1"
version = "2.3.0-rc1"
description = "The official Python library for the writer API"
dynamic = ["readme"]
license = "Apache-2.0"
Expand Down Expand Up @@ -162,7 +162,7 @@ reportPrivateUsage = false
[tool.ruff]
line-length = 120
output-format = "grouped"
target-version = "py37"
target-version = "py38"

[tool.ruff.format]
docstring-code-format = true
Expand Down
4 changes: 2 additions & 2 deletions scripts/mock
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ echo "==> Starting mock server with URL ${URL}"

# Run prism mock on the given spec
if [ "$1" == "--daemon" ]; then
npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log &
npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log &

# Wait for server to come online
echo -n "Waiting for server"
Expand All @@ -37,5 +37,5 @@ if [ "$1" == "--daemon" ]; then

echo
else
npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL"
npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL"
fi
2 changes: 1 addition & 1 deletion scripts/test
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ elif ! prism_is_running ; then
echo -e "To run the server, pass in the path or url of your OpenAPI"
echo -e "spec to the prism command:"
echo
echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}"
echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}"
echo

exit 1
Expand Down
5 changes: 4 additions & 1 deletion src/writerai/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,10 @@ def _build_request(
is_body_allowed = options.method.lower() != "get"

if is_body_allowed:
kwargs["json"] = json_data if is_given(json_data) else None
if isinstance(json_data, bytes):
kwargs["content"] = json_data
else:
kwargs["json"] = json_data if is_given(json_data) else None
kwargs["files"] = files
else:
headers.pop("Content-Type", None)
Expand Down
8 changes: 4 additions & 4 deletions src/writerai/_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ def _transform_file(file: FileTypes) -> HttpxFileTypes:
return file

if is_tuple_t(file):
return (file[0], _read_file_content(file[1]), *file[2:])
return (file[0], read_file_content(file[1]), *file[2:])

raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")


def _read_file_content(file: FileContent) -> HttpxFileContent:
def read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return pathlib.Path(file).read_bytes()
return file
Expand Down Expand Up @@ -118,12 +118,12 @@ async def _async_transform_file(file: FileTypes) -> HttpxFileTypes:
return file

if is_tuple_t(file):
return (file[0], await _async_read_file_content(file[1]), *file[2:])
return (file[0], await async_read_file_content(file[1]), *file[2:])

raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple")


async def _async_read_file_content(file: FileContent) -> HttpxFileContent:
async def async_read_file_content(file: FileContent) -> HttpxFileContent:
if isinstance(file, os.PathLike):
return await anyio.Path(file).read_bytes()

Expand Down
27 changes: 24 additions & 3 deletions src/writerai/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,18 @@ def construct( # pyright: ignore[reportIncompatibleMethodOverride]
else:
fields_values[name] = field_get_default(field)

extra_field_type = _get_extra_fields_type(__cls)

_extra = {}
for key, value in values.items():
if key not in model_fields:
parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value

if PYDANTIC_V2:
_extra[key] = value
_extra[key] = parsed
else:
_fields_set.add(key)
fields_values[key] = value
fields_values[key] = parsed

object.__setattr__(m, "__dict__", fields_values)

Expand Down Expand Up @@ -371,6 +375,23 @@ def _construct_field(value: object, field: FieldInfo, key: str) -> object:
return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None))


def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None:
if not PYDANTIC_V2:
# TODO
return None

schema = cls.__pydantic_core_schema__
if schema["type"] == "model":
fields = schema["schema"]
if fields["type"] == "model-fields":
extras = fields.get("extras_schema")
if extras and "cls" in extras:
# mypy can't narrow the type
return extras["cls"] # type: ignore[no-any-return]

return None


def is_basemodel(type_: type) -> bool:
"""Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`"""
if is_union(type_):
Expand Down Expand Up @@ -440,7 +461,7 @@ def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]
type_ = type_.__value__ # type: ignore[unreachable]

# unwrap `Annotated[T, ...]` -> `T`
if metadata is not None:
if metadata is not None and len(metadata) > 0:
meta: tuple[Any, ...] = tuple(metadata)
elif is_annotated_type(type_):
meta = get_args(type_)[1:]
Expand Down
2 changes: 1 addition & 1 deletion src/writerai/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.

__title__ = "writerai"
__version__ = "2.2.1" # x-release-please-version
__version__ = "2.3.0-rc1" # x-release-please-version
4 changes: 2 additions & 2 deletions src/writerai/resources/applications/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def update(
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
) -> ApplicationGraphsResponse:
"""
Updates the Knowledge Graphs listed and associates them with the no-code agent.
Updates the list of Knowledge Graphs associated with a no-code chat agent.

Args:
graph_ids: A list of Knowledge Graph IDs to associate with the application. Note that this
Expand Down Expand Up @@ -150,7 +150,7 @@ async def update(
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
) -> ApplicationGraphsResponse:
"""
Updates the Knowledge Graphs listed and associates them with the no-code agent.
Updates the list of Knowledge Graphs associated with a no-code chat agent.

Args:
graph_ids: A list of Knowledge Graph IDs to associate with the application. Note that this
Expand Down
Loading