Troubleshooting Part-DB File Upload Errors _type Field Issue In Python Script

by James Vasile 78 views

Hey guys! 👋 Ever tried automating file uploads to Part-DB using a Python script and run into a snag? I totally get it! Dealing with errors, especially those cryptic ones, can be super frustrating. Let's dive into troubleshooting a common issue you might encounter while using a Python script to upload files to Part-DB. We'll focus on a specific error related to the _type field in the upload payload, break it down, and figure out how to fix it. This guide will not only help you resolve this particular error but also give you a solid foundation for tackling similar issues in the future. Let’s make this smooth and straightforward! 🚀

Understanding the Script and Its Features

Before we get into the nitty-gritty of the error, let’s quickly recap what this Python script does. This script is designed to automate the process of uploading files to a Part-DB instance. Here's a breakdown of its cool features:

  • Recursive Directory Processing: The script can go through directories and subdirectories, making it super efficient for batch uploads. 📁
  • Automated Part Creation: It automatically creates parts in Part-DB based on the directory names. Say goodbye to manual entry! ✍️
  • File Attachment: The script attaches files within those directories to the corresponding parts. This is where the magic happens! ✨
  • Multi-File Type Support: It supports a wide range of file types, including STL, SVG, JPG, PNG, GIF, BMP, TXT, XCF, PSD, and 3MF. 🖼️
  • Custom MIME Type Handling: It handles specialized files with custom MIME types. No more MIME type headaches! 💆
  • Interactive Category and Attachment Type Selection: The script allows you to interactively select categories and attachment types. User-friendly all the way! 🤝

Here's the script we're working with:

import os
import mimetypes
import requests
import base64

API_BASE_URL = "http://parts.local:8080/api"
API_KEY = "tcp_160835b5abbb7818da705abce584a34eb11fea7e7acbdef3274e56c1f887d698"

# Register custom MIME types
mimetypes.add_type('application/sla', '.stl')
mimetypes.add_type('image/x-xcf', '.xcf')
mimetypes.add_type('image/vnd.adobe.photoshop', '.psd')
mimetypes.add_type('model/3mf', '.3mf')

# ALLOWED_EXTS list already includes images, txt, psd, xcf, stl, etc.
ALLOWED_EXTS = {'.stl', '.svg', '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.txt', '.xcf', '.psd', '.3mf'}

def get_categories():
 response = requests.get(
 f"{API_BASE_URL}/categories",
 headers={"Authorization": f"Bearer {API_KEY}"}
 )
 if response.status_code != 200:
 print("❌ Failed to fetch categories:", response.text)
 return []
 data = response.json()
 return data.get("hydra:member", data)

def select_category(categories):
 print("\nAvailable Categories:")
 for idx, cat in enumerate(categories):
 print(f"{idx+1}: {cat.get('name', cat.get('title', str(cat)))} ({cat.get('@id', cat.get('id', ''))})")
 while True:
 choice = input("Select category number: ").strip()
 if not choice.isdigit() or not (1 <= int(choice) <= len(categories)):
 print("Invalid selection. Try again.")
 else:
 return categories[int(choice)-1]['@id']

def create_part(part_name, category_iri):
 payload = {
 "name": part_name,
 "description": f"Auto-created from folder {part_name}",
 "category": category_iri
 }
 response = requests.post(
 f"{API_BASE_URL}/parts",
 json=payload,
 headers={"Authorization": f"Bearer {API_KEY}"}
 )
 if response.status_code not in (200, 201):
 print(f"❌ Failed to create part: {response.status_code} {response.text}")
 return None
 part = response.json()
 return part.get("@id") or part.get("id")

def upload_attachment(part_iri, file_path, attachment_type_iri):
 filename = os.path.basename(file_path)
 ext = os.path.splitext(filename)[1].lower()
 if ext not in ALLOWED_EXTS:
 print(f"⚠️ Skipping {filename}: Not an allowed file type")
 return
 mime_type, _ = mimetypes.guess_type(filename)
 if not mime_type:
 print(f"⚠️ Skipping {filename}: Unknown MIME type")
 return
 with open(file_path, "rb") as f:
 encoded = base64.b64encode(f.read()).decode("utf-8")
 payload = {
 "_type": "Attachment",
 "element": part_iri,
 "name": filename, 
 "filename": filename,
 "data": f"data:{mime_type};base64,{encoded}",
 "private": False,
 "attachment_type": attachment_type_iri,
 
 }
 response = requests.post(
 f"{API_BASE_URL}/attachments",
 json=payload,
 headers={
 "Authorization": f"Bearer {API_KEY}",
 "Content-Type": "application/json"
 }
 )
 if response.status_code in (200, 201):
 print(f"📎 Uploaded: {filename}")
 else:
 print(f"⚠️ Failed to upload {filename}: {response.status_code} {response.text}")

def process_directory(root_dir, category_iri, attachment_type_iri):
 for dirpath, dirnames, filenames in os.walk(root_dir):
 # Ensure we get a valid folder name. os.path.normpath removes trailing separators.
 part_name = os.path.basename(os.path.normpath(dirpath))
 if not part_name:
 print(f"⚠️ Skipping directory {dirpath}: Part name is blank")
 continue
 print(f"\n📦 Creating part for directory: {dirpath} (Part name: {part_name})")
 part_iri = create_part(part_name, category_iri)
 if not part_iri:
 continue
 for filename in filenames:
 file_path = os.path.join(dirpath, filename)
 upload_attachment(part_iri, file_path, attachment_type_iri)

def get_attachment_types():
 response = requests.get(
 f"{API_BASE_URL}/attachment_types",
 headers={"Authorization": f"Bearer {API_KEY}"}
 )
 if response.status_code != 200:
 print("❌ Failed to fetch attachment types:", response.text)
 return []
 data = response.json()
 return data.get("hydra:member", data)

def select_attachment_type(types):
 print("\nAvailable Attachment Types:")
 for idx, t in enumerate(types):
 print(f"{idx+1}: {t.get('name', t.get('title', str(t)))} ({t.get('@id', t.get('id', ''))})")
 while True:
 choice = input("Select attachment type number: ").strip()
 if not choice.isdigit() or not (1 <= int(choice) <= len(types)):
 print("Invalid selection. Try again.")
 else:
 return types[int(choice)-1]['@id']


 

def inspect_attachments():
 response = requests.get(
 f"{API_BASE_URL}/attachments",
 headers={"Authorization": f"Bearer {API_KEY}"}
 )
 if response.status_code == 200:
 data = response.json()
 if data.get("hydra:member"):
 print(f"Example attachment: {data['hydra:member'][0]}")



def find_attachments(name=None, part_id=None):
 """Find attachments by name or associated part"""
 query_params = []
 
 if name:
 query_params.append(f"name={name}")
 if part_id:
 query_params.append(f"part={part_id}")
 
 query_string = "&".join(query_params)
 url = f"{API_BASE_URL}/attachments"
 if query_string:
 url = f"{url}?{query_string}"
 
 response = requests.get(
 url,
 headers={"Authorization": f"Bearer {API_KEY}"}
 )
 
 if response.status_code == 200:
 data = response.json()
 attachments = data.get("hydra:member", [])
 if attachments:
 print(f"Found {len(attachments)} attachments:")
 for idx, attachment in enumerate(attachments):
 print(f"{idx+1}: {attachment.get('name')} ({attachment.get('@id')})")
 return attachments
 else:
 print("No attachments found matching the criteria.")
 return []
 else:
 print(f"❌ Failed to fetch attachments: {response.status_code} {response.text}")
 return []



print("###############")
#inspect_attachments()
#find_attachments(part_id="/api/part/33") ##Replace with actual part ID
#print("---------")

def main():
 root_dir = input("Enter root directory to process recursively: ").strip()
 if not os.path.isdir(root_dir):
 print("❌ Invalid directory.")
 return
 categories = get_categories()
 if not categories:
 print("❌ No categories available.")
 return
 category_iri = select_category(categories)
 
 attachment_types = get_attachment_types()
 if not attachment_types:
 print("❌ No attachment types available.")
 return
 attachment_type_iri = select_attachment_type(attachment_types)
 
 # Add this line to actually process the directory:
 process_directory(root_dir, category_iri, attachment_type_iri)
 
if __name__ == "__main__":
 main()

Decoding the Error Message

Now, let's zoom in on the error message you’re seeing:

Failed to upload 1-Nemo2-cookiecad 2in.stl: 400 {"@id":"/api/errors/400","@type":"hydra:Error","title":"An error occuurred","detail":"The type \"Attachment\" is not a valid value.","status":400,"type":"/errors/400","description":"The type \"Attachment\" is not a valid value.","hydra:description":"The type \"Attachment\" is not a valid value.","hydra:title":"An error occurred"}

This error message is a goldmine of information, guys! Here’s what we can gather:

  • Status Code 400: This is an HTTP status code indicating a “Bad Request.” It means the server couldn't process the request due to a client error (that’s us!). 😬
  • Detail: “The type "Attachment" is not a valid value.” This is the key part. The server is complaining about the _type field in our payload. It seems like the API doesn’t want or recognize the value “Attachment” for this field. 🤔
  • File Name: We also know that the error occurred while trying to upload 1-Nemo2-cookiecad 2in.stl. This is helpful for pinpointing the exact file causing the issue. 📍

In essence, the Part-DB API is telling us,