Closed The-Compiler closed 8 years ago
I'm not an expert in encoding/decoding, but I think the line in your second test is wrong (the following code is in Python 2):
>>> 'id,name\nimport_test.test2,tést'.encode('utf-8')
# UnicodeDecodeError
The string is not an unicode one, and you state is it encoded in UTF-8.
Either the input string is unicode (u'...'
):
>>> u'id,name\nimport_test.test2,tést'.encode('utf-8')
'id,name\nimport_test.test2,t\xc3\xa9st'
Or the input string is an UTF-8 one but in a bytes
object, you need to decode it:
>>> 'id,name\nimport_test.test2,tést'.decode('utf-8')
u'id,name\nimport_test.test2,t\xe9st'
EDIT: My bad, didn't see you were using Python 3. I need to do some real tests to check what is wrong.
Have you test the import through the Web interface, and traces the JSON-RPC requests parameters? If the Web client works, then the bug is in OdooRPC. There are some helper function in the code to decode/encode parameters following the Python version used, maybe a bug is there...
In Python 3, 'foo'
is a unicode string (like u'foo'
in python 2) and b'foo'
is a byte string (like 'foo'
in Python 2), so I think that's fine.
It seems like the web interface doesn't actually use the JSON-RPC API to upload the file, but does a multipart/form-data
POST
to /base_import/set_file
with something like this:
------WebKitFormBoundary1k67DLRI4R12FQg3
Content-Disposition: form-data; name="session_id"
d69...37b
------WebKitFormBoundary1k67DLRI4R12FQg3
Content-Disposition: form-data; name="import_id"
80
------WebKitFormBoundary1k67DLRI4R12FQg3
Content-Disposition: form-data; name="file"; filename="product.template.csv"
Content-Type: text/csv
id,default_code
GSD_Artikel_64768,PRT.146D.0000
------WebKitFormBoundary1k67DLRI4R12FQg3
Content-Disposition: form-data; name="jsonp"
import_callback_12
------WebKitFormBoundary1k67DLRI4R12FQg3--
(I haven't tried with a non-ascii char yet, but I guess it'll be very similar)
Then it uses JSON-RPC to call parse_preview
.
I'm guessing OdooRPC doesn't have anything which helps me with using that API?
It was harder than I hoped it would be, but I finally have a running prototype:
# encoding: utf-8
import io
import uuid
import odoorpc
import odoorpc.error
import email.generator
import email.mime.multipart
import email.message
def create_id(env):
payload = {
'res_model': 'product.product',
}
import_id = env.create(payload)
assert isinstance(import_id, int)
return import_id
def upload_file(odoo, import_id, data):
login_data = odoo.json(
'/web/session/authenticate',
{'db': 'beh', 'login': odoo._login, 'password': odoo._password}
)
session_id = login_data['result']['session_id']
boundary = '----odoo-import-{}'.format(uuid.uuid4())
mime_msg = email.mime.multipart.MIMEMultipart(boundary=boundary)
sess_id_msg = email.message.Message()
sess_id_msg.set_payload(str(session_id))
sess_id_msg.add_header('Content-Disposition', 'form-data',
name='session_id')
mime_msg.attach(sess_id_msg)
import_id_msg = email.message.Message()
import_id_msg.set_payload(str(import_id))
import_id_msg.add_header('Content-Disposition', 'form-data',
name='import_id')
mime_msg.attach(import_id_msg)
file_msg = email.message.Message()
file_msg.set_payload(data)
file_msg.add_header('Content-Disposition', 'form-data', name='file',
filename='test.csv')
file_msg.add_header('Content-Type', 'text/csv')
mime_msg.attach(file_msg)
outio = io.StringIO()
generator = email.generator.Generator(outio)
generator.flatten(mime_msg)
msg = outio.getvalue()
msg = '\n\n'.join(msg.split('\n\n')[1:]) # Remove headers
headers = {
'Content-Type': 'multipart/form-data; boundary="{}"'.format(boundary),
'MIME-Version': '1.0',
}
odoo.http('base_import/set_file', data=msg.encode('utf-8'),
headers=headers)
def main():
odoo = odoorpc.ODOO(...)
odoo.login(...)
env = odoo.env['base_import.import']
options = {
'quoting': '"',
'separator': ',',
'encoding': 'utf-8',
'headers': True
}
data = "id,default_code\nGSD_Artikel_64768,tést"
import_id = create_id(env)
upload_file(odoo, import_id, data)
preview = env.parse_preview(import_id, options=options)
if 'error' in preview:
raise Exception(preview)
if __name__ == '__main__':
main()
Unfortunately I currently don't have the time to integrate this feature into OdooRPC properly, sorry...
But it seems it's an odoo thing after all, as the browser uses the JSON API to get the import ID but supplies the file like this.
Thanks for all your help!
Thank you for this piece of code. Indeed, if I can't find a way to use the JSON-RPC api, maybe I could add a method to import a CSV file, and your code will help to achieve that for sure.
The way to use the API to import data is as follows (however I've yet to successfully load non-ASCII data):
model, filename = ...
fields = [...]
options = {
'quoting': '"',
'separator': ',',
'encoding': 'utf-8',
'headers': True
}
client.config['timeout'] = 1200 # for large files
bi = client.env['base_import.import']
with open(filename) as f:
id = bi.create({'res_model': model, 'file_name': filename, 'file_type': 'text/csv', 'file': f.read()})
bi.do(id, fields=fields, options=options)
@mistotebe using the API that way with non-ASCII data doesn't work, as outlined above. You'll need to post the data as multipart/form-data (like the browser does as well), see my snippet above.
Are there any plans on porting this to python-requests? That might make this much easier to implement (one would still need to obtain the csrf token for 9.0 somehow, admin user has access to enough data to recreate it, but noone else).
Not from my side - I had various funny problems at first, so I wanted to be sure I can recreate exactly the multipart/form-data
payload the browser sends via JS, byte for byte.
I agree it'd be better to use requests if that works as well, but I'm afraid I don't have the time to do so (and I have a solution which works).
Yeah, I've done an import recently where I needed that and doing what you propose above with requests is way easier:
# get sid and csrf from element with class "oe_import" on import page
sid =
csrf_token =
r = requests.post(url + 'base_import/set_file',
files={'file': (filename, open(filename, 'rb'))},
data={'csrf_token': csrf_token, 'import_id': import_id},
cookies={'session_id': sid})
In v9 the base_import/set_file endpoint seems to require a valid csrf token. As a test I have tried copying a csrf token and session_id from a browser upload web-page and have pasted this into a multipart/form-data upload POST submission as shown above. Whatever I try I get a a bad CSRF error - is there a way to avoid the CSRF token requirement, eg by supplying the normal login parameters that you would for any normal xml/json-rpc request, or to get a CSRF token issued with a programmatic call ?
CSRF Tokens are specifically designed to work with the frontend and not backend RPC calls (it requires a form to be rendered beforehand).
Basically XMLRPC is the way to go here with uploading files. Try the openrpc-client-lib for example. When sending the file, mark it as xmlprclib.Binary
(https://docs.python.org/2/library/xmlrpclib.html#xmlrpclib.Binary) -- this will handle base64 encoding/decoding transparently.
One drawback of XMLRPC here is that the parse_preview
method won't work -- it returns a dict whose values are integers which isn't supported by XMLRPC. So for previews, one has to use JSONRPC :)
Sample code:
xml_odoo = openerplib.get_connection(
hostname=ODOO_SERVER['hostname'],
port=ODOO_SERVER['port'],
database=ODOO_SERVER['database'],
login=ODOO_SERVER['login'],
password=ODOO_SERVER['password']
)
imp_xml_obj = xml_odoo.get_model('base_import.import')
imp_obj = odoo.env['base_import.import']
c = codecs.open(fpath, 'rb', 'utf-8').read().encode('utf-8')
imp_id = imp_xml_obj.create({
'res_model': res_model,
'file': xmlrpclib.Binary(c),
'file_type': 'text/csv',
'file_name': file_name,
})
I'm trying to upload an ir.attachment. Anyone succeeded in doing that ?
Currently I'm trying to post using http ('/web/binary/upload_attachment') but I'm unable to get it to work properly.
@martintamare
You should be able to do that directly with the create
method on ir.attachment
.
attachment_model = odoo.env['ir.attachment']
attachment_id = attachment_model.create({'datas': B64_ENCODED_VALUE, ...})
This was my first guess, but
File "/usr/local/lib/python3.6/json/encoder.py", line 180, in default
o.__class__.__name__)
TypeError: Object of type 'bytes' is not JSON serializable
I encoded my data using :
data = some_file_handler.read()
base64.b64encode(data)
ok fixed, using .encode('utf-8') after. I hate dealing with encoding stuff :)
Thanks for the quick reply !
As described in #20/#21, I'm having some issues importing data into a binary field for the
base_import.import
model. Unfortunately I'm out of ideas, so I'd really appreciate some help here. I'm still not sure if this is something I do wrong, an OdooRPC bug, or even an odoo server bug.Test script
I wrote this little test script which tries to import ascii data (which works) and to import non-ascii data in various ways.
All those examples are Python 3, i.e.
'tést'
is a python3str
(python2unicode
) and.encode()
gives us a python3bytes
.test 2: utf-8 data as plaintext (bytes)
This tries to send
'id,name\nimport_test.test2,tést'.encode('utf-8')
which causes:test 3: utf-8 data as plaintext (string)
This tries to simply send an unicode object (python3
str
), which fails on the server:I investigated the source where the error occurs, where there's this comment:
So I tried if base64 would work:
test 4: utf-8 as encoded base64
This tries to send
base64.b64encode('id,name\nimport_test.test4,tést'.encode('utf-8'))
which fails on the client, like with test 2:test 5: utf-8 as base64 str
This seems to work, but it looks like the server actually stores the
base64
as data in the database, which causes this response whenparse_preview
is called:So if I understand correctly,
base_import.import
doesn't expect its data as base64, but there's no way to transfer non-ASCII data via the json-RPC API? Is there something I'm missing?I also tried adjusting the script and running it with Python 2, with the same outcome.