From 6ffcbcc59f7991673f0889cec386505f1c72f98b Mon Sep 17 00:00:00 2001 From: Kaustubh Maske Patil <37668193+nikochiko@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:41:15 +0530 Subject: [PATCH] fix http_client to accept files as local paths and return .output --- .fernignore | 3 + src/gooey/core/http_client.py | 94 +++++++++++++++++++++++++--- src/gooey/core/pydantic_utilities.py | 7 +++ 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/.fernignore b/.fernignore index 084a8eb..2cfb924 100644 --- a/.fernignore +++ b/.fernignore @@ -1 +1,4 @@ # Specify files that shouldn't be modified by Fern + +src/gooey/core/http_client.py +src/gooey/core/pydantic_utilities.py diff --git a/src/gooey/core/http_client.py b/src/gooey/core/http_client.py index b07401b..4ab0eb2 100644 --- a/src/gooey/core/http_client.py +++ b/src/gooey/core/http_client.py @@ -2,7 +2,8 @@ import asyncio import email.utils -import json +import json as json_module +import os import re import time import typing @@ -144,7 +145,7 @@ def get_request_body( json_body = maybe_filter_request_body(json, request_options, omit) # If you have an empty JSON body, you should just send None - return (json_body if json_body != {} else None), data_body if data_body != {} else None + return (json_body if json_body != {} else None), (data_body if data_body != {} else None) class HttpClient: @@ -190,6 +191,7 @@ def request( else self.base_timeout ) + path, json, data, files = gooey_process_request_params(path=path, data=data, files=files, json=json, omit=omit) json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) response = self.httpx_client.request( @@ -224,7 +226,7 @@ def request( json=json_body, data=data_body, content=content, - files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + files=(convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None), timeout=timeout, ) @@ -246,7 +248,21 @@ def request( omit=omit, ) - return response + # custom gooey code + location = response.headers.get("location") + if not location: + return response + + while True: + response = self.request(location, method="get", headers=headers, request_options=request_options) + if not response.is_success: + return response + else: + body = response.json() + if body.get("status") in ["starting", "running"]: + continue + else: # failed, completed, or something not in the spec + return response @contextmanager def stream( @@ -306,7 +322,7 @@ def stream( json=json_body, data=data_body, content=content, - files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + files=(convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None), timeout=timeout, ) as stream: yield stream @@ -355,6 +371,7 @@ async def request( else self.base_timeout ) + path, json, data, files = gooey_process_request_params(path=path, data=data, files=files, json=json, omit=omit) json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) # Add the input to each of these and do None-safety checks @@ -390,7 +407,7 @@ async def request( json=json_body, data=data_body, content=content, - files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + files=(convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None), timeout=timeout, ) @@ -411,7 +428,22 @@ async def request( retries=retries + 1, omit=omit, ) - return response + + # custom gooey code + location = response.headers.get("location") + if not location: + return response + + while True: + response = await self.request(location, method="get", headers=headers, request_options=request_options) + if not response.is_success: + return response + else: + body = response.json() + if body.get("status") in ["starting", "running"]: + continue + else: # failed, completed, or something not in the spec + return response @asynccontextmanager async def stream( @@ -471,7 +503,53 @@ async def stream( json=json_body, data=data_body, content=content, - files=convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None, + files=(convert_file_dict_to_httpx_tuples(remove_none_from_dict(files)) if files is not None else None), timeout=timeout, ) as stream: yield stream + + +class GooeyRequestParams(typing.NamedTuple): + """Custom Request Parameters for Gooey""" + + path: str | None + json: typing.Optional[typing.Any] + data: typing.Optional[typing.Any] + files: typing.Optional[typing.Any] + + +def gooey_process_request_params( + *, + path: str | None, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + files: typing.Optional[typing.Any], + omit: typing.Any, +) -> GooeyRequestParams: + """ + Hack to allow providing filepaths as strings in the SDK. + """ + if json or not isinstance(data, typing.MutableMapping) or not path or not path.rstrip("/").endswith("/async"): + return GooeyRequestParams(path=path, json=json, data=data, files=files) + + if files and isinstance(files, typing.MutableMapping): + for k, v in files.items(): + if v and isinstance(v, list) and all(isinstance(item, str) and os.path.exists(item) for item in v): + files[k] = [open(item, "rb") for item in v] + elif v and isinstance(v, str) and os.path.exists(v): + files[k] = open(v, "rb") + elif isinstance(v, str) or v is omit or not v: + # a URL, None, or omitted value + data[k] = files.pop(k) + + if files: + return GooeyRequestParams( + path=path.rstrip("/") + "/form", + json=None, + data={ + "json": json_module.dumps(maybe_filter_request_body(data, request_options=None, omit=omit)), + }, + files=files, + ) + else: + return GooeyRequestParams(path=path, json=data, data=None, files=None) diff --git a/src/gooey/core/pydantic_utilities.py b/src/gooey/core/pydantic_utilities.py index eb42918..9664ee5 100644 --- a/src/gooey/core/pydantic_utilities.py +++ b/src/gooey/core/pydantic_utilities.py @@ -56,6 +56,13 @@ def parse_obj_as(type_: typing.Type[T], object_: typing.Any) -> T: + if type_.__name__.endswith("Output") and isinstance(object_, typing.Mapping) and "output" in object_: + return _parse_obj_as(type_, object_["output"]) + + return _parse_obj_as(type_, object_) + + +def _parse_obj_as(type_: typing.Type[T], object_: typing.Any) -> T: if IS_PYDANTIC_V2: adapter = pydantic.TypeAdapter(type_) # type: ignore # Pydantic v2 return adapter.validate_python(object_)