Skip to content
Open
Changes from 1 commit
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
225 changes: 225 additions & 0 deletions examples/resource_extra_fields_export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
"""
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
)
Comment on lines +53 to +71
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

cfg="$(fd '^configuration\.py$' | head -n1)"
example_cfg="examples/client.py"

test -n "$cfg"
echo "Inspecting generated auth semantics in: $cfg"
sed -n '/def get_api_key_with_prefix/,/def /p' "$cfg"

echo
echo "Inspecting example auth setup in: $example_cfg"
rg -n -C2 'set_default_header|api_key_prefix|api_key\[' "$example_cfg"

Repository: elabftw/elabapi-python

Length of output: 48


🏁 Script executed:

# Find key files and understand repository structure
fd -t f -e py '(client\.py|configuration\.py|resource_extra)' | head -20

Repository: elabftw/elabapi-python

Length of output: 125


🏁 Script executed:

# List top-level structure
ls -la

Repository: elabftw/elabapi-python

Length of output: 763


🏁 Script executed:

# Search for elabapi_python module location and Configuration class
rg -l "class Configuration" --max-count=5

Repository: elabftw/elabapi-python

Length of output: 48


🏁 Script executed:

# Examine both example files to understand auth setup
echo "=== examples/client.py ===" 
cat -n examples/client.py

echo -e "\n=== examples/resource_extra_fields_export.py (first 80 lines) ===" 
head -80 examples/resource_extra_fields_export.py | cat -n

Repository: elabftw/elabapi-python

Length of output: 5742


🏁 Script executed:

# Check imports and find elabapi_python
rg "^import|^from" examples/ | head -20

Repository: elabftw/elabapi-python

Length of output: 1135


🏁 Script executed:

# Search for other uses of api_key_prefix in the codebase to understand the pattern
rg "api_key_prefix" --max-count=10

Repository: elabftw/elabapi-python

Length of output: 164


🏁 Script executed:

# Look at how other examples handle authentication
rg -l "Configuration\|ApiClient" examples/*.py | head -10

Repository: elabftw/elabapi-python

Length of output: 48


🏁 Script executed:

# Check if there are any other authentication patterns in examples
fd -t f -e py examples/ | xargs rg -l "set_default_header|api_key" | head -10

Repository: elabftw/elabapi-python

Length of output: 494


🏁 Script executed:

# Look for OpenAPI generated client documentation or patterns
fd -t f -e py -e md | xargs rg -l "get_api_key_with_prefix|prepend_prefix" 2>/dev/null | head -10

Repository: elabftw/elabapi-python

Length of output: 48


🏁 Script executed:

# Check all examples for their authentication patterns
echo "=== Checking all example files for auth setup ==="
for file in examples/*.py; do
  echo -e "\n--- $file ---"
  rg -n "Configuration\|set_default_header|api_key\[|api_key_prefix" "$file" | head -15
done

Repository: elabftw/elabapi-python

Length of output: 1334


🏁 Script executed:

# Let's look at the full context of both authentication setups in resource_extra_fields_export.py
echo "=== Full resource_extra_fields_export.py auth section ==="
sed -n '50,75p' examples/resource_extra_fields_export.py | cat -n

Repository: elabftw/elabapi-python

Length of output: 1008


🏁 Script executed:

# Check README or any documentation about authentication
cat README.md | head -100

Repository: elabftw/elabapi-python

Length of output: 3790


🏁 Script executed:

# Search for requirements or setup.py to understand elabapi_python version/source
fd -t f '(requirements|setup|pyproject)' | xargs cat 2>/dev/null | head -50

Repository: elabftw/elabapi-python

Length of output: 48


🏁 Script executed:

# Let's check if there's any documentation in comments about how api_key works
rg -i "api.?key|authorization" --type md | head -20

Repository: elabftw/elabapi-python

Length of output: 212


🏁 Script executed:

# Let's check all examples to see if any others use api_key/api_key_prefix pattern
rg -c "api_key\[|api_key_prefix" examples/*.py

Repository: elabftw/elabapi-python

Length of output: 108


🏁 Script executed:

# Check if examples/00-getting-started.py (first example) shows auth setup
sed -n '1,80p' examples/00-getting-started.py | cat -n

Repository: elabftw/elabapi-python

Length of output: 4970


Consolidate authentication configuration to the standard pattern.

Lines 55–56 employ configuration.api_key and configuration.api_key_prefix, which deviate from the established practice shown in the README and examples/client.py. Moreover, lines 68–71 redundantly set the same Authorization header, creating unnecessary duplication. Remove the configuration-based approach and rely solely on api_client.set_default_header(), as demonstrated in the standard examples/client.py (lines 55–58).

Suggested change
 # 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
📝 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
# 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
)
# Create configuration object for the eLabFTW API
configuration = elabapi_python.Configuration()
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
)
🤖 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 53 - 71, Remove the
nonstandard config-based auth and the duplicate header: delete the two lines
that set configuration.api_key and configuration.api_key_prefix (the uses of
configuration.api_key and configuration.api_key_prefix after creating
Configuration()), keep the Configuration() setup for host and verify_ssl,
instantiate ApiClient(configuration) as before, and rely on a single call to
api_client.set_default_header(header_name='Authorization', header_value=API_KEY)
to set auth; this consolidates authentication to the standard pattern used in
examples.


# 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 +132
Copy link
Copy Markdown

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 = {}
Copy link
Copy Markdown

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.


# Extract all fields defined in FIELD_ORDER
for field in FIELD_ORDER:
if field in extra_fields:
value = extra_fields[field].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 +185 to +189
Copy link
Copy Markdown

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 +236 to +244
Copy link
Copy Markdown

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.")