deta / deta-python

deta's official sdk
https://deta.space/docs/en/build/reference/sdk/base
MIT License
153 stars 25 forks source link

Added custom JSON encoder to support serializing pathlib.Path objects. #82

Closed deepaerial closed 1 year ago

deepaerial commented 1 year ago

Rationale:

This pull requested was created due to one problem I've encountered when using Deta client.

Problem:

When trying to put dictionary object containing one field of type pathlib.Path I get the following problem.

/api/tests/test_endpoints.py::test_get_downloads Failed with Error: [undefined]failed on setup with "TypeError: Object of type PosixPath is not JSON serializable"
app_client = <starlette.testclient.TestClient object at 0x103a594f0>
uid = 'e88df59a96dd40a08bc760b4fcc2b34d'
datasource = <ytdl_api.datasource.DetaDB object at 0x103d30940>
mock_download_params = DownloadParams(url=AnyHttpUrl('https://www.youtube.com/watch?v=NcBjx_eyvxc', scheme='https', host='www.youtube.com', t...='/watch', query='v=NcBjx_eyvxc'), video_stream_id='136', audio_stream_id='251', media_format=<MediaFormat.MP4: 'mp4'>)

    @pytest.fixture()
    def mock_persisted_download(
        app_client: TestClient,
        uid: str,
        datasource: IDataSource,
        mock_download_params: DownloadParams,
    ) -> Generator[Download, None, None]:
>       response = app_client.put(
            "/api/download", cookies={"uid": uid}, json=mock_download_params.dict()
        )

api/tests/test_endpoints.py:29: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
api/.venv/lib/python3.9/site-packages/requests/sessions.py:647: in put
    return self.request("PUT", url, data=data, **kwargs)
api/.venv/lib/python3.9/site-packages/starlette/testclient.py:476: in request
    return super().request(
api/.venv/lib/python3.9/site-packages/requests/sessions.py:587: in request
    resp = self.send(prep, **send_kwargs)
api/.venv/lib/python3.9/site-packages/requests/sessions.py:701: in send
    r = adapter.send(request, **kwargs)
api/.venv/lib/python3.9/site-packages/starlette/testclient.py:270: in send
    raise exc
api/.venv/lib/python3.9/site-packages/starlette/testclient.py:267: in send
    portal.call(self.app, scope, receive, send)
api/.venv/lib/python3.9/site-packages/anyio/from_thread.py:283: in call
    return cast(T_Retval, self.start_task_soon(func, *args).result())
/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py:446: in result
    return self.__get_result()
/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/concurrent/futures/_base.py:391: in __get_result
    raise self._exception
api/.venv/lib/python3.9/site-packages/anyio/from_thread.py:219: in _call_func
    retval = await retval
api/.venv/lib/python3.9/site-packages/fastapi/applications.py:269: in __call__
    await super().__call__(scope, receive, send)
api/.venv/lib/python3.9/site-packages/starlette/applications.py:124: in __call__
    await self.middleware_stack(scope, receive, send)
api/.venv/lib/python3.9/site-packages/starlette/middleware/errors.py:184: in __call__
    raise exc
api/.venv/lib/python3.9/site-packages/starlette/middleware/errors.py:162: in __call__
    await self.app(scope, receive, _send)
api/.venv/lib/python3.9/site-packages/starlette/middleware/cors.py:84: in __call__
    await self.app(scope, receive, send)
api/.venv/lib/python3.9/site-packages/starlette/exceptions.py:93: in __call__
    raise exc
api/.venv/lib/python3.9/site-packages/starlette/exceptions.py:82: in __call__
    await self.app(scope, receive, sender)
api/.venv/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py:21: in __call__
    raise e
api/.venv/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py:18: in __call__
    await self.app(scope, receive, send)
api/.venv/lib/python3.9/site-packages/starlette/routing.py:670: in __call__
    await route.handle(scope, receive, send)
api/.venv/lib/python3.9/site-packages/starlette/routing.py:266: in handle
    await self.app(scope, receive, send)
api/.venv/lib/python3.9/site-packages/starlette/routing.py:68: in app
    await response(scope, receive, send)
api/.venv/lib/python3.9/site-packages/starlette/responses.py:165: in __call__
    await self.background()
api/.venv/lib/python3.9/site-packages/starlette/background.py:43: in __call__
    await task()
api/.venv/lib/python3.9/site-packages/starlette/background.py:28: in __call__
    await run_in_threadpool(self.func, *self.args, **self.kwargs)
api/.venv/lib/python3.9/site-packages/starlette/concurrency.py:41: in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
api/.venv/lib/python3.9/site-packages/anyio/to_thread.py:31: in run_sync
    return await get_asynclib().run_sync_in_worker_thread(
api/.venv/lib/python3.9/site-packages/anyio/_backends/_asyncio.py:937: in run_sync_in_worker_thread
    return await future
api/.venv/lib/python3.9/site-packages/anyio/_backends/_asyncio.py:867: in run
    result = context.run(func, *args)
api/ytdl_api/downloaders.py:195: in download
    asyncio.run(on_finish_callback(self.datasource, self.event_queue, download))
/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/runners.py:44: in run
    return loop.run_until_complete(main)
/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/asyncio/base_events.py:647: in run_until_complete
    return future.result()
api/ytdl_api/callbacks.py:64: in on_finish_callback
    datasource.put_download(download)
api/ytdl_api/datasource.py:162: in put_download
    self.base.put(data, key)
api/.venv/lib/python3.9/site-packages/deta/base.py:142: in put
    code, res = self._request("/items", "PUT", {"items": [data]})
api/.venv/lib/python3.9/site-packages/deta/base.py:86: in _request
    body=json.dumps(data),
/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/__init__.py:231: in dumps
    return _default_encoder.encode(obj)
/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py:199: in encode
    chunks = self.iterencode(o, _one_shot=True)
/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py:257: in iterencode
    return _iterencode(o, 0)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <json.encoder.JSONEncoder object at 0x101844dc0>
o = PosixPath('/var/folders/5k/yrwqz7p14b74jkz9f3yz_1000000gn/T/tmpel1b3k7v/dd0d0f5c71c94f6c8e596d3d46d40bc2.mp4')

    def default(self, o):
        """Implement this method in a subclass such that it returns
        a serializable object for ``o``, or calls the base implementation
        (to raise a ``TypeError``).

        For example, to support arbitrary iterators, you could
        implement default like this::

            def default(self, o):
                try:
                    iterable = iter(o)
                except TypeError:
                    pass
                else:
                    return list(iterable)
                # Let the base class default method raise the TypeError
                return JSONEncoder.default(self, o)

        """
>       raise TypeError(f'Object of type {o.__class__.__name__} '
                        f'is not JSON serializable')
E       TypeError: Object of type PosixPath is not JSON serializable

/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/json/encoder.py:179: TypeError

As the stacktrace says, we cannot put object if it contains some object which is not "serializable by default". That means that I need to somehow prepare my dictionary before putting it into the base.

Proposed solution:

My proposal is to add custom json encoder which will handle such situations automatically. Not entirely sure if it will be better to have one custom encoder built-in or allow users/developers to provide such encoder by themselves.

P.S.

I would be glad to hear some feedback on this issue and thank you for nice and simple client library for Deta.

deepaerial commented 1 year ago

Is this repo still alive?

abdelhai commented 1 year ago

@deepaerial yes, we will review it w a couple of weeks. Thanks 🙏