How to send a pandas dataframe using POST method and receive it in Hug/other REST API framework? pickle.loads fails to unpickle after sending

Question:

How to send a pandas DataFrame using a POST method?

For example, the following hug server listens to a POST requests and responds with a pickled pandas DataFrame:

import hug
import pickle
import traceback
import pandas as pd

@hug.post()
def call(pickle_dump):
    print(type(pickle_dump))
    try:
        df = pickle.loads(pickle_dump)
        return pickle.dumps(df.iloc[0])
    except:
        print(traceback.format_exc())
        return pickle.dumps(pd.DataFrame())

When the following POST request is made:

import requests
import pandas as pd

df = pd.DataFrame(pd.np.random.randn(10,20))
r = requests.post('http://localhost:8000/call', data = {'pickle_dump':pickle.dumps(df)})
pickle.loads(r.text)

The server returns this traceback:

<class 'str'>
Traceback (most recent call last):
  File "post.py", line 9, in call
    df = pickle.loads(pickle_dump)
TypeError: a bytes-like object is required, not 'str'

127.0.0.1 - - [23/Jul/2018 17:12:12] "POST /call HTTP/1.1" 200 10

And likewise the client returns:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-292-956952cbfca9> in <module>()
      5 r = requests.post('http://localhost:8000/call', data = {'pickle_dump':pickle.dumps(df)})
      6 
----> 7 pickle.loads(r.text)

TypeError: a bytes-like object is required, not 'str'

This seems to be related to the fact that when a byte object is sent to the hug api, the bytes are converted to a str in the following way:

For example pickle.dumps(b'test') returns b'x80x03Cx04testqx00.' on the client.
When it is received in the hug server, this becomes str('x80x03Cx04testqx00.') (missing b). The object can be decoded back to it’s original form using pickle.loads('x80x03Cx04testqx00.'.encode()[1:]).

Applying the above process on a DataFrame results in an UnpicklingError:

> pickle.dumps(pd.DataFrame())
b'x80x03cpandas.core.framenDataFramenqx00)x81qx01}qx02(Xtx00x00x00_metadataqx03]qx04Xx04x00x00x00_typqx05Xtx00x00x00dataframeqx06Xx05x00x00x00_dataqx07cpandas.core.internalsnBlockManagernqx08)x81qt(]qn(cpandas.core.indexes.basen_new_Indexnqx0bcpandas.core.indexes.basenIndexnqx0c}qr(Xx04x00x00x00nameqx0eNXx04x00x00x00dataqx0fcnumpy.core.multiarrayn_reconstructnqx10cnumpynndarraynqx11Kx00x85qx12Cx01bqx13x87qx14Rqx15(Kx01Kx00x85qx16cnumpyndtypenqx17Xx02x00x00x00O8qx18Kx00Kx01x87qx19Rqx1a(Kx03Xx01x00x00x00|qx1bNNNJxffxffxffxffJxffxffxffxffK?tqx1cbx89]qx1dtqx1ebux86qx1fRq hx0bhx0c}q!(hx0eNhx0fhx10hx11Kx00x85q"hx13x87q#Rq$(Kx01Kx00x85q%hx1ax89]q&tq'bux86q(Rq)e]q*]q+}q,Xx06x00x00x000.14.1q-}q.(Xx06x00x00x00blocksq/]q0Xx04x00x00x00axesq1hnustq2bub.'

Reversing the pickle

pickle.loads('x80x03cpandas.core.framenDataFramenqx00)x81qx01}qx02(Xtx00x00x00_metadataqx03]qx04Xx04x00x00x00_typqx05Xtx00x00x00dataframeqx06Xx05x00x00x00_dataqx07cpandas.core.internalsnBlockManagernqx08)x81qt(]qn(cpandas.core.indexes.basen_new_Indexnqx0bcpandas.core.indexes.basenIndexnqx0c}qr(Xx04x00x00x00nameqx0eNXx04x00x00x00dataqx0fcnumpy.core.multiarrayn_reconstructnqx10cnumpynndarraynqx11Kx00x85qx12Cx01bqx13x87qx14Rqx15(Kx01Kx00x85qx16cnumpyndtypenqx17Xx02x00x00x00O8qx18Kx00Kx01x87qx19Rqx1a(Kx03Xx01x00x00x00|qx1bNNNJxffxffxffxffJxffxffxffxffK?tqx1cbx89]qx1dtqx1ebux86qx1fRq hx0bhx0c}q!(hx0eNhx0fhx10hx11Kx00x85q"hx13x87q#Rq$(Kx01Kx00x85q%hx1ax89]q&tq'bux86q(Rq)e]q*]q+}q,Xx06x00x00x000.14.1q-}q.(Xx06x00x00x00blocksq/]q0Xx04x00x00x00axesq1hnustq2bub.'.encode()[1:])

Results in:

---------------------------------------------------------------------------
UnpicklingError                           Traceback (most recent call last)
<ipython-input-314-7082d60a5569> in <module>()
----> 1 pickle.loads('x80x03cpandas.core.framenDataFramenqx00)x81qx01}qx02(Xtx00x00x00_metadataqx03]qx04Xx04x00x00x00_typqx05Xtx00x00x00dataframeqx06Xx05x00x00x00_dataqx07cpandas.core.internalsnBlockManagernqx08)x81qt(]qn(cpandas.core.indexes.basen_new_Indexnqx0bcpandas.core.indexes.basenIndexnqx0c}qr(Xx04x00x00x00nameqx0eNXx04x00x00x00dataqx0fcnumpy.core.multiarrayn_reconstructnqx10cnumpynndarraynqx11Kx00x85qx12Cx01bqx13x87qx14Rqx15(Kx01Kx00x85qx16cnumpyndtypenqx17Xx02x00x00x00O8qx18Kx00Kx01x87qx19Rqx1a(Kx03Xx01x00x00x00|qx1bNNNJxffxffxffxffJxffxffxffxffK?tqx1cbx89]qx1dtqx1ebux86qx1fRq hx0bhx0c}q!(hx0eNhx0fhx10hx11Kx00x85q"hx13x87q#Rq$(Kx01Kx00x85q%hx1ax89]q&tq'bux86q(Rq)e]q*]q+}q,Xx06x00x00x000.14.1q-}q.(Xx06x00x00x00blocksq/]q0Xx04x00x00x00axesq1hnustq2bub.'.encode()[1:])

UnpicklingError: 

I am open to using any framework which will allow me to send and receive a pandas DataFrame using HTTP requests.

Both the server and the client are run in the same environment with identical package versions.

How to send and receive a pandas DataFrame using HTTP methods?

Asked By: Greg

||

Answers:

It seems like b64 encoding the pickled string seems to alleviate the issue. For brevity, I will use an example to demonstrate.

Suppose I have the following dataframe:

>>> import pandas as pd
>>> df = pd.DataFrame({'a': [0, 1, 2, 3]})
>>> df
   a
0  0
1  1
2  2
3  3

Now, let’s pickle the object to a bytes-like string, and then b64encode the pickled string:

>>> import pickle
>>> pickled = pickle.dumps(df)
>>> import base64
>>> pickled_b64 = base64.b64encode(pickled)
>>> pickled_b64
b'gANjcGFuZGFzLmNvcmUuZnJhbWUKRGF0YUZyYW1lCnEAKYFxAX1xAihYCQAAAF9tZXRhZGF0YXEDXXEEWAQAAABfdHlwcQVYCQAAAGRhdGFmcmFtZXEGWAUAAABfZGF0YXEHY3BhbmRhcy5jb3JlLmludGVybmFscwpCbG9ja01hbmFnZXIKcQgpgXEJKF1xCihjcGFuZGFzLmNvcmUuaW5kZXhlcy5iYXNlCl9uZXdfSW5kZXgKcQtjcGFuZGFzLmNvcmUuaW5kZXhlcy5iYXNlCkluZGV4CnEMfXENKFgEAAAAbmFtZXEOTlgEAAAAZGF0YXEPY251bXB5LmNvcmUubXVsdGlhcnJheQpfcmVjb25zdHJ1Y3QKcRBjbnVtcHkKbmRhcnJheQpxEUsAhXESQwFicROHcRRScRUoSwFLAYVxFmNudW1weQpkdHlwZQpxF1gCAAAATzhxGEsASwGHcRlScRooSwNYAQAAAHxxG05OTkr/////Sv////9LP3RxHGKJXXEdWAEAAABhcR5hdHEfYnWGcSBScSFoC2NwYW5kYXMuY29yZS5pbmRleGVzLnJhbmdlClJhbmdlSW5kZXgKcSJ9cSMoaA5OWAUAAABzdGFydHEkSwBYBAAAAHN0b3BxJUsEWAQAAABzdGVwcSZLAXWGcSdScShlXXEpaBBoEUsAhXEqaBOHcStScSwoSwFLAUsEhnEtaBdYAgAAAGk4cS5LAEsBh3EvUnEwKEsDWAEAAAA8cTFOTk5K/////0r/////SwB0cTJiiUMgAAAAAAAAAAABAAAAAAAAAAIAAAAAAAAAAwAAAAAAAABxM3RxNGJhXXE1aAtoDH1xNihoDk5oD2gQaBFLAIVxN2gTh3E4UnE5KEsBSwGFcTpoGoldcTtoHmF0cTxidYZxPVJxPmF9cT9YBgAAADAuMTQuMXFAfXFBKFgGAAAAYmxvY2tzcUJdcUN9cUQoWAgAAABtZ3JfbG9jc3FFY2J1aWx0aW5zCnNsaWNlCnFGSwBLAUsBh3FHUnFIWAYAAAB2YWx1ZXNxSWgsdWFYBAAAAGF4ZXNxSmgKdXN0cUtidWIu'

So the 64encoded string is also a bytes-like string, but it doesn’t contain the hex escape sequences so when it gets converted to a string, the string is still preserved when encoding it to bytes.

Now, let’s mimic what hug does to the string, as you have noted:

>>> hug_pickled_str = pickled_b64.decode('utf-8')
>>> hug_pickled_str
'gANjcGFuZGFzLmNvcmUuZnJhbWUKRGF0YUZyYW1lCnEAKYFxAX1xAihYCQAAAF9tZXRhZGF0YXEDXXEEWAQAAABfdHlwcQVYCQAAAGRhdGFmcmFtZXEGWAUAAABfZGF0YXEHY3BhbmRhcy5jb3JlLmludGVybmFscwpCbG9ja01hbmFnZXIKcQgpgXEJKF1xCihjcGFuZGFzLmNvcmUuaW5kZXhlcy5iYXNlCl9uZXdfSW5kZXgKcQtjcGFuZGFzLmNvcmUuaW5kZXhlcy5iYXNlCkluZGV4CnEMfXENKFgEAAAAbmFtZXEOTlgEAAAAZGF0YXEPY251bXB5LmNvcmUubXVsdGlhcnJheQpfcmVjb25zdHJ1Y3QKcRBjbnVtcHkKbmRhcnJheQpxEUsAhXESQwFicROHcRRScRUoSwFLAYVxFmNudW1weQpkdHlwZQpxF1gCAAAATzhxGEsASwGHcRlScRooSwNYAQAAAHxxG05OTkr/////Sv////9LP3RxHGKJXXEdWAEAAABhcR5hdHEfYnWGcSBScSFoC2NwYW5kYXMuY29yZS5pbmRleGVzLnJhbmdlClJhbmdlSW5kZXgKcSJ9cSMoaA5OWAUAAABzdGFydHEkSwBYBAAAAHN0b3BxJUsEWAQAAABzdGVwcSZLAXWGcSdScShlXXEpaBBoEUsAhXEqaBOHcStScSwoSwFLAUsEhnEtaBdYAgAAAGk4cS5LAEsBh3EvUnEwKEsDWAEAAAA8cTFOTk5K/////0r/////SwB0cTJiiUMgAAAAAAAAAAABAAAAAAAAAAIAAAAAAAAAAwAAAAAAAABxM3RxNGJhXXE1aAtoDH1xNihoDk5oD2gQaBFLAIVxN2gTh3E4UnE5KEsBSwGFcTpoGoldcTtoHmF0cTxidYZxPVJxPmF9cT9YBgAAADAuMTQuMXFAfXFBKFgGAAAAYmxvY2tzcUJdcUN9cUQoWAgAAABtZ3JfbG9jc3FFY2J1aWx0aW5zCnNsaWNlCnFGSwBLAUsBh3FHUnFIWAYAAAB2YWx1ZXNxSWgsdWFYBAAAAGF4ZXNxSmgKdXN0cUtidWIu'

Now to make the string consumable on the server-side:

>>> ss_df = pickle.loads(base64.b64decode(hug_pickled_str.encode()))
>>> ss_df
   a
0  0
1  1
2  2
3  3

Therefore, you would need to base64 encode your pickled string and pass that string as the data to your API.

Answered By: Scratch'N'Purr