Skip to content

Commit

Permalink
feat(api): add support for graphs and files endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
stainless-bot committed Jun 28, 2024
1 parent 4e5e10e commit d8f828c
Show file tree
Hide file tree
Showing 34 changed files with 2,769 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.prism.log
.vscode
_dev

Expand Down
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
configured_endpoints: 3
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-1bf320b94d765777875e8999e58068f684f81c4657bc25bf0d321b6adce778e1.yml
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/writerai%2Fwriter-e54b835d1d64d37343017f4919c60998496a1b2b5c8e0c67af82093efc613f2f.yml
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ You can directly override the [httpx client](https://www.python-httpx.org/api/#c

- Support for proxies
- Custom transports
- Additional [advanced](https://www.python-httpx.org/advanced/#client-instances) functionality
- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality

```python
from writerai import Writer, DefaultHttpxClient
Expand Down
40 changes: 40 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,43 @@ from writerai.types import ModelListResponse
Methods:

- <code title="get /v1/models">client.models.<a href="./src/writerai/resources/models.py">list</a>() -> <a href="./src/writerai/types/model_list_response.py">ModelListResponse</a></code>

# Graphs

Types:

```python
from writerai.types import (
Graph,
GraphCreateResponse,
GraphUpdateResponse,
GraphDeleteResponse,
GraphRemoveFileFromGraphResponse,
)
```

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="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="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="delete /v1/graphs/{graph_id}/file/{file_id}">client.graphs.<a href="./src/writerai/resources/graphs.py">remove_file_from_graph</a>(file_id, \*, graph_id) -> <a href="./src/writerai/types/graph_remove_file_from_graph_response.py">GraphRemoveFileFromGraphResponse</a></code>

# Files

Types:

```python
from writerai.types import File, FileDeleteResponse
```

Methods:

- <code title="get /v1/files/{fileId}">client.files.<a href="./src/writerai/resources/files.py">retrieve</a>(file_id) -> <a href="./src/writerai/types/file.py">File</a></code>
- <code title="get /v1/files">client.files.<a href="./src/writerai/resources/files.py">list</a>(\*\*<a href="src/writerai/types/file_list_params.py">params</a>) -> <a href="./src/writerai/types/file.py">SyncCursorPage[File]</a></code>
- <code title="delete /v1/files/{fileId}">client.files.<a href="./src/writerai/resources/files.py">delete</a>(file_id) -> <a href="./src/writerai/types/file_delete_response.py">FileDeleteResponse</a></code>
- <code title="get /v1/files/{fileId}/download">client.files.<a href="./src/writerai/resources/files.py">download</a>(file_id) -> BinaryAPIResponse</code>
- <code title="post /v1/files">client.files.<a href="./src/writerai/resources/files.py">upload</a>(\*\*<a href="src/writerai/types/file_upload_params.py">params</a>) -> <a href="./src/writerai/types/file.py">File</a></code>
3 changes: 3 additions & 0 deletions bin/publish-pypi
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
set -eux
mkdir -p dist
rye build --clean
# Patching importlib-metadata version until upstream library version is updated
# https://github.com/pypa/twine/issues/977#issuecomment-2189800841
"$HOME/.rye/self/bin/python3" -m pip install 'importlib-metadata==7.2.1'
rye publish --yes --token=$PYPI_TOKEN
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ include = [
[tool.hatch.build.targets.wheel]
packages = ["src/writerai"]

[tool.hatch.build.targets.sdist]
# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc)
include = [
"/*.toml",
"/*.json",
"/*.lock",
"/*.md",
"/mypy.ini",
"/noxfile.py",
"bin/*",
"examples/*",
"src/*",
"tests/*",
]

[tool.hatch.metadata.hooks.fancy-pypi-readme]
content-type = "text/markdown"

Expand Down
3 changes: 2 additions & 1 deletion requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
-e file:.
annotated-types==0.6.0
# via pydantic
anyio==4.1.0
anyio==4.4.0
# via httpx
# via writer-sdk
argcomplete==3.1.2
Expand Down Expand Up @@ -86,6 +86,7 @@ tomli==2.0.1
# via mypy
# via pytest
typing-extensions==4.8.0
# via anyio
# via mypy
# via pydantic
# via pydantic-core
Expand Down
3 changes: 2 additions & 1 deletion requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
-e file:.
annotated-types==0.6.0
# via pydantic
anyio==4.1.0
anyio==4.4.0
# via httpx
# via writer-sdk
certifi==2023.7.22
Expand Down Expand Up @@ -38,6 +38,7 @@ sniffio==1.3.0
# via httpx
# via writer-sdk
typing-extensions==4.8.0
# via anyio
# via pydantic
# via pydantic-core
# via writer-sdk
25 changes: 20 additions & 5 deletions src/writerai/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
RequestOptions,
ModelBuilderProtocol,
)
from ._utils import is_dict, is_list, is_given, lru_cache, is_mapping
from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping
from ._compat import model_copy, model_dump
from ._models import GenericModel, FinalRequestOptions, validate_type, construct_type
from ._response import (
Expand Down Expand Up @@ -358,6 +358,7 @@ def __init__(
self._custom_query = custom_query or {}
self._strict_response_validation = _strict_response_validation
self._idempotency_header = None
self._platform: Platform | None = None

if max_retries is None: # pyright: ignore[reportUnnecessaryComparison]
raise TypeError(
Expand Down Expand Up @@ -456,7 +457,7 @@ def _build_request(
raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`")

headers = self._build_headers(options)
params = _merge_mappings(self._custom_query, options.params)
params = _merge_mappings(self.default_query, options.params)
content_type = headers.get("Content-Type")

# If the given Content-Type header is multipart/form-data then it
Expand Down Expand Up @@ -592,6 +593,12 @@ def default_headers(self) -> dict[str, str | Omit]:
**self._custom_headers,
}

@property
def default_query(self) -> dict[str, object]:
return {
**self._custom_query,
}

def _validate_headers(
self,
headers: Headers, # noqa: ARG002
Expand All @@ -616,7 +623,10 @@ def base_url(self, url: URL | str) -> None:
self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url))

def platform_headers(self) -> Dict[str, str]:
return platform_headers(self._version)
# the actual implementation is in a separate `lru_cache` decorated
# function because adding `lru_cache` to methods will leak memory
# https://github.com/python/cpython/issues/88476
return platform_headers(self._version, platform=self._platform)

def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None:
"""Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified.
Expand Down Expand Up @@ -1492,6 +1502,11 @@ async def _request(
stream_cls: type[_AsyncStreamT] | None,
remaining_retries: int | None,
) -> ResponseT | _AsyncStreamT:
if self._platform is None:
# `get_platform` can make blocking IO calls so we
# execute it earlier while we are in an async context
self._platform = await asyncify(get_platform)()

cast_to = self._maybe_override_cast_to(cast_to, options)
await self._prepare_options(options)

Expand Down Expand Up @@ -1915,11 +1930,11 @@ def get_platform() -> Platform:


@lru_cache(maxsize=None)
def platform_headers(version: str) -> Dict[str, str]:
def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]:
return {
"X-Stainless-Lang": "python",
"X-Stainless-Package-Version": version,
"X-Stainless-OS": str(get_platform()),
"X-Stainless-OS": str(platform or get_platform()),
"X-Stainless-Arch": str(get_architecture()),
"X-Stainless-Runtime": get_python_runtime(),
"X-Stainless-Runtime-Version": get_python_version(),
Expand Down
16 changes: 16 additions & 0 deletions src/writerai/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Writer(SyncAPIClient):
chat: resources.ChatResource
completions: resources.CompletionsResource
models: resources.ModelsResource
graphs: resources.GraphsResource
files: resources.FilesResource
with_raw_response: WriterWithRawResponse
with_streaming_response: WriterWithStreamedResponse

Expand Down Expand Up @@ -111,6 +113,8 @@ def __init__(
self.chat = resources.ChatResource(self)
self.completions = resources.CompletionsResource(self)
self.models = resources.ModelsResource(self)
self.graphs = resources.GraphsResource(self)
self.files = resources.FilesResource(self)
self.with_raw_response = WriterWithRawResponse(self)
self.with_streaming_response = WriterWithStreamedResponse(self)

Expand Down Expand Up @@ -223,6 +227,8 @@ class AsyncWriter(AsyncAPIClient):
chat: resources.AsyncChatResource
completions: resources.AsyncCompletionsResource
models: resources.AsyncModelsResource
graphs: resources.AsyncGraphsResource
files: resources.AsyncFilesResource
with_raw_response: AsyncWriterWithRawResponse
with_streaming_response: AsyncWriterWithStreamedResponse

Expand Down Expand Up @@ -285,6 +291,8 @@ def __init__(
self.chat = resources.AsyncChatResource(self)
self.completions = resources.AsyncCompletionsResource(self)
self.models = resources.AsyncModelsResource(self)
self.graphs = resources.AsyncGraphsResource(self)
self.files = resources.AsyncFilesResource(self)
self.with_raw_response = AsyncWriterWithRawResponse(self)
self.with_streaming_response = AsyncWriterWithStreamedResponse(self)

Expand Down Expand Up @@ -398,27 +406,35 @@ def __init__(self, client: Writer) -> None:
self.chat = resources.ChatResourceWithRawResponse(client.chat)
self.completions = resources.CompletionsResourceWithRawResponse(client.completions)
self.models = resources.ModelsResourceWithRawResponse(client.models)
self.graphs = resources.GraphsResourceWithRawResponse(client.graphs)
self.files = resources.FilesResourceWithRawResponse(client.files)


class AsyncWriterWithRawResponse:
def __init__(self, client: AsyncWriter) -> None:
self.chat = resources.AsyncChatResourceWithRawResponse(client.chat)
self.completions = resources.AsyncCompletionsResourceWithRawResponse(client.completions)
self.models = resources.AsyncModelsResourceWithRawResponse(client.models)
self.graphs = resources.AsyncGraphsResourceWithRawResponse(client.graphs)
self.files = resources.AsyncFilesResourceWithRawResponse(client.files)


class WriterWithStreamedResponse:
def __init__(self, client: Writer) -> None:
self.chat = resources.ChatResourceWithStreamingResponse(client.chat)
self.completions = resources.CompletionsResourceWithStreamingResponse(client.completions)
self.models = resources.ModelsResourceWithStreamingResponse(client.models)
self.graphs = resources.GraphsResourceWithStreamingResponse(client.graphs)
self.files = resources.FilesResourceWithStreamingResponse(client.files)


class AsyncWriterWithStreamedResponse:
def __init__(self, client: AsyncWriter) -> None:
self.chat = resources.AsyncChatResourceWithStreamingResponse(client.chat)
self.completions = resources.AsyncCompletionsResourceWithStreamingResponse(client.completions)
self.models = resources.AsyncModelsResourceWithStreamingResponse(client.models)
self.graphs = resources.AsyncGraphsResourceWithStreamingResponse(client.graphs)
self.files = resources.AsyncFilesResourceWithStreamingResponse(client.files)


Client = Writer
Expand Down
4 changes: 4 additions & 0 deletions src/writerai/_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@
maybe_transform as maybe_transform,
async_maybe_transform as async_maybe_transform,
)
from ._reflection import (
function_has_argument as function_has_argument,
assert_signatures_in_sync as assert_signatures_in_sync,
)
42 changes: 42 additions & 0 deletions src/writerai/_utils/_reflection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from __future__ import annotations

import inspect
from typing import Any, Callable


def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool:
"""Returns whether or not the given function has a specific parameter"""
sig = inspect.signature(func)
return arg_name in sig.parameters


def assert_signatures_in_sync(
source_func: Callable[..., Any],
check_func: Callable[..., Any],
*,
exclude_params: set[str] = set(),
) -> None:
"""Ensure that the signature of the second function matches the first."""

check_sig = inspect.signature(check_func)
source_sig = inspect.signature(source_func)

errors: list[str] = []

for name, source_param in source_sig.parameters.items():
if name in exclude_params:
continue

custom_param = check_sig.parameters.get(name)
if not custom_param:
errors.append(f"the `{name}` param is missing")
continue

if custom_param.annotation != source_param.annotation:
errors.append(
f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(source_param.annotation)}"
)
continue

if errors:
raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors))
19 changes: 18 additions & 1 deletion src/writerai/_utils/_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import anyio
import anyio.to_thread

from ._reflection import function_has_argument

T_Retval = TypeVar("T_Retval")
T_ParamSpec = ParamSpec("T_ParamSpec")

Expand Down Expand Up @@ -59,6 +61,21 @@ def do_work(arg1, arg2, kwarg1="", kwarg2="") -> str:

async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval:
partial_f = functools.partial(function, *args, **kwargs)
return await anyio.to_thread.run_sync(partial_f, cancellable=cancellable, limiter=limiter)

# In `v4.1.0` anyio added the `abandon_on_cancel` argument and deprecated the old
# `cancellable` argument, so we need to use the new `abandon_on_cancel` to avoid
# surfacing deprecation warnings.
if function_has_argument(anyio.to_thread.run_sync, "abandon_on_cancel"):
return await anyio.to_thread.run_sync(
partial_f,
abandon_on_cancel=cancellable,
limiter=limiter,
)

return await anyio.to_thread.run_sync(
partial_f,
cancellable=cancellable,
limiter=limiter,
)

return wrapper
Loading

0 comments on commit d8f828c

Please sign in to comment.