如何使用请求 POST 方法上传 PDF 文件并且请求必须使用 python 格式化为多部分 MIME for xeroAPI?

How to upload PDF file using request POST method and requests must be formatted as multipart MIME using python for xeroAPI?

我正在尝试使用 python 请求库(POST 方法)在 Xero 帐户中上传 PDF 文件,Xeros FilesAPI 说“请求必须格式化为多部分 MIME”并且有一些必填字段(link),但我不知道该怎么做...如果我执行 GET 请求,我将获得 Xero 帐户中的文件列表,但在发布文件时遇到问题( POST-请求)...

我的代码:

post_url = 'https://api.xero.com/files.xro/1.0/Files/'
files = {'file': open('/home/mobin/PycharmProjects/s3toxero/data/in_test_upload.pdf', 'rb')}

response = requests.post(
    post_url,
    headers={
        'Authorization': 'Bearer ' + new_tokens[0],
        'Xero-tenant-id': xero_tenant_id,
        'Accept': 'application/json',
        'Content-type': 'multipart/form-data; boundary=JLQPFBPUP0',
        'Content-Length': '1068',
    },
    files=files,
)

json_response = response.json()

print(f'Uploading Responsoe ==> {json_response}')
print(f'Uploading Responsoe ==> {response}')

错误Mesage/Response:

Uploading Responsoe ==> [{'type': 'Validation', 'title': 'Validation failure', 'detail': 'No file is was attached'}]
Uploading Responsoe ==> <Response [400]>

据我所知,您设置的边界不正确。您在 header 中设置它但不告诉 requests 库使用自定义边界。让我举个例子:

>>> import requests
>>> post_url = 'https://api.xero.com/files.xro/1.0/Files/'
>>> files = {'file': open('/tmp/test.txt', 'rb')}
>>> headers = {
...    'Authorization': 'Bearer secret',
...    'Xero-tenant-id': '42',
...    'Accept': 'application/json',
...    'Content-type': 'multipart/form-data; boundary=JLQPFBPUP0',
...    'Content-Length': '1068',
... }
>>> print(requests.Request('POST', post_url, files=files, headers=headers).prepare().body.decode('utf8'))
--f3e21ca5e554dd96430f07bb7a0d0e77
Content-Disposition: form-data; name="file"; filename="test.txt"


--f3e21ca5e554dd96430f07bb7a0d0e77--

如您所见,实际边界 (f3e21ca5e554dd96430f07bb7a0d0e77) 与 header (JLQPFBPUP0) 中传递的边界不同。

你实际上可以直接使用请求模块来控制边界,就像这样:

让我们准备一个测试文件:

$ touch /tmp/test.txt
$ echo 'Hello, World!' > /tmp/test.txt 

测试一下:

>>> import requests
>>> post_url = 'https://api.xero.com/files.xro/1.0/Files/'
>>> files = {'file': open('/tmp/test.txt', 'rb')}
>>> headers = {
...     'Authorization': 'Bearer secret',
...     'Xero-tenant-id': '42',
...     'Accept': 'application/json',
...     'Content-Length': '1068',
... }
>>> body, content_type = requests.models.RequestEncodingMixin._encode_files(files, {})
>>> headers['Content-type'] = content_type
>>> print(requests.Request('POST', post_url, data=body, headers=headers).prepare().body.decode('utf8'))
--db57d23ff5dee7dc8dbab418e4bcb6dc
Content-Disposition: form-data; name="file"; filename="test.txt"

Hello, World!

--db57d23ff5dee7dc8dbab418e4bcb6dc--

>>> headers['Content-type']
'multipart/form-data; boundary=db57d23ff5dee7dc8dbab418e4bcb6dc'

此处边界与header中的边界相同。

另一种选择是使用 requests-toolbelt;下面的示例取自 this GitHub issue 线程:

from requests_toolbelt import MultipartEncoder

fields = {
    # your multipart form fields
}

m = MultipartEncoder(fields, boundary='my_super_custom_header')
r = requests.post(url, headers={'Content-Type': m.content_type}, data=m.to_string())

但是最好完全不用手工传递bundary,把这个工作委托给requests库


更新:

使用 Xero 文件 API 和 Python 请求的最小工作示例:

from os.path import abspath
import requests

access_token = 'secret'
tenant_id = 'secret'

filename = abspath('./example.png')

post_url = 'https://api.xero.com/files.xro/1.0/Files'
files = {'filename': open(filename, 'rb')}
values = {'name': 'Xero'}

headers = {
    'Authorization': f'Bearer {access_token}',
    'Xero-tenant-id': f'{tenant_id}',
    'Accept': 'application/json',
}

response = requests.post(
    post_url,
    headers=headers,
    files=files,
    data=values
)

assert response.status_code == 201

我已经用 Xero 的文件 API 测试了这个,在与我的主 app.py 文件相同的目录中上传了一个名为“helloworld.rtf”的文件。

var1 = "Bearer "
var2 = YOUR_ACCESS_TOKEN
access_token_header = var1 + var2
body = open('helloworld.rtf', 'rb')

mp_encoder = MultipartEncoder(
fields={
     'helloworld.rtf': ('helloworld.rtf', body),
     }
 )

r = requests.post(
    'https://api.xero.com/files.xro/1.0/Files',
    data=mp_encoder,  # The MultipartEncoder is posted as data
    # The MultipartEncoder provides the content-type header with the boundary:
    headers={
        'Content-Type': mp_encoder.content_type,
        'xero-tenant-id': YOUR_XERO_TENANT_ID,
        'Authorization': access_token_header
    }
)

看起来你已经解决了。供参考和任何未来使用 Xero 支持包的开发​​人员 (https://github.com/XeroAPI/xero-python)

我们刚刚将 files_api 示例代码添加到示例应用程序中,因此如果您使用 Python SDK

,以下内容将上传文件

https://github.com/XeroAPI/xero-python-oauth2-app/pull/29/files

name = "my-image"
filename= "my-image.jpg"
mime_type = "image/jpg"
with open('my-image.jpg', 'rb') as f:
    body = f.read()

try:
    file_object = files_api.upload_file(
        xero_tenant_id, 
        name = name, 
        filename= filename, 
        mime_type = mime_type,
        body=body
    )
except AccountingBadRequestException as exception:
    json = jsonify(exception.error_data)
else:
    json = serialize_model(file_object)