I was able to solve this. Here is what I did:
- Create a new Document without primary content nor attachments.
- Rename and checkout
- Follow the 3 stages upload content procedure but:
- In stage 2 use encode_multipart_formdata function from urlib3
- In stage 3 use the 0.1 iteration, not to the checked out one
- Undo the checkout
Here is the (edited) code I used
# get product_id and folder_id first
dataDocument = {
"Name": name,
"Description": name,
# "Number": number, # rename later
"Revision": revision,
"Folder@odata.bind": f"Folders('{folder_id}')",
"Context@odata.bind": f"Containers('{product_id}')"
}
# create empty Design document 0.1
full_url = self.url + \
f"/servlet/odata/DocMgmt/Documents"
response = self.callWindchillPOST(full_url, dataDocument)
if response == None:
print("Error creating document")
return None
# rename document
doc_id = response['ID']
full_url = self.url + \
f"/servlet/odata/DocMgmt/Documents('{doc_id}')/PTC.DocMgmt.UpdateCommonProperties"
# The payload specifies the updates for common attributes.
payload = {
"Updates": {
"Name": newName,
"Number": newNumber
}
}
data = self.callWindchillPOST(full_url, payload) # Rename
# now use the 3 stage upload
# fill file_paths[] with the file paths to the attachemnts.
fill_paths[0] = "C:\\ ... \\primary_content.pdf"
fill_paths[1] = "C:\\ ... \\attachement1.pdf"
fill_paths[2] = "C:\\ ... \\attachement2.pdf"
# keep basenames, sizes, contents and mimetypes
file_basenames = []
file_sizes = []
file_contents = []
file_mimetypes = []
for path in file_paths:
if not os.path.exists(path):
print(f"Error: Local file '{path}' not found.")
return None
else:
file_basenames.append(os.path.basename(path))
file_sizes.append(os.path.getsize(path))
file_contents.append(open(path, "rb").read())
file_mimetypes.append(mimetypes.guess_type(path)[0])
# Checkout Document
documentCheckOut = self.documentCheckOut(doc_id)
if documentCheckOut == None:
print(f"Error checking out document {number}")
return None
# ------------------------------------------------------------------
# STAGE 1: Request upload URL and StreamID from Windchill
# ------------------------------------------------------------------
print("Stage 1: Initializing content upload session...")
stage1_url = self.url + f"/servlet/odata/v7/DocMgmt/Documents('{doc_id}')/PTC.DocMgmt.UploadStage1Action"
stage1_payload = {
"NoOfFiles": len(file_paths) # total number of files
}
stage1_resp = self.callWindchillPOST(stage1_url, stage1_payload)
if not stage1_resp or 'value' not in stage1_resp or len(stage1_resp['value']) == 0:
print("Failed to initialize Stage 1 upload.")
self.documentUndoCheckOut(documentCheckOut['ID'])
return None
print("Stage 1 initialized successfully.")
# ------------------------------------------------------------------
# STAGE 2: Upload primary file bytes to the target URL
# ------------------------------------------------------------------
print("Stage 2: Uploading primary file content...")
stage2_url = stage1_resp['value'][0]['ReplicaUrl']
master_url = stage1_resp['value'][0]['MasterUrl']
stream_ids = stage1_resp['value'][0]['StreamIds']
filename_ids = stage1_resp['value'][0]['FileNames']
# create multipart request
boundary = "99--77665544332211" # any separator will do
stage2_headers = {
'CSRF_NONCE': self.headers['CSRF_NONCE'], # required!
'Content-Type': f"multipart/form-data; boundary={boundary}" # boundary
}
# fields must be filled in this order precisely !!
# 1. Master_URL
fields = {
"Master_URL": master_url
}
# 2. CacheDescriptor_array
cacheDescriptor_array = []
for i in range(len(file_basenames)):
cacheDescriptor_array.append(f"{stream_ids[i]}:{filename_ids[i]}:{stream_ids[i]}:{file_sizes[i]}") # file_sizes is optional
fields["CacheDescriptor_array"] = ";".join(cacheDescriptor_array)
# 3. Files
for i in range(len(file_basenames)):
fields[filename_ids[i]] = (f"{file_basenames[i]}", file_contents[i], file_mimetypes[i])
body, header = encode_multipart_formdata(fields, boundary) # header not used
stage2_resp = self.callWindchillPOSTraw(stage2_url, body, stage2_headers)
if stage2_resp == None:
print(f"Stage 2 upload failed.")
self.documentUndoCheckOut(documentCheckOut['ID'])
return None
print("Stage 2 succeded.")
# ------------------------------------------------------------------
# STAGE 3: Create Document with Primary Content and attachments
# ------------------------------------------------------------------
print("Stage 3: Creating Document with contents...")
create_doc_url = self.url + "/servlet/odata/DocMgmt/Documents"
contentInfos = stage2_resp["contentInfos"]
data3 = {
"ContentInfo": [
# {
# "StreamId" : stream_id,
# "EncodedInfo" : encodedInfo,
# "FileName" : basefilename,
# "PrimaryContent" : True,
# "MimeType" : "application/pdf",
# "FileSize" : fileSize
# }
]
}
primaryContent = True # first one is primary
for i in range(len(contentInfos)):
data3["ContentInfo"].append({
"StreamId" : contentInfos[i]["streamId"],
"EncodedInfo" : contentInfos[i]["encodedInfo"],
"FileName" : file_basenames[i],
"PrimaryContent" : primaryContent,
"MimeType" : file_mimetypes[i],
"FileSize" : contentInfos[i]["fileSize"]
})
primaryContent = False # the others are attachemnts
# Now modify the document doc_id (not the checked out one)!!!!
stage3_url = self.url + f"/servlet/odata/v7/DocMgmt/Documents('{doc_id}')/PTC.DocMgmt.UploadStage3Action"
response3 = self.callWindchillPOST(stage3_url, data3)
# undo the checkout to remove the working copy
full_url = self.url + \
f"/servlet/odata/DocMgmt/Documents('{id}')/PTC.DocMgmt.UndoCheckOut"
responseUndoCheckout = self.callWindchillPATCH(full_url, data)
if response3 == None:
print(f"Stage 3 upload failed.")
return None
print(f"Stage 3 upload succeded.")