Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions examples/resource_extra_fields_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
"""
eLabFTW Resource Export Script
--------------------------------

This script connects to an eLabFTW instance via the API and exports all items
from a specific category.

For each item, the script:
- Extracts the internal eLabFTW resource ID
- Extracts the item title
- Reads all configured extra fields (custom metadata fields)
- Exports the data in a predefined column order

The export is written to:
- A CSV file (for universal compatibility)
- An XLSX Excel file (with formatting, filters, and auto column width)

The FIELD_ORDER list defines the exact column structure of the export.
This ensures a stable and reproducible layout, which is important for:
- Regulatory documentation
- Data imports into other systems
- Standardized reporting
"""

import elabapi_python
import json
import csv
from openpyxl import Workbook
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter

#########################
# CONFIG #
#########################

# Base URL of your eLabFTW API (must include /api/v2)
API_HOST_URL = 'https://YOUR-URL/api/v2'

# Personal API key generated in eLabFTW
API_KEY = 'YOUR API Key'

# Category ID from which items should be exported
CATEGORY_ID = 123456

# Output filenames
OUTPUT_FILE = "export.csv"
OUTPUT_XLSX = "export.xlsx"

#########################
# API CONFIGURATION #
#########################

# Create configuration object for the eLabFTW API
configuration = elabapi_python.Configuration()
configuration.api_key['api_key'] = API_KEY
configuration.api_key_prefix['api_key'] = 'Authorization'
configuration.host = API_HOST_URL

configuration.debug = False

# Set to True if valid SSL certificates are used
configuration.verify_ssl = True

# Create API client
api_client = elabapi_python.ApiClient(configuration)

# Set authorization header manually (required for authentication)
api_client.set_default_header(
header_name='Authorization',
header_value=API_KEY
)

# Load Items API endpoint
itemsApi = elabapi_python.ItemsApi(api_client)

###############################
# DEFINE EXPORT ORDER HERE #
###############################

"""
FIELD_ORDER defines the exact column order of metadata fields
in the exported files.

Why is this necessary?

eLabFTW stores extra fields dynamically in JSON format.
Without explicitly defining the order:
- Column positions could change
- Fields might appear in random order
- Downstream processing (e.g., Excel templates, validation scripts)
could break

By defining FIELD_ORDER:
- The export structure remains stable
- Reports remain consistent
- Future modifications can be controlled centrally

You can modify this list to match your own category setup.
Simply replace the example field names below with the exact
field titles used in your system.
"""

FIELD_ORDER = [
"Record Number", # e.g. internal running number
"Project ID", # e.g. reference or file number
"Organism Name", # e.g. E. coli strain
"Gene / Target", # e.g. transgene or modification
"Storage Location", # e.g. freezer or storage unit
"Experiment Purpose", # short description of use
"Donor Organism",
"Recipient Organism",
"Vector",
"Resistance Marker",
"Sequence Information",
"Risk Assessment Reference",
"Created By",
"Created At",
"Comments"
]

# --------------------------------------------------
# ----------- SCRIPT STARTS HERE ------------------
# --------------------------------------------------

print("Starting export...")

# Retrieve all items from the specified category
items = itemsApi.read_items(cat=CATEGORY_ID)

print(f"Items found: {len(items)}")

Comment on lines +125 to +131
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Engage defensive protocols: the API call lacks error handling.

The read_items call on line 128 can fail due to network issues, authentication errors, or invalid category IDs. Currently, any exception would crash the script without a clear diagnostic message. Additionally, this script lacks a if __name__ == "__main__": guard, meaning it executes immediately upon import—problematic if someone attempts to reuse functions from this module.

🛡️ Proposed fix: Add main guard and API error handling
+def main():
+    print("Starting export...")
 
-print("Starting export...")
+    # Retrieve all items from the specified category
+    try:
+        items = itemsApi.read_items(cat=CATEGORY_ID)
+    except elabapi_python.ApiException as e:
+        print(f"API request failed: {e}")
+        return
 
-# Retrieve all items from the specified category
-items = itemsApi.read_items(cat=CATEGORY_ID)
+    print(f"Items found: {len(items)}")
+    # ... rest of logic ...
 
-print(f"Items found: {len(items)}")
+if __name__ == "__main__":
+    main()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 125 - 131, Wrap the
script execution in a main guard (if __name__ == "__main__":) so importing this
module won't run the export immediately, and add defensive error handling around
the itemsApi.read_items call: call itemsApi.read_items(cat=CATEGORY_ID) inside a
try/except that catches broad request/API exceptions (and Exception as a
fallback), logs/prints a clear diagnostic including the exception message and
returns/exit non‑zero on failure; ensure variables referenced after the call
(like items and len(items)) are only used when the call succeeds. Reference:
itemsApi.read_items and the top-level script body where the print("Starting
export...") and items = itemsApi.read_items(...) occur.

rows = []

# Iterate through all retrieved items
for item in items:

row = {}

# Internal eLabFTW resource ID (primary identifier)
row["Ressourcen ID"] = item.id or ""

# Item title
row["Titel"] = item.title or ""

# Metadata is stored as JSON string
metadata_raw = item.metadata

if metadata_raw:
metadata = json.loads(metadata_raw)
extra_fields = metadata.get("extra_fields", {})
else:
extra_fields = {}
Comment on lines +145 to +152
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

We must anticipate malformed data, Number One.

The json.loads call will raise a JSONDecodeError if the metadata contains invalid JSON. In space—and in data processing—we prepare for the unexpected.

🛡️ Proposed fix: Handle JSON parsing errors
     # Metadata is stored as JSON string
     metadata_raw = item.metadata

     if metadata_raw:
-        metadata = json.loads(metadata_raw)
-        extra_fields = metadata.get("extra_fields", {})
+        try:
+            metadata = json.loads(metadata_raw)
+            extra_fields = metadata.get("extra_fields", {})
+        except json.JSONDecodeError:
+            print(f"Warning: Invalid metadata JSON for item {item.id}")
+            extra_fields = {}
     else:
         extra_fields = {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 145 - 152, Wrap the
json.loads call that parses item.metadata in a try/except to catch
json.JSONDecodeError (and optionally ValueError), so malformed JSON doesn't
crash the export; on error set extra_fields = {} (and/or metadata = {}) and log
or warn about the bad payload referencing item.metadata or item.id for
debugging. Specifically update the block that assigns metadata =
json.loads(metadata_raw) / extra_fields = metadata.get("extra_fields", {}) to
handle parsing failures and fall back to an empty dict for extra_fields.


normalized_fields = {k.strip().lower(): v for k, v in extra_fields.items()}

for field in FIELD_ORDER:
key = field.strip().lower()
if key in normalized_fields:
value = normalized_fields[key].get("value", "")
else:
value = ""

# Convert list-type fields into comma-separated string
if isinstance(value, list):
value = ", ".join(value)

row[field] = value
Comment on lines +163 to +167
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ensure type safety before joining list values.

If value contains non-string elements, the join operation will fail. A minor adjustment ensures we navigate safely through any data anomalies.

🛡️ Proposed fix: Convert elements to strings
         # Convert list-type fields into comma-separated string
         if isinstance(value, list):
-            value = ", ".join(value)
+            value = ", ".join(str(v) for v in value)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 161 - 165, The code
that converts list-type fields into a comma-separated string should coerce each
element to a string before joining to avoid TypeError for non-string items;
update the block that checks isinstance(value, list) (using the variables value,
field, row) to build a list of strings (e.g., [str(v) for v in value]) and then
join that list, so row[field] gets a safe string representation.


rows.append(row)

#########################
# WRITE CSV #
#########################

# Define final column structure
csv_columns = ["Ressourcen ID", "Titel"] + FIELD_ORDER

# Write CSV file (UTF-8 encoding for special characters)
with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=csv_columns)
writer.writeheader()
writer.writerows(rows)

print("CSV export finished.")

#########################
# WRITE XLSX #
#########################

wb = Workbook()
ws = wb.active
ws.title = "Export"

# Write header row
ws.append(csv_columns)

# Make header row bold
for cell in ws[1]:
cell.font = Font(bold=True)

# Write data rows
for row in rows:
ws.append([row[col] for col in csv_columns])

# Enable auto-filter for the entire sheet
ws.auto_filter.ref = ws.dimensions

# Automatically adjust column width based on content length
for col in ws.columns:
max_length = 0
column = col[0].column
column_letter = get_column_letter(column)

for cell in col:
try:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
except:
pass

adjusted_width = max_length + 2
ws.column_dimensions[column_letter].width = adjusted_width
Comment on lines +214 to +222
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

A bare except clause is unworthy of Starfleet protocols.

This silently swallows all exceptions, making debugging nearly impossible when anomalies occur. We must be specific about what we catch, or at minimum, log the encounter.

🛡️ Proposed fix: Handle exceptions properly
     for cell in col:
         try:
             if cell.value:
                 max_length = max(max_length, len(str(cell.value)))
-        except:
-            pass
+        except (TypeError, AttributeError):
+            continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for cell in col:
try:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
except:
pass
adjusted_width = max_length + 2
ws.column_dimensions[column_letter].width = adjusted_width
for cell in col:
try:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
except (TypeError, AttributeError):
continue
adjusted_width = max_length + 2
ws.column_dimensions[column_letter].width = adjusted_width
🧰 Tools
🪛 Ruff (0.15.2)

[error] 216-216: Do not use bare except

(E722)


[error] 216-217: try-except-pass detected, consider logging the exception

(S110)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 212 - 220, The loop
over "for cell in col" silently swallows all exceptions via a bare except;
change it to catch specific exceptions (e.g., TypeError, ValueError,
AttributeError) when calling len(str(cell.value)) and handle them
explicitly—either skip the cell or record/log the error—so max_length is still
computed safely; update the block around the "for cell in col" loop and the
calculation of adjusted_width and ws.column_dimensions[column_letter].width to
use the specific exception types and emit a warning (or use the module logger)
when an unexpected cell value is encountered.


# Save Excel file
wb.save(OUTPUT_XLSX)

print("XLSX export finished successfully.")