diff --git a/.gitignore b/.gitignore index 0f9a66a..8779740 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.prism.log .vscode _dev diff --git a/.stats.yml b/.stats.yml index f53008d..e396c3c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -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 diff --git a/README.md b/README.md index 73110b8..03ab73f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/api.md b/api.md index 0c42b4d..9a27ad9 100644 --- a/api.md +++ b/api.md @@ -33,3 +33,43 @@ from writerai.types import ModelListResponse Methods: - client.models.list() -> ModelListResponse + +# Graphs + +Types: + +```python +from writerai.types import ( + Graph, + GraphCreateResponse, + GraphUpdateResponse, + GraphDeleteResponse, + GraphRemoveFileFromGraphResponse, +) +``` + +Methods: + +- client.graphs.create(\*\*params) -> GraphCreateResponse +- client.graphs.retrieve(graph_id) -> Graph +- client.graphs.update(graph_id, \*\*params) -> GraphUpdateResponse +- client.graphs.list(\*\*params) -> SyncCursorPage[Graph] +- client.graphs.delete(graph_id) -> GraphDeleteResponse +- client.graphs.add_file_to_graph(graph_id, \*\*params) -> File +- client.graphs.remove_file_from_graph(file_id, \*, graph_id) -> GraphRemoveFileFromGraphResponse + +# Files + +Types: + +```python +from writerai.types import File, FileDeleteResponse +``` + +Methods: + +- client.files.retrieve(file_id) -> File +- client.files.list(\*\*params) -> SyncCursorPage[File] +- client.files.delete(file_id) -> FileDeleteResponse +- client.files.download(file_id) -> BinaryAPIResponse +- client.files.upload(\*\*params) -> File diff --git a/bin/publish-pypi b/bin/publish-pypi index 826054e..05bfccb 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 9f88960..6d2d20a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/requirements-dev.lock b/requirements-dev.lock index 1b60f5e..70a53ed 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -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 @@ -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 diff --git a/requirements.lock b/requirements.lock index efb1c1b..bd53c62 100644 --- a/requirements.lock +++ b/requirements.lock @@ -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 @@ -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 diff --git a/src/writerai/_base_client.py b/src/writerai/_base_client.py index 633afbb..5665fda 100644 --- a/src/writerai/_base_client.py +++ b/src/writerai/_base_client.py @@ -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 ( @@ -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( @@ -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 @@ -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 @@ -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. @@ -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) @@ -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(), diff --git a/src/writerai/_client.py b/src/writerai/_client.py index 97dc25b..ab24307 100644 --- a/src/writerai/_client.py +++ b/src/writerai/_client.py @@ -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 @@ -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) @@ -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 @@ -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) @@ -398,6 +406,8 @@ 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: @@ -405,6 +415,8 @@ 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: @@ -412,6 +424,8 @@ 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: @@ -419,6 +433,8 @@ 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 diff --git a/src/writerai/_utils/__init__.py b/src/writerai/_utils/__init__.py index 31b5b22..3efe66c 100644 --- a/src/writerai/_utils/__init__.py +++ b/src/writerai/_utils/__init__.py @@ -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, +) diff --git a/src/writerai/_utils/_reflection.py b/src/writerai/_utils/_reflection.py new file mode 100644 index 0000000..9a53c7b --- /dev/null +++ b/src/writerai/_utils/_reflection.py @@ -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)) diff --git a/src/writerai/_utils/_sync.py b/src/writerai/_utils/_sync.py index 595924e..d0d8103 100644 --- a/src/writerai/_utils/_sync.py +++ b/src/writerai/_utils/_sync.py @@ -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") @@ -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 diff --git a/src/writerai/pagination.py b/src/writerai/pagination.py new file mode 100644 index 0000000..16ba687 --- /dev/null +++ b/src/writerai/pagination.py @@ -0,0 +1,85 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Any, List, Generic, TypeVar, Optional, cast +from typing_extensions import Protocol, override, runtime_checkable + +from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage + +__all__ = ["SyncCursorPage", "AsyncCursorPage"] + +_T = TypeVar("_T") + + +@runtime_checkable +class CursorPageItem(Protocol): + id: Optional[str] + + +class SyncCursorPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + has_more: bool + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> Optional[PageInfo]: + is_forwards = not self._options.params.get("before", False) + + data = self.data + if not data: + return None + + if is_forwards: + item = cast(Any, data[-1]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"after": item.id}) + else: + item = cast(Any, self.data[0]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"before": item.id}) + + +class AsyncCursorPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + has_more: bool + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> Optional[PageInfo]: + is_forwards = not self._options.params.get("before", False) + + data = self.data + if not data: + return None + + if is_forwards: + item = cast(Any, data[-1]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"after": item.id}) + else: + item = cast(Any, self.data[0]) + if not isinstance(item, CursorPageItem) or item.id is None: + # TODO emit warning log + return None + + return PageInfo(params={"before": item.id}) diff --git a/src/writerai/resources/__init__.py b/src/writerai/resources/__init__.py index d4fe705..4d17d60 100644 --- a/src/writerai/resources/__init__.py +++ b/src/writerai/resources/__init__.py @@ -8,6 +8,22 @@ ChatResourceWithStreamingResponse, AsyncChatResourceWithStreamingResponse, ) +from .files import ( + FilesResource, + AsyncFilesResource, + FilesResourceWithRawResponse, + AsyncFilesResourceWithRawResponse, + FilesResourceWithStreamingResponse, + AsyncFilesResourceWithStreamingResponse, +) +from .graphs import ( + GraphsResource, + AsyncGraphsResource, + GraphsResourceWithRawResponse, + AsyncGraphsResourceWithRawResponse, + GraphsResourceWithStreamingResponse, + AsyncGraphsResourceWithStreamingResponse, +) from .models import ( ModelsResource, AsyncModelsResource, @@ -44,4 +60,16 @@ "AsyncModelsResourceWithRawResponse", "ModelsResourceWithStreamingResponse", "AsyncModelsResourceWithStreamingResponse", + "GraphsResource", + "AsyncGraphsResource", + "GraphsResourceWithRawResponse", + "AsyncGraphsResourceWithRawResponse", + "GraphsResourceWithStreamingResponse", + "AsyncGraphsResourceWithStreamingResponse", + "FilesResource", + "AsyncFilesResource", + "FilesResourceWithRawResponse", + "AsyncFilesResourceWithRawResponse", + "FilesResourceWithStreamingResponse", + "AsyncFilesResourceWithStreamingResponse", ] diff --git a/src/writerai/resources/files.py b/src/writerai/resources/files.py new file mode 100644 index 0000000..64fc842 --- /dev/null +++ b/src/writerai/resources/files.py @@ -0,0 +1,527 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import file_list_params, file_upload_params +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven, FileTypes +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + to_custom_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_custom_streamed_response_wrapper, + async_to_custom_raw_response_wrapper, + async_to_custom_streamed_response_wrapper, +) +from ..pagination import SyncCursorPage, AsyncCursorPage +from ..types.file import File +from .._base_client import ( + AsyncPaginator, + make_request_options, +) +from ..types.file_delete_response import FileDeleteResponse + +__all__ = ["FilesResource", "AsyncFilesResource"] + + +class FilesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> FilesResourceWithRawResponse: + return FilesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FilesResourceWithStreamingResponse: + return FilesResourceWithStreamingResponse(self) + + def retrieve( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Get metadata of a file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._get( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + graph_id: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[File]: + """ + Get metadata of all files + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/files", + page=SyncCursorPage[File], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "graph_id": graph_id, + "limit": limit, + "order": order, + }, + file_list_params.FileListParams, + ), + ), + model=File, + ) + + def delete( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleteResponse: + """ + Delete file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._delete( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleteResponse, + ) + + def download( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BinaryAPIResponse: + """ + Download a file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return self._get( + f"/v1/files/{file_id}/download", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BinaryAPIResponse, + ) + + def upload( + self, + *, + content: FileTypes, + content_disposition: str, + content_length: int, + content_type: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Upload file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + "Content-Disposition": content_disposition, + "Content-Length": str(content_length), + "Content-Type": content_type, + **(extra_headers or {}), + } + return self._post( + "/v1/files", + body=maybe_transform(content, file_upload_params.FileUploadParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + +class AsyncFilesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncFilesResourceWithRawResponse: + return AsyncFilesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFilesResourceWithStreamingResponse: + return AsyncFilesResourceWithStreamingResponse(self) + + async def retrieve( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Get metadata of a file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._get( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + graph_id: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[File, AsyncCursorPage[File]]: + """ + Get metadata of all files + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/files", + page=AsyncCursorPage[File], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "graph_id": graph_id, + "limit": limit, + "order": order, + }, + file_list_params.FileListParams, + ), + ), + model=File, + ) + + async def delete( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FileDeleteResponse: + """ + Delete file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._delete( + f"/v1/files/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=FileDeleteResponse, + ) + + async def download( + self, + file_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncBinaryAPIResponse: + """ + Download a file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + extra_headers = {"Accept": "application/octet-stream", **(extra_headers or {})} + return await self._get( + f"/v1/files/{file_id}/download", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AsyncBinaryAPIResponse, + ) + + async def upload( + self, + *, + content: FileTypes, + content_disposition: str, + content_length: int, + content_type: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Upload file + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = { + "Content-Disposition": content_disposition, + "Content-Length": str(content_length), + "Content-Type": content_type, + **(extra_headers or {}), + } + return await self._post( + "/v1/files", + body=await async_maybe_transform(content, file_upload_params.FileUploadParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + +class FilesResourceWithRawResponse: + def __init__(self, files: FilesResource) -> None: + self._files = files + + self.retrieve = to_raw_response_wrapper( + files.retrieve, + ) + self.list = to_raw_response_wrapper( + files.list, + ) + self.delete = to_raw_response_wrapper( + files.delete, + ) + self.download = to_custom_raw_response_wrapper( + files.download, + BinaryAPIResponse, + ) + self.upload = to_raw_response_wrapper( + files.upload, + ) + + +class AsyncFilesResourceWithRawResponse: + def __init__(self, files: AsyncFilesResource) -> None: + self._files = files + + self.retrieve = async_to_raw_response_wrapper( + files.retrieve, + ) + self.list = async_to_raw_response_wrapper( + files.list, + ) + self.delete = async_to_raw_response_wrapper( + files.delete, + ) + self.download = async_to_custom_raw_response_wrapper( + files.download, + AsyncBinaryAPIResponse, + ) + self.upload = async_to_raw_response_wrapper( + files.upload, + ) + + +class FilesResourceWithStreamingResponse: + def __init__(self, files: FilesResource) -> None: + self._files = files + + self.retrieve = to_streamed_response_wrapper( + files.retrieve, + ) + self.list = to_streamed_response_wrapper( + files.list, + ) + self.delete = to_streamed_response_wrapper( + files.delete, + ) + self.download = to_custom_streamed_response_wrapper( + files.download, + StreamedBinaryAPIResponse, + ) + self.upload = to_streamed_response_wrapper( + files.upload, + ) + + +class AsyncFilesResourceWithStreamingResponse: + def __init__(self, files: AsyncFilesResource) -> None: + self._files = files + + self.retrieve = async_to_streamed_response_wrapper( + files.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + files.list, + ) + self.delete = async_to_streamed_response_wrapper( + files.delete, + ) + self.download = async_to_custom_streamed_response_wrapper( + files.download, + AsyncStreamedBinaryAPIResponse, + ) + self.upload = async_to_streamed_response_wrapper( + files.upload, + ) diff --git a/src/writerai/resources/graphs.py b/src/writerai/resources/graphs.py new file mode 100644 index 0000000..73c7add --- /dev/null +++ b/src/writerai/resources/graphs.py @@ -0,0 +1,672 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import ( + graph_list_params, + graph_create_params, + graph_update_params, + graph_add_file_to_graph_params, +) +from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven +from .._utils import ( + maybe_transform, + async_maybe_transform, +) +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncCursorPage, AsyncCursorPage +from ..types.file import File +from ..types.graph import Graph +from .._base_client import ( + AsyncPaginator, + make_request_options, +) +from ..types.graph_create_response import GraphCreateResponse +from ..types.graph_delete_response import GraphDeleteResponse +from ..types.graph_update_response import GraphUpdateResponse +from ..types.graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse + +__all__ = ["GraphsResource", "AsyncGraphsResource"] + + +class GraphsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> GraphsResourceWithRawResponse: + return GraphsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> GraphsResourceWithStreamingResponse: + return GraphsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + description: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphCreateResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/graphs", + body=maybe_transform( + { + "name": name, + "description": description, + }, + graph_create_params.GraphCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphCreateResponse, + ) + + def retrieve( + self, + graph_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Graph: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return self._get( + f"/v1/graphs/{graph_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Graph, + ) + + def update( + self, + graph_id: str, + *, + name: str, + description: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphUpdateResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return self._put( + f"/v1/graphs/{graph_id}", + body=maybe_transform( + { + "name": name, + "description": description, + }, + graph_update_params.GraphUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphUpdateResponse, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncCursorPage[Graph]: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/graphs", + page=SyncCursorPage[Graph], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + graph_list_params.GraphListParams, + ), + ), + model=Graph, + ) + + def delete( + self, + graph_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphDeleteResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return self._delete( + f"/v1/graphs/{graph_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphDeleteResponse, + ) + + def add_file_to_graph( + self, + graph_id: str, + *, + file_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return self._post( + f"/v1/graphs/{graph_id}/file", + body=maybe_transform({"file_id": file_id}, graph_add_file_to_graph_params.GraphAddFileToGraphParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + def remove_file_from_graph( + self, + file_id: str, + *, + graph_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphRemoveFileFromGraphResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return self._delete( + f"/v1/graphs/{graph_id}/file/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphRemoveFileFromGraphResponse, + ) + + +class AsyncGraphsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncGraphsResourceWithRawResponse: + return AsyncGraphsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncGraphsResourceWithStreamingResponse: + return AsyncGraphsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + description: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphCreateResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/graphs", + body=await async_maybe_transform( + { + "name": name, + "description": description, + }, + graph_create_params.GraphCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphCreateResponse, + ) + + async def retrieve( + self, + graph_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Graph: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return await self._get( + f"/v1/graphs/{graph_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=Graph, + ) + + async def update( + self, + graph_id: str, + *, + name: str, + description: str | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphUpdateResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return await self._put( + f"/v1/graphs/{graph_id}", + body=await async_maybe_transform( + { + "name": name, + "description": description, + }, + graph_update_params.GraphUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphUpdateResponse, + ) + + def list( + self, + *, + after: str | NotGiven = NOT_GIVEN, + before: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + order: Literal["asc", "desc"] | NotGiven = NOT_GIVEN, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[Graph, AsyncCursorPage[Graph]]: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/graphs", + page=AsyncCursorPage[Graph], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "after": after, + "before": before, + "limit": limit, + "order": order, + }, + graph_list_params.GraphListParams, + ), + ), + model=Graph, + ) + + async def delete( + self, + graph_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphDeleteResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return await self._delete( + f"/v1/graphs/{graph_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphDeleteResponse, + ) + + async def add_file_to_graph( + self, + graph_id: str, + *, + file_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> File: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + return await self._post( + f"/v1/graphs/{graph_id}/file", + body=await async_maybe_transform( + {"file_id": file_id}, graph_add_file_to_graph_params.GraphAddFileToGraphParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=File, + ) + + async def remove_file_from_graph( + self, + file_id: str, + *, + graph_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> GraphRemoveFileFromGraphResponse: + """ + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not graph_id: + raise ValueError(f"Expected a non-empty value for `graph_id` but received {graph_id!r}") + if not file_id: + raise ValueError(f"Expected a non-empty value for `file_id` but received {file_id!r}") + return await self._delete( + f"/v1/graphs/{graph_id}/file/{file_id}", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GraphRemoveFileFromGraphResponse, + ) + + +class GraphsResourceWithRawResponse: + def __init__(self, graphs: GraphsResource) -> None: + self._graphs = graphs + + self.create = to_raw_response_wrapper( + graphs.create, + ) + self.retrieve = to_raw_response_wrapper( + graphs.retrieve, + ) + self.update = to_raw_response_wrapper( + graphs.update, + ) + self.list = to_raw_response_wrapper( + graphs.list, + ) + self.delete = to_raw_response_wrapper( + graphs.delete, + ) + self.add_file_to_graph = to_raw_response_wrapper( + graphs.add_file_to_graph, + ) + self.remove_file_from_graph = to_raw_response_wrapper( + graphs.remove_file_from_graph, + ) + + +class AsyncGraphsResourceWithRawResponse: + def __init__(self, graphs: AsyncGraphsResource) -> None: + self._graphs = graphs + + self.create = async_to_raw_response_wrapper( + graphs.create, + ) + self.retrieve = async_to_raw_response_wrapper( + graphs.retrieve, + ) + self.update = async_to_raw_response_wrapper( + graphs.update, + ) + self.list = async_to_raw_response_wrapper( + graphs.list, + ) + self.delete = async_to_raw_response_wrapper( + graphs.delete, + ) + self.add_file_to_graph = async_to_raw_response_wrapper( + graphs.add_file_to_graph, + ) + self.remove_file_from_graph = async_to_raw_response_wrapper( + graphs.remove_file_from_graph, + ) + + +class GraphsResourceWithStreamingResponse: + def __init__(self, graphs: GraphsResource) -> None: + self._graphs = graphs + + self.create = to_streamed_response_wrapper( + graphs.create, + ) + self.retrieve = to_streamed_response_wrapper( + graphs.retrieve, + ) + self.update = to_streamed_response_wrapper( + graphs.update, + ) + self.list = to_streamed_response_wrapper( + graphs.list, + ) + self.delete = to_streamed_response_wrapper( + graphs.delete, + ) + self.add_file_to_graph = to_streamed_response_wrapper( + graphs.add_file_to_graph, + ) + self.remove_file_from_graph = to_streamed_response_wrapper( + graphs.remove_file_from_graph, + ) + + +class AsyncGraphsResourceWithStreamingResponse: + def __init__(self, graphs: AsyncGraphsResource) -> None: + self._graphs = graphs + + self.create = async_to_streamed_response_wrapper( + graphs.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + graphs.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + graphs.update, + ) + self.list = async_to_streamed_response_wrapper( + graphs.list, + ) + self.delete = async_to_streamed_response_wrapper( + graphs.delete, + ) + self.add_file_to_graph = async_to_streamed_response_wrapper( + graphs.add_file_to_graph, + ) + self.remove_file_from_graph = async_to_streamed_response_wrapper( + graphs.remove_file_from_graph, + ) diff --git a/src/writerai/types/__init__.py b/src/writerai/types/__init__.py index d04f7bf..0e54913 100644 --- a/src/writerai/types/__init__.py +++ b/src/writerai/types/__init__.py @@ -3,9 +3,22 @@ from __future__ import annotations from .chat import Chat as Chat +from .file import File as File +from .graph import Graph as Graph from .completion import Completion as Completion from .streaming_data import StreamingData as StreamingData from .chat_chat_params import ChatChatParams as ChatChatParams +from .file_list_params import FileListParams as FileListParams +from .graph_list_params import GraphListParams as GraphListParams +from .file_upload_params import FileUploadParams as FileUploadParams from .chat_streaming_data import ChatStreamingData as ChatStreamingData +from .graph_create_params import GraphCreateParams as GraphCreateParams +from .graph_update_params import GraphUpdateParams as GraphUpdateParams from .model_list_response import ModelListResponse as ModelListResponse +from .file_delete_response import FileDeleteResponse as FileDeleteResponse +from .graph_create_response import GraphCreateResponse as GraphCreateResponse +from .graph_delete_response import GraphDeleteResponse as GraphDeleteResponse +from .graph_update_response import GraphUpdateResponse as GraphUpdateResponse from .completion_create_params import CompletionCreateParams as CompletionCreateParams +from .graph_add_file_to_graph_params import GraphAddFileToGraphParams as GraphAddFileToGraphParams +from .graph_remove_file_from_graph_response import GraphRemoveFileFromGraphResponse as GraphRemoveFileFromGraphResponse diff --git a/src/writerai/types/file.py b/src/writerai/types/file.py new file mode 100644 index 0000000..9448c40 --- /dev/null +++ b/src/writerai/types/file.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["File"] + + +class File(BaseModel): + id: str + + created_at: datetime + + graph_ids: List[str] + + name: str diff --git a/src/writerai/types/file_delete_response.py b/src/writerai/types/file_delete_response.py new file mode 100644 index 0000000..450d558 --- /dev/null +++ b/src/writerai/types/file_delete_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .._models import BaseModel + +__all__ = ["FileDeleteResponse"] + + +class FileDeleteResponse(BaseModel): + id: str + + deleted: bool diff --git a/src/writerai/types/file_list_params.py b/src/writerai/types/file_list_params.py new file mode 100644 index 0000000..6086695 --- /dev/null +++ b/src/writerai/types/file_list_params.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["FileListParams"] + + +class FileListParams(TypedDict, total=False): + after: str + + before: str + + graph_id: str + + limit: int + + order: Literal["asc", "desc"] diff --git a/src/writerai/types/file_upload_params.py b/src/writerai/types/file_upload_params.py new file mode 100644 index 0000000..4a47b72 --- /dev/null +++ b/src/writerai/types/file_upload_params.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._types import FileTypes +from .._utils import PropertyInfo + +__all__ = ["FileUploadParams"] + + +class FileUploadParams(TypedDict, total=False): + content: Required[FileTypes] + + content_disposition: Required[Annotated[str, PropertyInfo(alias="Content-Disposition")]] + + content_length: Required[Annotated[int, PropertyInfo(alias="Content-Length")]] + + content_type: Required[Annotated[str, PropertyInfo(alias="Content-Type")]] diff --git a/src/writerai/types/graph.py b/src/writerai/types/graph.py new file mode 100644 index 0000000..2045a5d --- /dev/null +++ b/src/writerai/types/graph.py @@ -0,0 +1,30 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["Graph", "FileStatus"] + + +class FileStatus(BaseModel): + completed: int + + failed: int + + in_progress: int + + total: int + + +class Graph(BaseModel): + id: str + + created_at: datetime + + file_status: FileStatus + + name: str + + description: Optional[str] = None diff --git a/src/writerai/types/graph_add_file_to_graph_params.py b/src/writerai/types/graph_add_file_to_graph_params.py new file mode 100644 index 0000000..b1bab49 --- /dev/null +++ b/src/writerai/types/graph_add_file_to_graph_params.py @@ -0,0 +1,11 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["GraphAddFileToGraphParams"] + + +class GraphAddFileToGraphParams(TypedDict, total=False): + file_id: Required[str] diff --git a/src/writerai/types/graph_create_params.py b/src/writerai/types/graph_create_params.py new file mode 100644 index 0000000..24638cf --- /dev/null +++ b/src/writerai/types/graph_create_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["GraphCreateParams"] + + +class GraphCreateParams(TypedDict, total=False): + name: Required[str] + + description: str diff --git a/src/writerai/types/graph_create_response.py b/src/writerai/types/graph_create_response.py new file mode 100644 index 0000000..831d298 --- /dev/null +++ b/src/writerai/types/graph_create_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["GraphCreateResponse"] + + +class GraphCreateResponse(BaseModel): + id: str + + created_at: datetime + + name: str + + description: Optional[str] = None diff --git a/src/writerai/types/graph_delete_response.py b/src/writerai/types/graph_delete_response.py new file mode 100644 index 0000000..e689b60 --- /dev/null +++ b/src/writerai/types/graph_delete_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .._models import BaseModel + +__all__ = ["GraphDeleteResponse"] + + +class GraphDeleteResponse(BaseModel): + id: str + + deleted: bool diff --git a/src/writerai/types/graph_list_params.py b/src/writerai/types/graph_list_params.py new file mode 100644 index 0000000..37668ef --- /dev/null +++ b/src/writerai/types/graph_list_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["GraphListParams"] + + +class GraphListParams(TypedDict, total=False): + after: str + + before: str + + limit: int + + order: Literal["asc", "desc"] diff --git a/src/writerai/types/graph_remove_file_from_graph_response.py b/src/writerai/types/graph_remove_file_from_graph_response.py new file mode 100644 index 0000000..bcf6d72 --- /dev/null +++ b/src/writerai/types/graph_remove_file_from_graph_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + + + +from .._models import BaseModel + +__all__ = ["GraphRemoveFileFromGraphResponse"] + + +class GraphRemoveFileFromGraphResponse(BaseModel): + id: str + + deleted: bool diff --git a/src/writerai/types/graph_update_params.py b/src/writerai/types/graph_update_params.py new file mode 100644 index 0000000..06ef5a9 --- /dev/null +++ b/src/writerai/types/graph_update_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["GraphUpdateParams"] + + +class GraphUpdateParams(TypedDict, total=False): + name: Required[str] + + description: str diff --git a/src/writerai/types/graph_update_response.py b/src/writerai/types/graph_update_response.py new file mode 100644 index 0000000..63a3f42 --- /dev/null +++ b/src/writerai/types/graph_update_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["GraphUpdateResponse"] + + +class GraphUpdateResponse(BaseModel): + id: str + + created_at: datetime + + name: str + + description: Optional[str] = None diff --git a/src/writerai/types/model_list_response.py b/src/writerai/types/model_list_response.py index db27d54..cdec7b2 100644 --- a/src/writerai/types/model_list_response.py +++ b/src/writerai/types/model_list_response.py @@ -12,7 +12,7 @@ class Model(BaseModel): """The ID of the particular LLM that you want to use""" name: str - """The name of the particular LLM that you want to use""" + """The name of the particular LLM that you want to use.""" class ModelListResponse(BaseModel): diff --git a/tests/api_resources/test_files.py b/tests/api_resources/test_files.py new file mode 100644 index 0000000..7f19fff --- /dev/null +++ b/tests/api_resources/test_files.py @@ -0,0 +1,449 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import httpx +import pytest +from respx import MockRouter + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types import File, FileDeleteResponse +from writerai._response import ( + BinaryAPIResponse, + AsyncBinaryAPIResponse, + StreamedBinaryAPIResponse, + AsyncStreamedBinaryAPIResponse, +) +from writerai.pagination import SyncCursorPage, AsyncCursorPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestFiles: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_retrieve(self, client: Writer) -> None: + file = client.files.retrieve( + "string", + ) + assert_matches_type(File, file, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Writer) -> None: + response = client.files.with_raw_response.retrieve( + "string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Writer) -> None: + with client.files.with_streaming_response.retrieve( + "string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.files.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_list(self, client: Writer) -> None: + file = client.files.list() + assert_matches_type(SyncCursorPage[File], file, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Writer) -> None: + file = client.files.list( + after="string", + before="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + ) + assert_matches_type(SyncCursorPage[File], file, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Writer) -> None: + response = client.files.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(SyncCursorPage[File], file, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Writer) -> None: + with client.files.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(SyncCursorPage[File], file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Writer) -> None: + file = client.files.delete( + "string", + ) + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Writer) -> None: + response = client.files.with_raw_response.delete( + "string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Writer) -> None: + with client.files.with_streaming_response.delete( + "string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.files.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_method_download(self, client: Writer, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + file = client.files.download( + "string", + ) + assert file.is_closed + assert file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, BinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_raw_response_download(self, client: Writer, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + file = client.files.with_raw_response.download( + "string", + ) + + assert file.is_closed is True + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + assert file.json() == {"foo": "bar"} + assert isinstance(file, BinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_streaming_response_download(self, client: Writer, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + with client.files.with_streaming_response.download( + "string", + ) as file: + assert not file.is_closed + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + + assert file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, StreamedBinaryAPIResponse) + + assert cast(Any, file.is_closed) is True + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + def test_path_params_download(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.files.with_raw_response.download( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + def test_method_upload(self, client: Writer) -> None: + file = client.files.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) + assert_matches_type(File, file, path=["response"]) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + def test_raw_response_upload(self, client: Writer) -> None: + response = client.files.with_raw_response.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + def test_streaming_response_upload(self, client: Writer) -> None: + with client.files.with_streaming_response.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncFiles: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_retrieve(self, async_client: AsyncWriter) -> None: + file = await async_client.files.retrieve( + "string", + ) + assert_matches_type(File, file, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.retrieve( + "string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.retrieve( + "string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.files.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncWriter) -> None: + file = await async_client.files.list() + assert_matches_type(AsyncCursorPage[File], file, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: + file = await async_client.files.list( + after="string", + before="string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + ) + assert_matches_type(AsyncCursorPage[File], file, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(AsyncCursorPage[File], file, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(AsyncCursorPage[File], file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncWriter) -> None: + file = await async_client.files.delete( + "string", + ) + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.delete( + "string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.delete( + "string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(FileDeleteResponse, file, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.files.with_raw_response.delete( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_method_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + file = await async_client.files.download( + "string", + ) + assert file.is_closed + assert await file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, AsyncBinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + file = await async_client.files.with_raw_response.download( + "string", + ) + + assert file.is_closed is True + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + assert await file.json() == {"foo": "bar"} + assert isinstance(file, AsyncBinaryAPIResponse) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_streaming_response_download(self, async_client: AsyncWriter, respx_mock: MockRouter) -> None: + respx_mock.get("/v1/files/string/download").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + async with async_client.files.with_streaming_response.download( + "string", + ) as file: + assert not file.is_closed + assert file.http_request.headers.get("X-Stainless-Lang") == "python" + + assert await file.json() == {"foo": "bar"} + assert cast(Any, file.is_closed) is True + assert isinstance(file, AsyncStreamedBinaryAPIResponse) + + assert cast(Any, file.is_closed) is True + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + @pytest.mark.respx(base_url=base_url) + async def test_path_params_download(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.files.with_raw_response.download( + "", + ) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + async def test_method_upload(self, async_client: AsyncWriter) -> None: + file = await async_client.files.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) + assert_matches_type(File, file, path=["response"]) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + async def test_raw_response_upload(self, async_client: AsyncWriter) -> None: + response = await async_client.files.with_raw_response.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + @pytest.mark.skip(reason="requests with binary data not yet supported in test environment") + @parametrize + async def test_streaming_response_upload(self, async_client: AsyncWriter) -> None: + async with async_client.files.with_streaming_response.upload( + content=b"raw file contents", + content_disposition="string", + content_length=0, + content_type="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + file = await response.parse() + assert_matches_type(File, file, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_graphs.py b/tests/api_resources/test_graphs.py new file mode 100644 index 0000000..4c784dd --- /dev/null +++ b/tests/api_resources/test_graphs.py @@ -0,0 +1,612 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from writerai import Writer, AsyncWriter +from tests.utils import assert_matches_type +from writerai.types import ( + File, + Graph, + GraphCreateResponse, + GraphDeleteResponse, + GraphUpdateResponse, + GraphRemoveFileFromGraphResponse, +) +from writerai.pagination import SyncCursorPage, AsyncCursorPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestGraphs: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + def test_method_create(self, client: Writer) -> None: + graph = client.graphs.create( + name="string", + ) + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + def test_method_create_with_all_params(self, client: Writer) -> None: + graph = client.graphs.create( + name="string", + description="string", + ) + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_create(self, client: Writer) -> None: + response = client.graphs.with_raw_response.create( + name="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_create(self, client: Writer) -> None: + with client.graphs.with_streaming_response.create( + name="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_retrieve(self, client: Writer) -> None: + graph = client.graphs.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Graph, graph, path=["response"]) + + @parametrize + def test_raw_response_retrieve(self, client: Writer) -> None: + response = client.graphs.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(Graph, graph, path=["response"]) + + @parametrize + def test_streaming_response_retrieve(self, client: Writer) -> None: + with client.graphs.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(Graph, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_retrieve(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.retrieve( + "", + ) + + @parametrize + def test_method_update(self, client: Writer) -> None: + graph = client.graphs.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + def test_method_update_with_all_params(self, client: Writer) -> None: + graph = client.graphs.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + description="string", + ) + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_update(self, client: Writer) -> None: + response = client.graphs.with_raw_response.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_update(self, client: Writer) -> None: + with client.graphs.with_streaming_response.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_update(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.update( + "", + name="string", + ) + + @parametrize + def test_method_list(self, client: Writer) -> None: + graph = client.graphs.list() + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + def test_method_list_with_all_params(self, client: Writer) -> None: + graph = client.graphs.list( + after="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + before="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + ) + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + def test_raw_response_list(self, client: Writer) -> None: + response = client.graphs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + def test_streaming_response_list(self, client: Writer) -> None: + with client.graphs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(SyncCursorPage[Graph], graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_method_delete(self, client: Writer) -> None: + graph = client.graphs.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_delete(self, client: Writer) -> None: + response = client.graphs.with_raw_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_delete(self, client: Writer) -> None: + with client.graphs.with_streaming_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_delete(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.delete( + "", + ) + + @parametrize + def test_method_add_file_to_graph(self, client: Writer) -> None: + graph = client.graphs.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) + assert_matches_type(File, graph, path=["response"]) + + @parametrize + def test_raw_response_add_file_to_graph(self, client: Writer) -> None: + response = client.graphs.with_raw_response.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(File, graph, path=["response"]) + + @parametrize + def test_streaming_response_add_file_to_graph(self, client: Writer) -> None: + with client.graphs.with_streaming_response.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(File, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_add_file_to_graph(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.add_file_to_graph( + "", + file_id="string", + ) + + @parametrize + def test_method_remove_file_from_graph(self, client: Writer) -> None: + graph = client.graphs.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + @parametrize + def test_raw_response_remove_file_from_graph(self, client: Writer) -> None: + response = client.graphs.with_raw_response.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = response.parse() + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + @parametrize + def test_streaming_response_remove_file_from_graph(self, client: Writer) -> None: + with client.graphs.with_streaming_response.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = response.parse() + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + def test_path_params_remove_file_from_graph(self, client: Writer) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + client.graphs.with_raw_response.remove_file_from_graph( + "string", + graph_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + client.graphs.with_raw_response.remove_file_from_graph( + "", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + +class TestAsyncGraphs: + parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"]) + + @parametrize + async def test_method_create(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.create( + name="string", + ) + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.create( + name="string", + description="string", + ) + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_create(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.create( + name="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_create(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.create( + name="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(GraphCreateResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_retrieve(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(Graph, graph, path=["response"]) + + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(Graph, graph, path=["response"]) + + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.retrieve( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(Graph, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.retrieve( + "", + ) + + @parametrize + async def test_method_update(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + description="string", + ) + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_update(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_update(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.update( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + name="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(GraphUpdateResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_update(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.update( + "", + name="string", + ) + + @parametrize + async def test_method_list(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.list() + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.list( + after="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + before="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + limit=0, + order="asc", + ) + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + async def test_raw_response_list(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + + @parametrize + async def test_streaming_response_list(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(AsyncCursorPage[Graph], graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_method_delete(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_delete(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_delete(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.delete( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(GraphDeleteResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_delete(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.delete( + "", + ) + + @parametrize + async def test_method_add_file_to_graph(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) + assert_matches_type(File, graph, path=["response"]) + + @parametrize + async def test_raw_response_add_file_to_graph(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(File, graph, path=["response"]) + + @parametrize + async def test_streaming_response_add_file_to_graph(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.add_file_to_graph( + "182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + file_id="string", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(File, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_add_file_to_graph(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.add_file_to_graph( + "", + file_id="string", + ) + + @parametrize + async def test_method_remove_file_from_graph(self, async_client: AsyncWriter) -> None: + graph = await async_client.graphs.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + @parametrize + async def test_raw_response_remove_file_from_graph(self, async_client: AsyncWriter) -> None: + response = await async_client.graphs.with_raw_response.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + graph = await response.parse() + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + @parametrize + async def test_streaming_response_remove_file_from_graph(self, async_client: AsyncWriter) -> None: + async with async_client.graphs.with_streaming_response.remove_file_from_graph( + "string", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + graph = await response.parse() + assert_matches_type(GraphRemoveFileFromGraphResponse, graph, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @parametrize + async def test_path_params_remove_file_from_graph(self, async_client: AsyncWriter) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `graph_id` but received ''"): + await async_client.graphs.with_raw_response.remove_file_from_graph( + "string", + graph_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `file_id` but received ''"): + await async_client.graphs.with_raw_response.remove_file_from_graph( + "", + graph_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + )