-
Notifications
You must be signed in to change notification settings - Fork 7
Create resource_extra_fields_export.py #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Engage defensive protocols: the API call lacks error handling. The 🛡️ 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 |
||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We must anticipate malformed data, Number One. The 🛡️ 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 |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure type safety before joining list values. If 🛡️ 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 |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bare 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
Suggested change
🧰 Tools🪛 Ruff (0.15.2)[error] 216-216: Do not use bare (E722) [error] 216-217: (S110) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| # Save Excel file | ||||||||||||||||||||||||||||||||||||||
| wb.save(OUTPUT_XLSX) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| print("XLSX export finished successfully.") | ||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.