Skip to content

Multi-file batch analysis (+ Parquet, upload limit, Globus file/directory)#142

Draft
jeanbez wants to merge 51 commits into
developfrom
multi-file-analysis
Draft

Multi-file batch analysis (+ Parquet, upload limit, Globus file/directory)#142
jeanbez wants to merge 51 commits into
developfrom
multi-file-analysis

Conversation

@jeanbez

@jeanbez jeanbez commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds multi-file batch analysis to AIDRIN, plus supporting features and fixes. Branched from develop and merged up to date with the latest develop (29 commits, incl. the Infrastructure rebrand, CLI integration, and the upload-limit/413 work — reconciled with this branch's versions).

Highlights

  • Multi-file upload & analysis — upload up to AIDRIN_MAX_UPLOAD_FILES (default 50) files at once; each analyzed independently with an active-file switcher (top-bar selector) and a Batch Overview landing view (records/features/numerical/categorical/size per file).
  • Parquet support + a Parquet sample dataset.
  • Configurable upload size limitAIDRIN_MAX_UPLOAD_MB (default 1024 = 1 GB), enforced via MAX_CONTENT_LENGTH with a clean 413 page + client-side size check.
  • Globus: file or directory — the Globus panel now accepts a remote file or a folder; a listing function runs on the endpoint and every supported file is added to the batch. Type is inferred from the extension (no manual dropdown). Remote batch stats computed inline (version-independent).
  • Robustness — disk-backed per-user file list (survives --debug reloads), custom-metric files moved out of the watched package (no reloader thrash), retries on transient Globus 409/SystemExit 73, source-hash function registration, clear error surfacing, purge of cached Globus results on clear/disconnect/remove.
  • Privacy fixes — empty quasi-identifiers now return "Please select at least one quasi-identifier." instead of a raw pandas No group keys passed!, on every path; frontend guards for instant feedback.

Not included here

  • Porting differential-privacy / HIPAA / attribute-risk metrics to Globus lives on a follow-up branch (globus-remaining-metrics).

Test Plan

  • pytest410 passed, 8 skipped on the merged tree
  • Prettier clean (web/static/js, web/static/css)
  • create_app() boots with merged config (1 GB limit, 50-file cap, sample-data folder)
  • Manual: multi-file upload → Batch Overview → switch/remove files
  • Manual: Globus file + directory add (needs a healthy single-user endpoint)

Draft — opening for review of the multi-file + Globus-directory work while the remaining-metrics branch is finalized.

jeanbez added 30 commits June 4, 2026 11:40
Spec for analyzing multiple independent files: a session-free core batch
primitive (summarize_files) reused by web/CLI/library, plus a web active-file
switcher (compatibility shim over the existing single-file session keys) and a
combined cross-file summary. Local upload and Globus feed one source-agnostic
file list; per-file type inferred from extension; max 50 files.

Globus is remote compute, so the file list/UI is unified but metric execution
stays dispatched by source (local routes vs remote_metric_runner); the combined
summary shows Globus files with metadata only in phase 1.
Fix completeness to match the existing row-wise metric; correct the
MAX_CONTENT_LENGTH/load_dataframe branch attribution; enumerate the Globus
shim contract (repopulate globus_file_*); add a source==globus bypass for the
local os.path.exists stale-session check; store the file list server-side
(cookie-size risk); specify active-file removal, read_file tri-state handling,
full cache-key migration, type-key-as-suffix caveat, 413/max-files ordering,
and the AJAX upload rewrite.
Combined summary is the default landing view after a multi-file upload:
totals strip + per-file table (File/Type/Source/Records/Features/Size/Status),
table+totals only, no charts. No metrics in phase 1 — summarize_files does a
single light read for records/features/status (local), Globus rows metadata-only.
Drops completeness from the summary (and the row-wise/cell-wise concern with it).
Table sorted by file name (case-insensitive). Globus rows now fetch
records/features asynchronously via the existing remote _summary_statistics
(reusing the globus_summary cache) instead of showing n/a; total records
updates as remote counts stream in.
13 TDD tasks across 6 milestones: core batch layer (summarize_files,
infer_file_type), server-side file list + active-file shim, cache re-key,
file-management endpoints, multi-file upload, limits, Globus unification, and
the frontend switcher + batch-overview landing view.
Previously a file whose type couldn't be inferred (e.g. an extension with no
registered reader) was saved with an empty type, then silently wiped by the
inspector stale-session check (not file_type) — the user saw nothing happen.
Now the upload route rejects unsupported files up front with a 400 + message
listing the supported formats, so no empty-type file ever enters the list.
Scales to many files (searchable+scrollable, virtualize later); keeps the
sidebar focused on metric nav; reuses GET /files + activate.
…totals cards

- Add a 'Batch Overview' item to the sidebar nav (shown when >1 file) so the
  page is reachable after navigating into a metric panel.
- Uppercase the Batch Overview page title.
- Restyle the totals cards to match the Data Overview stat-card style
  (p-4, border, rounded-lg, text-3xl) for visual consistency.
The file list lived in in-memory TEMP_RESULTS_CACHE, which is wiped on any app
restart. Running a custom metric writes a customDR_*.py into the aidrin package,
which triggers the Flask --debug auto-reloader → restart → the list was lost,
so switching files afterward returned 'Unknown file'. Persist the list to a
per-user JSON on disk (keyed by the cookie-stable user_id, stored outside
UPLOAD_FOLDER) so it survives restarts and is shared across worker processes.
The custom-metric .py is written inside the watched aidrin package with
open(path, 'w'), which truncates to 0 bytes; the --debug reloader can restart
mid-write (or interrupt the load request), leaving an empty file and a blank
editor. Write atomically (temp file + os.replace) and fall back to the starter
template when the file is missing or empty, so the editor always shows content.
Generated customDR_*.py and remedy_data were written into aidrin/custom_metrics
(inside the package the --debug reloader watches), so every save/run/cleanup
restarted the dev server — the 'reloads after a while' flicker, and the cause
of the earlier lost-file-list and empty-editor bugs. Move CUSTOM_METRICS_FOLDER
to data/custom_metrics (project root). base_dr.py/__init__.py stay in the
package, so generated metrics still import aidrin.custom_metrics.base_dr; the
reloader no longer fires on custom-metric activity.
Changing the file while on the custom-metrics panel reloads with hash
#custom-metrics; initWorkspace restores that panel (creating the editor) and
batch-mode then shows the overview, hiding it. The /load-custom-metric fetch's
setValue() then runs on a hidden CodeMirror, which renders blank. Re-showing
the panel skipped init (editor already existed) and never refreshed. Now
showPanel refreshes the existing editor so the code renders.
jeanbez added 20 commits June 4, 2026 23:26
…readable

read_file returns None/an error string when the active file is missing (e.g.
cleaned up); the metric routes called DataFrame methods on it and crashed
(HIPAA: 'str' object has no attribute 'columns'). Guard all read_file sites in
metrics.py: on POST return a 400 with a re-upload message, on GET redirect to
the inspector (which clears the stale session).
…y on fresh upload

Activating a file (top-bar selector or a Batch Overview row) now sets the hash
to #data-overview before reloading, so you land on that file's Data Overview.
A fresh multi-file upload redirects to #batch-overview. initWorkspace loads the
batch overview only when it is the restored landing panel.
…opzone text

- clear_file now removes the disk-backed file list before session.clear()
  (which drops user_id), so it no longer orphans data/filelists/<id>.json.
- conftest 'app' fixture isolates UPLOAD_FOLDER/CUSTOM_METRICS_FOLDER (and the
  derived filelists dir) to tmp_path so tests don't pollute real data/.
- Remove the no-op fetchGlobusCount stub; Globus rows render 'n/a' directly.
- Dropzone no longer advertises DCAT (no DCAT reader; uploads are rejected).
The execution path already reads via the generic read_file (CSV/JSON/NPZ/HDF5/
Excel -> DataFrame); only the sidebar gated it to CSV. Remove the gate. Fix the
remedy output to always be a real .csv (it's written with to_csv; the old name
used the reader key as an extension, which is invalid for Excel). Guard the
read so a missing file returns a clean error instead of crashing.
…ution

Re-review findings:
- A URL fragment can't carry the 'land on batch overview' intent: the AJAX
  upload follows the redirect via fetch, and Response.url strips the fragment,
  so fresh multi-uploads were landing on Data Overview. Use a one-shot session
  flag (land_on_batch) consumed on the next inspector render. Add integration
  tests for multi- vs single-upload landing.
- custom.py executed the user metric twice (importlib then runpy); drop the
  redundant runpy run (and its now-unused import).
… extension

The Globus panel previously took a single remote file path + a manual file-type
dropdown. Now it accepts a file OR a folder path: a tiny stdlib listing function
runs on the endpoint (remote_list_data_files), the server infers each file's
type from its extension (decoupled from the endpoint's aidrin version) and adds
the supported files to the batch as source:globus entries (unsupported skipped,
respecting AIDRIN_MAX_UPLOAD_FILES). Drops the file-type dropdown. The remote
metric-execution path (/globus/submit) is unchanged. Mock-tested; needs a real
endpoint for live verification.
…new function

A live endpoint rejected the separate listing function with a 422
SEMANTICALLY_INVALID / (SystemExit) 73 at submit time. Fold the directory/file
listing into the already-registered, proven remote_metric_runner as a
'__list_files__' operation so it goes through the same serialization/registration
path metrics use. Removes remote_list_data_files/register_list_function; adds a
direct test of the list operation.
The function-UUID cache reused a stale registration after remote_metric_runner
was edited (e.g. adding __list_files__), so the endpoint ran the old code and
reported 'Unknown metric: __list_files__'. Key the cache on a hash of the
function source so any change forces a fresh registration once the process has
reloaded the new code.
Globus Compute returns 409 RESOURCE_CONFLICT when the endpoint is momentarily
busy and asks the caller to retry. Wrap client.run in _run_with_retry with
backoff (1s,2s,3s,4s) on 409/RESOURCE_CONFLICT so metric and listing
submissions ride out a busy endpoint instead of erroring out.
…_list_files__

Adds a registration log line (func_uuid, src hash, supports __list_files__) so a
stale-process situation is diagnosable: if it logs supports=False, the server is
running old code and must be fully restarted.
…marize_files

Globus batch-overview stats showed as dashes because __summarize_files__ called
aidrin.summarize_files on the endpoint, which doesn't exist in the endpoint's
released aidrin. Inline the computation with read_file + pandas (present in any
aidrin install) so it's version-independent, mirroring summarize_files' output.
Verified the remote output matches the local helper exactly.
…ries

Globus per-file stats showed as dashes with no explanation. (1) The 'pending'
guard used 'records not in e', so once an entry was cached as records=None from
a failed fetch it was never retried; use 'records is None' instead. (2) Capture
the failure reason (remote {error} like 'Unknown metric', or an exception) and
return it as overview_error; the UI now shows it as a toast instead of silent
dashes.
…' worker errors

The same registered function that listed files succeeded once then failed with
422 'Failed to start ... (SystemExit) 73' minutes later — an intermittent
endpoint worker-startup failure, not an invalid request. Treat it as transient
and retry with the existing jittered backoff.
…up keys passed!')

Running k-anonymity (or l-diversity/t-closeness/entropy) on a Globus file with
no quasi-identifiers selected reached pandas' groupby([]) and surfaced the raw
'No group keys passed!'. The local route guards this but the Globus path didn't.
Guard all four privacy functions to return a clear 'Please select at least one
quasi-identifier.' message, and add a frontend guard so the Globus submit gives
instant feedback instead of a slow remote round-trip.
Differential privacy and the single/multiple attribute risk scores have no
remote_metric_runner branch, so selecting them on a Globus file silently fell
back to k-anonymity (then 'no quasi-identifiers'). Detect those selections in
the Globus submit path and show a clear 'not available for remote files yet —
use a local file' message instead of misfiring.
# Conflicts:
#	tests/integration/test_inspector.py
#	web/__init__.py
#	web/routes/core.py
#	web/static/js/main.js
#	web/templates/_components/upload_panel.html
Comment thread web/routes/globus.py
result = status.get("result")
break
if status.get("status") == "failed":
return jsonify({"error": status.get("error", "Remote listing failed")}), 502
Comment thread web/routes/globus.py
if result is None:
return jsonify({"error": "Timed out listing remote files"}), 504
if isinstance(result, dict) and result.get("error"):
return jsonify({"error": result["error"]}), 400
Comment thread web/routes/globus.py
Comment on lines +436 to +441
return jsonify({
"added": added,
"skipped": skipped,
"active_file_id": first_id,
"overview_error": overview_error, # why per-file stats are missing
})
# Conflicts:
#	web/templates/_components/upload_panel.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants