From 85d446a370e91cfd9d6f99ad28e447cdad13ae78 Mon Sep 17 00:00:00 2001 From: Steven Roberts Date: Mon, 3 Aug 2026 11:18:09 -0700 Subject: [PATCH 1/3] Add development plan, photo policy, and maintainer ownership Plans the replacement for the spreadsheet in legacy/: git-backed CSVs as the source of truth, a static search site for reading, issue-form-driven PRs for writing, and an optional Claude vision pipeline for photo intake. Records two settled decisions: - Public repo. This puts the entire privacy burden on the photo path, so PHOTO_POLICY.md ships now rather than with Phase 4, and three guardrails become mandatory: strip all EXIF, review the image itself and not just the items derived from it, and allow rejection on privacy grounds alone. A merged photo is permanent -- it survives in history, forks, and scrapes -- so pre-merge review is the only real control. - @sr320 and @kubu4 both maintain, via CODEOWNERS. Co-Authored-By: Claude Opus 5 --- .github/CODEOWNERS | 10 ++ .github/workflows/validate.yml | 49 ++++++ .gitignore | 1 + PHOTO_POLICY.md | 32 ++++ PLAN.md | 270 +++++++++++++++++++++++++++++++++ README.md | 62 ++++++++ 6 files changed, 424 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/validate.yml create mode 100644 PHOTO_POLICY.md create mode 100644 PLAN.md create mode 100644 README.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..18561cb --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,10 @@ +# Inventory data and tooling require review from a maintainer before merge. +# Enforced by branch protection on `main` (Settings > Branches > require review +# from Code Owners). Either maintainer can approve and merge. +* @sr320 @kubu4 + +# Photos carry the most risk in a public repo -- see PHOTO_POLICY.md. Reviewers +# are checking the image itself for accidentally-captured notebook pages, +# screens, and sample manifests, not just the item list derived from it. +/photos/ @sr320 @kubu4 +/descriptions/ @sr320 @kubu4 diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..4ff9772 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,49 @@ +name: validate inventory data + +on: + pull_request: + paths: + - "data/**" + - "scripts/**" + - ".github/workflows/validate.yml" + push: + branches: [main] + paths: + - "data/**" + - "scripts/**" + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + # validate.py is stdlib-only on purpose, so there is nothing to install + # and nothing to break when a dependency moves. + - name: Validate data/*.csv + run: python3 scripts/validate.py + + - name: Check migration is reproducible + if: github.event_name == 'pull_request' + run: | + pip install -q -r requirements.txt + python3 scripts/migrate_legacy.py --out /tmp/regen > /dev/null + # The legacy spreadsheet is frozen, so re-running the migration must + # reproduce byte-identical output. A diff here means either the + # migration script changed behaviour (re-run it and commit the + # result) or data/ was hand-edited in a way that is fine -- in which + # case this check should be dropped once real edits start landing. + if ! diff -q /tmp/regen/locations.csv data/locations.csv; then + echo "::warning::locations.csv differs from a fresh migration" + fi + if ! diff -q /tmp/regen/items.csv data/items.csv; then + echo "::warning::items.csv differs from a fresh migration (expected once hand edits land)" + fi diff --git a/.gitignore b/.gitignore index e43b0f9..2836ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +.claude/settings.local.json diff --git a/PHOTO_POLICY.md b/PHOTO_POLICY.md new file mode 100644 index 0000000..2703b83 --- /dev/null +++ b/PHOTO_POLICY.md @@ -0,0 +1,32 @@ +# Photo policy + +**This repository is public.** Anything committed here is world-readable, permanently, and will be scraped and mirrored within hours. Photos are the only part of this inventory that can leak something you didn't intend, so this policy is the whole privacy control. + +## Before you take the photo + +Frame the shelf, drawer, or bin — nothing else. Check the edges of the frame for: + +- **Lab notebook pages** — open notebooks, loose protocol pages, printouts +- **Screens** — monitors, laptops, tablets, instrument displays, phones +- **Whiteboards and posted notes** — including sticky notes on cabinet doors +- **People** — hands are fine, faces and name badges are not +- **Sample manifests** — printed sample lists, freezer maps, plate maps taped to equipment +- **Anything with a person's name, a subject ID, or an unpublished result on it** + +Tube labels and reagent bottle labels are fine and are the point of the exercise. A printed sheet listing 96 sample IDs is not. + +## What happens on ingest + +The intake pipeline strips **all** EXIF metadata (not just GPS) before anything is committed. Do not rely on that for anything but metadata — it cannot see what's in the frame. + +## Review is the only real control + +A photo gets reviewed by [@sr320](https://github.com/sr320) or [@kubu4](https://github.com/kubu4) before merge. Reviewers check **the image itself**, not just the item list the model derived from it. A photo can be rejected on privacy grounds alone even if the extracted items are perfect. + +**This must happen before merge, because after merge it is too late.** Deleting a photo in a later commit does not remove it — it stays in git history, it stays in every clone and fork, and it has already been indexed. Removing it for real means rewriting history and force-pushing, which breaks everyone's clone and still doesn't recall what was scraped. + +If you realize a merged photo shouldn't be public: say so immediately in an issue and tag both maintainers. Don't quietly delete the file — that leaves it in history while making it look handled. + +## If you're unsure + +Don't commit it. Ask in the issue first, or just retake the photo with the questionable thing out of frame. Retaking a photo costs 30 seconds. diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..b8383d2 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,270 @@ +# Roberts Lab Inventory — Development Plan + +Status: **draft for review** · Author: planning pass, 2026-08-03 + +--- + +## 1. Where we are + +The legacy system is `legacy/Roberts Lab Inventory.xlsx`, five sheets: + +| Sheet | Rows | Columns | Notes | +|---|---|---|---| +| `DrawersCabinets updated 82921` | 144 | Room, Drawer/Cabinet, Number, *(unnamed contents col)* | 11 rows have a location but no contents | +| `Refrigerator Rm 209` | 145 | Shelf #, Contents, Recieved | most granular sheet; one item per row | +| `Shelf Rm 209 updated 9521` | 15 | Shelf #, Contents, Date | | +| `Shelf RM 213 updated 82921` | 50 | Shelf #, Contents, Date Rec'd, Notes | | +| `-20˚ Rm 213 updated 82921` | 16 | Shelf #, Drawer #, Contents | densest lists — up to 629 chars/cell | + +Measured problems, in priority order: + +1. **Stale.** Sheet names say last update Aug/Sep 2021. Five years of purchases, consumption, and moves are unrecorded. Any new system that doesn't make *re-inventorying* cheap will be stale again by 2028. +2. **Not item-granular.** ~370 location rows contain ~500 individual items (an early estimate of ~880 double-counted comma-separated fragments). One `-20˚` cell lists 11 kits with parenthetical dates. You cannot search, count, or expire-check a semicolon-delimited blob. +3. **No controlled vocabulary.** `Cabinet` / `Cabinet ` / `Cabinets`; rooms as `209.0`, `213.0`, `-80˚C room`. Location strings can't be joined or filtered reliably. +4. **Write-hostile.** One binary file, no concurrent edits, no history, no way to know who put the ZR-Duet kit on shelf 1 or whether it's still there. +5. **Dates unvalidated.** Received dates range 2005-06-27 → 2026-06-02; the latter is almost certainly a typo or a mis-entered expiration. + +--- + +## 2. Design principles + +These are the tie-breakers for every decision below. + +- **Read is the hot path.** The dominant query is "do we have X, and where is it?" asked from a phone while standing in the lab. Optimize search hard; accept slightly more friction on writes. +- **Adding an item must take under 30 seconds.** If it takes longer than writing on a Post-it, people will write on a Post-it. +- **Plain text, in git, is the source of truth.** Diffable, greppable, scriptable, restorable, and readable in 20 years without a vendor. Matches how this lab already works. +- **The LLM drafts; a human commits.** Vision models are excellent at "this drawer contains pipette tips, a P1000, and three Falcon tubes" and untrustworthy on catalog numbers and lot IDs. Every machine-generated row lands in a reviewable PR, never straight into the canonical file. +- **No build toolchain that can rot.** Static HTML + vendored JS + Python stdlib where possible. A tool nobody can rebuild in 2029 is a tool that's gone. + +--- + +## 3. Recommended architecture + +**Git-backed flat files + a static search site + GitHub Actions for the write and photo paths.** + +``` +lab-inventory/ +├── data/ +│ ├── locations.csv # controlled vocabulary of places +│ ├── items.csv # canonical inventory, one row per item +│ └── vendors.csv # optional lookup +├── photos/ +│ ├── inbox/ # drop zone; Action consumes and clears +│ └── 209-CAB-01_20260803.jpg +├── descriptions/ +│ └── 209-CAB-01_20260803.md # LLM description + provenance, per photo +├── site/ # GitHub Pages: search UI (generated + static) +├── scripts/ +│ ├── migrate_legacy.py # xlsx -> csv, one-time +│ ├── validate.py # CI gate on every PR +│ ├── build_site.py # csv -> inventory.json + location pages +│ ├── describe_photos.py # Claude vision -> descriptions + candidate rows +│ └── make_qr_labels.py # printable QR stickers per location +└── .github/ + ├── ISSUE_TEMPLATE/ # add-item / consumed-item / found-something forms + └── workflows/ # validate, build-site, describe-photos, expiry-scan +``` + +Why this over the alternatives: + +| Option | Verdict | +|---|---| +| **Git flat files + static site** (recommended) | Free, versioned, no auth to build (GitHub accounts already exist), LLM step is a natural Action, survives personnel turnover. Cost: lab members must tolerate a web form that opens a GitHub issue. | +| Another spreadsheet (Sheets + Apps Script) | Lowest friction to start, but reproduces every problem in §1. Sheets has no real review step, so LLM output would land unchecked. | +| Airtable / Notion | Genuinely easy, native mobile app, attachments and image fields for free. But ~$20/user/mo, data lives in a vendor, and versioning/provenance is weak. **Pick this instead if the lab flatly won't touch git** — the LLM step still works via automation + API script. | +| Real web app (SQLite + Flask on Fly/Render) | Best UX for writes, but needs hosting, auth, backups, and an owner. Not worth the maintenance load for ~1000 items and ~8 users. | + +**Escape hatch:** because the source of truth is CSV, migrating *to* Airtable or a real app later is a 30-line import script. Starting with flat files costs nothing in optionality. + +--- + +## 4. Data model + +Concrete, minimal, extensible. Human-readable composite IDs beat surrogate keys in a hand-editable file. + +### `data/locations.csv` + +```csv +location_id,room,kind,number,parent_id,label,notes +209-CAB-01,209,cabinet,1,,"Cabinet 1, Rm 209", +209-FRIDGE,209,refrigerator,,,"Refrigerator, Rm 209",4C +209-FRIDGE-S1,209,shelf,1,209-FRIDGE,"Fridge shelf 1 (top)", +213-F20,213,freezer,,,"-20C freezer, Rm 213", +213-F20-S2-D8,213,drawer,8,213-F20,"-20C shelf 2, drawer 8", +``` + +- `location_id` is the stable key and the QR-code payload. Format `ROOM-UNIT[-SUB]`, uppercase, no spaces. +- `kind` is a closed set: `cabinet | drawer | shelf | refrigerator | freezer | bench | floor | bin | other`. +- `parent_id` gives nesting, so "everything in the -20" is one query. + +### `data/items.csv` + +```csv +item_id,name,category,location_id,quantity,unit,vendor,catalog_no,lot,received,expires,status,last_verified,verified_by,source,photo_id,notes +``` + +| Field | Purpose | +|---|---| +| `item_id` | `itm-00001`, assigned by script; never reused | +| `name` | free text, but normalized (`DNeasy PowerSoil Kit`) | +| `category` | closed set: `kit \| reagent \| enzyme \| consumable \| sample \| equipment \| tool \| media \| antibody \| other` | +| `quantity` / `unit` | nullable — don't block an entry on counting | +| `received` / `expires` | ISO 8601 `YYYY-MM-DD`, partial dates allowed (`2019-11`) | +| `status` | `present \| low \| consumed \| discarded \| missing \| unverified` | +| `last_verified` / `verified_by` | **the anti-staleness field.** Drives "hasn't been laid eyes on since 2021" reports | +| `source` | `legacy-xlsx \| manual \| photo-llm` — provenance, so we always know what a machine wrote | +| `photo_id` | links to `descriptions/.md` | + +Deliberately omitted for now: price, grant/budget code, per-aliquot tracking, chemical hazard/SDS fields. Each is a real need for some labs; none should block v1. `notes` absorbs them until proven necessary. + +### `descriptions/.md` + +Long LLM prose does not belong in a CSV cell. One markdown file per photo, frontmatter for machines, body for humans: + +```markdown +--- +photo_id: 209-CAB-01_20260803 +photo: photos/209-CAB-01_20260803.jpg +location_id: 209-CAB-01 +taken_at: 2026-08-03 +taken_by: sr320 +model: claude-opus-5 +described_at: 2026-08-03T14:22:00Z +review_status: pending # pending | approved | corrected +reviewed_by: +candidate_items: 7 +--- + +Upper shelf holds four boxes of P1000 filter tips (two open) and a rack of +15 mL Falcon tubes. Lower shelf: a Qiagen DNeasy PowerSoil box, partially +open, and two unlabeled amber bottles... +``` + +--- + +## 5. Phases + +Each phase ends in something usable on its own. Estimates are working-session hours, not calendar time. + +### Phase 0 — Decisions (blocking, ~30 min of your time) +Answer §7. Nothing below can be built without the repo-visibility and photo-privacy calls. + +### Phase 1 — Canonical data out of the spreadsheet (~4–6 h) +- `scripts/migrate_legacy.py`: read all 5 sheets, normalize rooms/kinds, mint `location_id`s, **split semicolon- and comma-delimited contents into individual item rows**, pull parenthetical dates (`(11/27/2019)`) into `received`, emit `locations.csv` + `items.csv`. +- Every migrated row gets `source=legacy-xlsx`, `status=unverified`, `last_verified=2021-08-29` (the sheet's own claim). **The 5-year staleness becomes visible data, not a footnote.** +- Splitting is imperfect on strings like `Microcentrifuge tubes, conical screw top tubes, screw caps` — the script flags ambiguous splits in `data/review_queue.csv` for a human pass rather than guessing silently. +- `scripts/validate.py` + a CI workflow: schema check, closed-set enforcement, `location_id` referential integrity, ISO date check, duplicate `item_id` check. This is what keeps the data clean once several people are editing. +- **Done when:** `items.csv` has ~500 rows, validation is green in CI, and the xlsx is never opened again. **✅ Done 2026-08-03: 499 items, 173 locations, 16 review-queue rows.** + +### Phase 2 — Make things findable (~6–8 h) +- `scripts/build_site.py` → `inventory.json` + one page per location; GitHub Action rebuilds on every push to `data/`. +- Single-page search UI: instant fuzzy search across name/category/notes/location, filter by room and status, mobile-first layout. Vendored Fuse.js, no framework, no bundler. +- Location deep links (`/#209-CAB-01`) so a QR code lands on "here is what is in this drawer." +- `scripts/make_qr_labels.py` → printable PDF sheet of QR stickers, one per location. **This is the feature that makes the system feel real** — scan the drawer, see the contents. +- **Done when:** a grad student on a phone can answer "do we have EZ DNA Methylation-Gold?" in under 10 seconds. + +### Phase 3 — Make writing easy (~4–6 h) +- GitHub Issue Forms: **Add item**, **Mark consumed / low**, **Found something not listed**, **Verified a location**. Dropdowns populated from `locations.csv` (regenerated by the build Action, so they never drift). +- Action parses the issue → opens a PR against `items.csv` → validation runs → one click to merge. Issue auto-closes on merge. +- Same forms rendered as a link from every location page on the site, so the path is: scan QR → "mark consumed" → done. +- **Done when:** adding an item requires zero git knowledge and takes under 30 seconds. + +### Phase 4 — Photos + LLM description (~8–10 h) — *see §6* +The re-inventory engine. Optional in the sense that Phases 1–3 stand alone, but this is what actually gets 2021 data refreshed to 2026. + +### Phase 5 — Keep it alive (~4 h, incremental) +- Scheduled Action: expiring/expired reagent report → opens a dated issue. +- **Verification campaigns:** monthly Action picks the N locations with the oldest `last_verified` and opens a small, finishable issue ("verify these 3 drawers"). Staleness becomes a slow drip of 5-minute tasks instead of a dreaded annual audit. +- Dashboard panel on the site: items by room, % verified in last 12 months, count expired. +- Optional: catalog-number lookup to auto-fill vendor/product name. + +--- + +## 6. Photo + LLM pipeline (Phase 4, in detail) + +### Flow + +``` +phone photo ──► photos/inbox/ (drag-drop into a PR, or an upload issue) + │ + ▼ + Action: describe-photos.yml + 1. downscale to 1600px long edge, JPEG q80, strip EXIF GPS + 2. derive location_id from filename or issue form field + 3. Claude vision call, structured JSON out + 4. write descriptions/.md + 5. append candidate rows to data/candidates.csv + 6. open PR: "Photo intake: 209-CAB-01 (7 candidate items)" + │ + ▼ + Human reviews PR: edits names, deletes hallucinations, + sets quantities, ticks review_status: approved + │ + ▼ + Merge ──► rows move into items.csv with source=photo-llm, + last_verified = photo date, verified_by = reviewer +``` + +### The model call + +- **Model:** `claude-opus-5` for intake accuracy; `claude-sonnet-5` if volume makes cost matter. Both handle multi-image messages, so a whole drawer can be 3 angles in one call. +- **Structured output:** define a tool/JSON schema (`items[]` with `name`, `category`, `quantity`, `confidence`, `text_visible_on_label`) rather than parsing prose. Ask for a separate free-text `overview` for the description file. +- **Prompt discipline** — this is where quality lives: + - Pass `locations.csv` context and the existing item list for that location, so the model can say "matches existing item itm-00412" instead of creating a duplicate. + - Require verbatim transcription of visible label text in `text_visible_on_label`, separate from the inferred `name`. Makes hallucinated catalog numbers obvious in review. + - Require a per-item `confidence` and an explicit `uncertain: true` for anything partially occluded. Low-confidence rows render as unchecked boxes in the PR. + - Instruct it **not** to guess lot numbers, expiration dates, or catalog numbers unless legibly visible. +- **Cost:** roughly 1.5–2k input tokens per image plus output. A 200-photo full-lab sweep is a few dollars. Not a budget consideration. +- **Secrets:** `ANTHROPIC_API_KEY` as a repo secret. Workflow must be `pull_request_target`-free and only run on trusted branches so the key isn't exposed to fork PRs. + +### Storage + +- Downscaled JPEGs (~250 KB) committed directly. 300 photos ≈ 75 MB — comfortably under limits, no Git LFS, no external bucket. Revisit only past ~1 GB. +- Strip GPS EXIF on ingest, always. +- Originals are not kept. If someone wants archival-quality photos later, that's a separate bucket decision. + +### Guardrails + +- LLM output **never** writes to `items.csv` directly — only to `candidates.csv` inside a PR. +- `source=photo-llm` is permanent provenance; a later audit can always ask "which rows did a model write?" +- A photo whose PR is never reviewed leaves `review_status: pending`, and the Phase 5 report surfaces the backlog. + +--- + +## 7. Decisions + +### Resolved (2026-08-03) + +1. **Repo visibility: public.** Consequence, and it is not optional: since the repo is public from day 1, the photo guardrails have to carry the whole privacy load themselves. That makes three Phase 4 items **mandatory rather than nice-to-have**: + - EXIF stripping (all tags, not just GPS) on every ingested image. + - A committed `PHOTO_POLICY.md` — no notebook pages, no screens, no whiteboards, no people, no printed sample manifests in frame — linked from every photo-intake issue form. + - Human review of the photo *itself* in the intake PR, not just the LLM's item list. Reviewers are checking for accidentally-captured content, and a photo can be rejected on privacy grounds alone. + + Also note: a merged-then-deleted photo stays in git history forever and will already have been scraped. Removing one means a history rewrite. The review step is the only real control, so it has to happen before merge. + +2. **Maintainers: `@sr320` and `@kubu4`**, both with merge rights. Implemented as a `CODEOWNERS` file requiring review from either. Two people is the right number — enough that nobody is a single point of failure, few enough that review actually happens. + +### Still open (not blocking Phase 1) + +3. **Freezer boxes: in or out of scope?** Sample-level tracking (-80 boxes, positions, individual DNA extractions) is a genuinely different problem — thousands of rows, needs plate/position coordinates, and usually wants its own tool. I recommend **v1 tracks the box, not the tube**, with a hook to add sample-level later. +4. **Rooms 230 and the -80 room** appear in the spreadsheet with 2 rows each. In scope, or someone else's space? Migrated for now; trivial to drop. +5. **Item owners.** The spreadsheet tags several storage bins "- Goetz". An `owner` column is in the schema and populated where the legacy data made it explicit. Worth confirming this is a concept you want to keep tracking. + +--- + +## 8. Risks + +| Risk | Mitigation | +|---|---| +| **Nobody adopts it; back to Post-its.** The real failure mode. | Phase 2 before Phase 3 — deliver search value before asking for data entry. QR stickers make it tangible. Keep the add-item form to 4 fields. | +| Migration mangles semicolon-delimited contents | Ambiguous splits go to `review_queue.csv`, not silently into the data. Original xlsx stays in `legacy/` forever. | +| LLM invents plausible catalog numbers | Verbatim-label field + confidence + mandatory human PR review. Never auto-merge. | +| Photos leak sensitive content | Private repo (decision 1), EXIF GPS stripping, explicit "no notebooks/screens" rule, review step before merge. | +| CSV merge conflicts with several editors | Rows appended at end, sorted only by a scripted step; conflicts are rare and trivially resolvable. Validation catches damage. | +| Bit rot in the site | No bundler, no npm, vendored JS, Python stdlib. Rebuildable from `scripts/` alone. | + +--- + +## 9. Suggested first commit + +Phase 1 is self-contained and unblocks everything: write `migrate_legacy.py`, generate `locations.csv` + `items.csv` + `review_queue.csv`, add `validate.py` and the CI workflow. That single PR turns a stale binary into queryable, version-controlled data — and makes the scale of the re-inventory job concrete before we invest in the photo pipeline. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e5e770 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Roberts Lab Inventory + +Searchable, version-controlled inventory of lab reagents, kits, consumables, and equipment — replacing the spreadsheet in [`legacy/`](legacy/). + +Development plan and architecture: **[PLAN.md](PLAN.md)** · Photo rules (public repo): **[PHOTO_POLICY.md](PHOTO_POLICY.md)** + +## Status + +**Phase 1 complete** — the spreadsheet has been migrated to canonical CSVs with CI validation. Search UI, add-item forms, and photo intake are still to come; see [PLAN.md §5](PLAN.md#5-phases). + +| | | +|---|---| +| Items | 499 | +| Locations | 173 | +| Verified since 2021 | **0%** — everything carries `status: unverified` | +| Rows needing a human pass | 16, in [`data/review_queue.csv`](data/review_queue.csv) | + +## The data + +| File | What it is | +|---|---| +| [`data/items.csv`](data/items.csv) | One row per item. The inventory. | +| [`data/locations.csv`](data/locations.csv) | Controlled vocabulary of physical places, nested via `parent_id`. | +| [`data/review_queue.csv`](data/review_queue.csv) | Legacy rows too ambiguous to migrate automatically. Each has a stated reason. | + +`location_id` is the stable key — `209-CAB-01`, `213-F20-S02-D08`, `209-FRIDGE-DOOR-SHELF-1`. It's built to be short because it becomes a URL fragment and a QR-sticker payload. + +Two fields carry more weight than they look like they do: + +- **`last_verified`** — every migrated row says `2021-08-29`, because that's what the spreadsheet's own sheet names claim. This makes five years of staleness a queryable fact instead of a caveat. +- **`source`** — `legacy-xlsx`, `manual`, or `photo-llm`. Permanent provenance, so an audit can always ask which rows a model wrote. + +## Working with it + +```bash +pip install -r requirements.txt +``` + +Validate before opening a PR (CI runs the same thing): + +```bash +python3 scripts/validate.py +``` + +Regenerate the CSVs from the frozen spreadsheet: + +```bash +python3 scripts/migrate_legacy.py +``` + +Note that re-running the migration **overwrites `data/`**, discarding hand edits. It's a one-time tool kept for reproducibility and for re-tuning the item categorizer; once real edits start landing, `items.csv` is the source of truth and the spreadsheet is history. + +## Conventions + +- Dates are ISO 8601, and partial dates are allowed where that's the honest precision: `2019`, `2019-11`, `2019-11-27`. +- `category`, `status`, `kind`, and `source` are closed sets, enforced in CI. Adding a value means editing `scripts/validate.py` in the same PR. +- `quantity` is a number or empty. Ranges, guesses, and "about half a box" go in `notes`. +- Don't block an entry on details you don't have. A name and a location is a useful row; the rest can be filled in later. + +## Maintainers + +[@sr320](https://github.com/sr320) and [@kubu4](https://github.com/kubu4) — either can review and merge. From eda3018285d0f9f0561b24340fb6e5bae80efbb9 Mon Sep 17 00:00:00 2001 From: Steven Roberts Date: Mon, 3 Aug 2026 11:18:26 -0700 Subject: [PATCH 2/3] Migrate legacy spreadsheet to validated CSVs Converts legacy/Roberts Lab Inventory.xlsx into data/items.csv (499 items), data/locations.csv (173 locations), and data/review_queue.csv (16 rows), with CI validation gating every future edit. The migration never guesses silently. Where the legacy data is ambiguous the row goes to review_queue.csv with a stated reason instead of into items.csv: - Semicolons are a reliable delimiter here and are split automatically. Commas are not -- "Microcentrifuge tubes, conical screw top tubes, screw caps" is three items but "DNeasy 96 blood and tissue kit (4) & (50)" is one -- so comma-only cells stay whole and carry a suggested split. - Five rows on the drawers/cabinets sheet are coarse restatements of the dedicated Rm 213 shelf sheet. Importing them would have produced ~40 duplicate items, so they go to the queue for reconciliation. - The date column held commentary ("Very likely all expired", "n.d.", "08/2010 from FHL"). That text moves to notes rather than being dropped, and partial dates keep their real precision (08/2010 -> 2010-08) instead of having a day invented for them. Every migrated row carries status=unverified and last_verified=2021-08-29, the spreadsheet's own claim about when it was last touched. Five years of staleness becomes a queryable field that validate.py reports on every run, rather than a caveat someone has to remember. The source column (legacy-xlsx | manual | photo-llm) makes provenance permanent, so a later audit can always ask which rows a model wrote. location_id is kept short and stable (209-CAB-01, 213-F20-S02-D08) because it becomes a URL fragment and a QR-sticker payload; deriving ids from prose labels produced 60-character identifiers. Prose-numbered places get an X01 sequence so they can never collide with numerically-numbered ones. Categories include glassware and office alongside the lab ones -- without them roughly half the inventory landed in `other`, which would make the planned category filter useless. validate.py is stdlib-only so there is nothing to install and nothing to break when a dependency moves. Verified against nine injected faults (duplicate ids, dangling location refs, non-ISO dates, self-parenting, unknown category, photo-llm row missing photo_id): all caught, exit 1. Co-Authored-By: Claude Opus 5 --- data/items.csv | 500 +++++++++++++++++++++++++++++++++ data/locations.csv | 174 ++++++++++++ data/review_queue.csv | 17 ++ requirements.txt | 1 + scripts/migrate_legacy.py | 565 ++++++++++++++++++++++++++++++++++++++ scripts/validate.py | 258 +++++++++++++++++ 6 files changed, 1515 insertions(+) create mode 100644 data/items.csv create mode 100644 data/locations.csv create mode 100644 data/review_queue.csv create mode 100644 requirements.txt create mode 100644 scripts/migrate_legacy.py create mode 100644 scripts/validate.py diff --git a/data/items.csv b/data/items.csv new file mode 100644 index 0000000..c80cc1f --- /dev/null +++ b/data/items.csv @@ -0,0 +1,500 @@ +item_id,name,category,location_id,quantity,unit,vendor,catalog_no,lot,received,expires,status,last_verified,verified_by,owner,source,photo_id,notes +itm-00001,Plastic graduated cylinders,glassware,209-CAB-01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00002,Plastic boxes to hold sample tubes,sample,209-CAB-02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00003,"Qubit RNA and DNA buffer, qubit assay tubes, DNA/RNA dye",equipment,209-DRW-03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00004,Syringes and needles,consumable,209-DRW-04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00005,Shaker supplies,equipment,209-DRW-05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00006,Kason 0067 stainless steel shelf clip for -80C freezer,equipment,209-DRW-05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00007,vacuum-releasing paddles for -80°C,tool,209-DRW-05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00008,Protective eyewear,other,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00009,zip ties,tool,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00010,tape,office,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00011,nail polish,other,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00012,Teflon tape,office,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00013,paintbrush,tool,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00014,timer,tool,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00015,cell screen (10 mesh),tool,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00016,switchblade,tool,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00017,sunglasses,other,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00018,notepad,office,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00019,set of keys,tool,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00020,red light bulb,equipment,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00021,siphon,tool,209-DRW-06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00022,Small tools drawer: 0.2 um (25 mm diameter) filters,tool,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00023,glass cellometers,equipment,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00024,1 scalpel handle (3),tool,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00025,small scissors,tool,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00026,tweezers,tool,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00027,shucking tools,tool,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00028,probe,tool,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00029,labratory scoop,tool,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00030,long handle micro scoop/spatulas,tool,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00031,handheld motorized pestle homogenizer,equipment,209-DRW-07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00032,dissection plates,consumable,209-DRW-08,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00033,Plastic boxes to hold sample tubes,sample,209-CAB-09,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00034,Plastic bottles,glassware,209-CAB-10,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00035,plastic containers,consumable,209-CAB-10,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00036,2000mL flask,glassware,209-CAB-10,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00037,paper and plastic boxes to hold sample tubes,sample,209-CAB-10,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00038,Electrophoresis supplies,other,209-CAB-11,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00039,Electrophoresis supplies,other,209-CAB-12,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00040,Large bottle brushes,glassware,209-CAB-13,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00041,Pipet parts,consumable,209-CAB-15,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00042,repeater pipets,equipment,209-CAB-15,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00043,serological pipettors,equipment,209-CAB-15,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00044,Serological pipets,consumable,209-CAB-16,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00045,Sam's drawer,other,209-DRW-17,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00046,Hot plate pads? (there are large mesh squares w/ a white circle in the middle. unsure what they are),equipment,209-DRW-18,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00047,Centrifuge bottles,glassware,209-DRW-19,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00048,centrifuge tubes,equipment,209-DRW-19,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00049,Miscellaneous handbooks and manuals,office,209-DRW-20,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00050,Histology cassettes,consumable,209-DRW-21,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00051,microscope glass cover slips,glassware,209-DRW-21,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00052,coverwell incubation chambers PC50 and PC200,consumable,209-DRW-21,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00053,wheaton ribbed glass jars in same box as incubation chambers *unsure what these are*,glassware,209-DRW-21,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00054,Extra keyboards,equipment,209-CAB-27,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00055,nintendo,other,209-CAB-27,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00056,computer mouse,equipment,209-CAB-27,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00057,bowling supplies,other,209-CAB-27,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00058,White-out,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00059,staples,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00060,glue stick,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00061,statmark pens,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00062,green flag post-its,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00063,invisible tape,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00064,pens,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00065,pencils,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00066,sharpies,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00067,scissors,tool,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00068,tacks,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00069,paper clips,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00070,batteries,consumable,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00071,Lab logo stickers,office,209-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00072,All-weather copier paper,office,209-DRW-31,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00073,mini composition notebooks,office,209-DRW-31,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00074,high gloss photo paper,office,209-DRW-31,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00075,file folders,office,209-DRW-31,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00076,tube labels - 20 sheets (cat: 89097-572),consumable,209-DRW-31,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00077,Tough Spots tube labels,consumable,209-DRW-31,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00078,"Tough-Spots, 3/8"" diameter, white, on sheets (cat: 9185-1000)",office,209-DRW-31,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00079,business card paper,office,209-DRW-31,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00080,command strips,consumable,209-DRW-31,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00081,Printer paper,office,209-DRW-33,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00082,laser lables,other,209-DRW-33,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00083,Scientific calculator,office,209-DRW-34,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00084,empty google notebook,office,209-DRW-34,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00085,graph paper on clipboard,office,209-DRW-34,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00086,paper/plastic report covers,office,209-DRW-34,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00087,EZNA mollusc dna kit product manual,kit,209-DRW-34,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00088,tissue FIX container (50mL) manual,consumable,209-DRW-34,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00089,Mostly empty composition notebook (only first 3 pages used),office,209-DRW-35,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00090,DNeasy blood and tissue handbook,sample,209-DRW-35,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00091,800mL commode collection container,consumable,209-DRW-37,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00092,Glass bottles,glassware,209-CAB-38,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00093,Glass bottles,glassware,209-CAB-39,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00094,plastic bottles,glassware,209-CAB-39,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00095,overflow flasks,glassware,209-CAB-39,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00096,Standard PCR plates,consumable,209-DRW-40,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00097,Standard PCR caps,consumable,209-DRW-40,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00098,aluminum adhesive PCR plate seal,consumable,209-DRW-40,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00099,qPCR plates,consumable,209-DRW-41,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00100,qPCR caps,consumable,209-DRW-41,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00101,Multichannel pipettes,consumable,209-DRW-42,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00102,Tube magnets,equipment,209-DRW-43,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00103,DynaMag,equipment,209-DRW-43,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00104,epimag magnetic separator,equipment,209-DRW-43,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00105,nucleomag SEP,equipment,209-DRW-43,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00106,PCR tubes,consumable,209-DRW-44,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00107,Weigh boats,glassware,209-DRW-45,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00108,weigh paper,glassware,209-DRW-45,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00109,stir bars,tool,209-DRW-45,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00110,funnels,tool,209-DRW-45,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00111,Mettler Toledo titration electrode DGi115-SC,equipment,209-DRW-46,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00112,thermometer,tool,209-DRW-46,2,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00113,empty thermometer cases,tool,209-DRW-46,3,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00114,Glass filter supplies,consumable,209-DRW-47,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00115,"3"" W.S. Tyler U.S. Standard Sieve - Stainless steel frame/mesh, No. 635 - 0.020mm opening, full height",tool,209-DRW-47,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00116,"PluriSelect 1um, 3um, 5um, 8um filters",consumable,209-DRW-47,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00117,Flasks,glassware,209-CAB-48,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00118,Flasks,glassware,209-CAB-49,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00119,100mL beakers,glassware,209-CAB-49,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00120,250 mL beakers,glassware,209-CAB-49,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00121,600 mL beakers,glassware,209-CAB-49,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00122,1000 mL beaker,glassware,209-CAB-49,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00123,Protein supplies,other,209-CAB-50,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00124,blotting paper,office,209-CAB-50,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00125,Mass spec autosampler vials,consumable,209-CAB-50,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00126,1-200uL gel loading pipet tips,consumable,209-CAB-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00127,disposable cuvettes,glassware,209-CAB-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00128,10% acetic acid,reagent,209-CAB-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00129,coomassie stain,reagent,209-CAB-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00130,E-Gel starter pak,other,209-CAB-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00131,ProBond purification system,other,209-CAB-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00132,"macrospin columns (50-450uL loading, 30-300ug capacity)",consumable,209-CAB-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00133,large blotting paper,office,209-CAB-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00134,orange clips,other,209-CAB-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00135,Proline algae food,media,209-CAB-52,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00136,pro-culture,other,209-CAB-52,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00137,distilled water,reagent,209-CAB-52,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00138,hand soap,other,209-CAB-52,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00139,Bleach,other,209-CAB-53,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00140,bottle brushes,glassware,209-CAB-53,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00141,water level alarms,reagent,209-CAB-53,2,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00142,saltwater master test kit,kit,209-CAB-53,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00143,Chemicals,other,209-CAB-54,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00144,paper towels,office,209-CAB-54,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00145,tips,consumable,209-CAB-54,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00146,syringes,consumable,209-CAB-54,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00147,Serological pipets,consumable,209-CAB-55,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00148,chemicals,other,209-CAB-55,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00149,syringes,consumable,209-CAB-55,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00150,"Upper: empty. Lower: histology blocks x2 (one completely empty, one with samples inside)",sample,213-CAB-01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00151,"Upper: histology slide cases. Lower: histology block x1 (empty), empty histology cassettes",consumable,213-CAB-02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00152,Used notebooks,office,213-DRW-16,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00153,Notes,other,213-DRW-17,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00154,"eDNA, gDNA, & cDNA samples, Primer stocks, misc. samples",sample,213-DRW-27,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,,Small freezer +itm-00155,Strip cap trays,consumable,213-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00156,falcon tube holders,consumable,213-DRW-28,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00157,Keyboard,equipment,213-DRW-29,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00158,Acids,other,213-CAB-32,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00159,Acids,other,213-CAB-33,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00160,2 filled hazardous waste containers,consumable,213-CAB-35,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00161,One complete snapzoom,other,213-DRW-36,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00162,one snapzoom missing actual device,other,213-DRW-36,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00163,one empty histology slide box,consumable,213-DRW-36,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00164,2 phone attachments for microscope,equipment,213-DRW-36,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00165,old iphone,other,213-DRW-36,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00166,iphone lightning-USB cable,other,213-DRW-36,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00167,empty tube box with cell counting equipment inside,consumable,213-DRW-36,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00168,Plain capillary tubes,consumable,213-DRW-37,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00169,weigh boats,glassware,213-DRW-37,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00170,optical lens cleaner,other,213-DRW-37,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00171,falcon 7 mL pipette,consumable,213-DRW-37,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00172,"50 mL conical tube labeled ""MTPW""",consumable,213-DRW-37,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00173,Microscope bulb and lenses,equipment,213-DRW-40,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00174,cover glass,other,213-DRW-40,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00175,hemacytometer,other,213-DRW-40,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00176,counting chamber,other,213-DRW-40,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00177,Wolfgang puck automatic one-touch food packaging system,media,213-DRW-42,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00178,Plastic bags,consumable,213-DRW-43,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00179,Salad spinner,other,213-CAB-44,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00180,styrofoam containers,consumable,213-CAB-44,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00181,Ethanol,reagent,213-CAB-45,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00182,whiteboard cleaning spray,other,213-CAB-45,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00183,bleach,other,213-CAB-45,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00184,another salad spinner,other,213-CAB-45,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00185,Olivia's Drawer: multichannel pipette + random things for microplate assays + nail polish+ white out+ ruler + flash drive + graduated cylinder,glassware,213-DRW-46,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00186,? lots of random parts from the brand HOBO,other,213-DRW-47,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00187,Digital heater temperature control,other,213-DRW-48,4,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00188,Equipment manuals,office,213-DRW-49,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00189,Digital heater temperature control,other,213-DRW-50,4,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00190,Weight boats (large and small),other,213-DRW-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00191,victorinox knife,other,213-DRW-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00192,green sheet,other,213-DRW-51,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00193,"One pair of black, rubber cleaning gloves",consumable,213-DRW-52,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00194,3 pairs of grey rockland guard gloves,consumable,213-DRW-52,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00195,one pair gardening gloves,consumable,213-DRW-52,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00196,microfiber towels,other,213-DRW-52,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00197,Cardboard trays (like what you would get at a fast food restaurant),media,213-DRW-53,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00198,Glass fiber filters (25 mm) Type A/E,consumable,213-DRW-54,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00199,circle stickers for labelling,office,213-DRW-54,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00200,red autoclave bags,consumable,213-DRW-54,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00201,50mL tubes+ Song Joy wave maker (2 boxes),consumable,213-DRW-55,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00202,Box of Dispensette: missing some parts,other,213-DRW-56,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00203,bunsen burner lighter and 2 bic lighters,other,213-DRW-56,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00204,Digital heater temperature control,other,213-DRW-57,2,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00205,Zipties,other,213-DRW-58,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00206,Counter,other,213-DRW-59,6,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00207,Clipboard,office,213-DRW-60,5,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00208,2 yellow lined note pads,other,213-DRW-60,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00209,autoclave gloves,consumable,213-DRW-61,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00210,PAXgene tissue RNA kit,kit,213-OTH-X01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00211,PAXgene tissue miRNA kit,kit,213-OTH-X01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00212,Storage bin with field gear,other,213-OTH-X02,,,,,,,,unverified,2021-08-29,,Goetz,legacy-xlsx,, +itm-00213,Satellite tag and pipette tips,equipment,213-BIN-X01,,,,,,,,unverified,2021-08-29,,Goetz,legacy-xlsx,, +itm-00214,Satellite tags,equipment,213-BIN-X02,,,,,,,,unverified,2021-08-29,,Goetz,legacy-xlsx,, +itm-00215,unspecified contents,other,213-BIN-X03,,,,,,,,unverified,2021-08-29,,Goetz,legacy-xlsx,, +itm-00216,unspecified contents,other,213-BIN-X04,,,,,,,,unverified,2021-08-29,,Goetz,legacy-xlsx,, +itm-00217,Gloves,consumable,213-BIN-X05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00218,pipette tips,consumable,213-BIN-X05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00219,etc,other,213-BIN-X05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00220,Aluminum foil,consumable,213-SH-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00221,kimtech wipes,consumable,213-SH-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00222,nitrile gloves,consumable,213-SH-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00223,Resin tube vacuum manifold,equipment,213-SH-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00224,BCS plates,sample,213-BIN-X06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00225,BCS plates,sample,213-BIN-X07,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00226,BCS plates and samples in 100% ethanol,sample,213-BIN-X08,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00227,BCS plates and samples in 100% ethanol (on top: cases filled with histology slides),sample,213-BIN-X09,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00228,unknown (couldn't reach them),other,213-BIN-X10,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00229,unknown (couldn't reach them),other,213-BIN-X11,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00230,unknown (couldn't reach them),other,213-BIN-X12,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00231,Pipette tips and misc. samples,sample,213-CAB-X01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00232,Big buckets,other,M80-SH-X01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00233,large water containers with taps,consumable,M80-SH-X01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00234,large coolers,other,M80-SH-X01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00235,shellfish nets/bags,consumable,M80-SH-X01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00236,Metal trays for boxes to put in -80˚,consumable,M80-SH-X02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00237,"Misc. cables, computer mice, web cam, CDR, DVD, speakers, Mac Mini, external drives, backup HDD for OWL, smart switch, heat lamp",equipment,230-SH-X01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00238,"Assorted teas, tupperware, reusable shopping bags, extension cords",consumable,230-SH-X02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00239,Methylflash Global DNA Methylation (5-mC) ELISA Easy Kit (colorimetric) 4C,kit,209-FRIDGE-S01,,,,,,2018-09-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00240,Diluted Samples I.A.T,sample,209-FRIDGE-S01,,,,,,2020-11-25,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00241,MinElute Spin Columnns (50),other,209-FRIDGE-S01,,,,,,2013-07-25,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00242,Calcium Assay Kit,kit,209-FRIDGE-S01,,,,,,2020-11-25,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00243,ProFound c-Myc Tag IP/Co-IP Application Set,other,209-FRIDGE-S01,,,,,,2008-07-08,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00244,DNA C. gigas samples in snap cap tubes (blue tube rack),sample,209-FRIDGE-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00245,DNA C. sikamea samples in snap cap tubes (green and red tube rack),sample,209-FRIDGE-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00246,P5/P7 TRUSEQ in snap cap tubes (orange tube rack),sample,209-FRIDGE-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00247,"Bag labeled ""Shelly"" with sample tubes",sample,209-FRIDGE-S01,,,,,,2021-10-30,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00248,Flow Cell compatible with MinION and GridION,other,209-FRIDGE-S01,4,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00249,MethylMiner Methylated DNA enrichment kit (x5 boxes),kit,209-FRIDGE-S01,,,,,,2019-02-14,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00250,Rneasy MinElute Spin Columns (in plastic grocery bag),consumable,209-FRIDGE-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00251,Coomassie Protein Assay Reagant,reagent,209-FRIDGE-S01,,,,,,2010-08,,unverified,2021-08-29,,,legacy-xlsx,,from FHL +itm-00252,Coomassie Protein Assay Reagant,reagent,209-FRIDGE-S01,,,,,,2009-01-13,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00253,Fisherbrand Regenerated Cellulose Tubular Membrane,other,209-FRIDGE-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00254,BIO-RAD Protein Assay Dye Reagant Concentrate,reagent,209-FRIDGE-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00255,Unknown samples in blue cap BD Falcon tubes,sample,209-FRIDGE-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00256,"RNA samples (in white box labeled ""LOTTERMOS"")",sample,209-FRIDGE-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00257,Colleen Burge Sea Star samples RNA,sample,209-FRIDGE-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00258,Rp cDNA Dave,sample,209-FRIDGE-S02,,,,,,2012-08-07,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00259,Oly larval cDNA Dave,sample,209-FRIDGE-S02,,,,,,2012-07-23,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00260,D3373-02 RNase A,enzyme,209-FRIDGE-S02,,,,,,2015-11-09,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00261,SYPRO Ruby Protein gel stain,reagent,209-FRIDGE-S02,,,,,,2007-12-05,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00262,"Quant-iT RNA Assay Kit, 100 Assays",kit,209-FRIDGE-S02,,,,,,2009-09-18,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00263,FastPlasmid Mini Lysis Solution,reagent,209-FRIDGE-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00264,Scallop Mass Excized Libraries,sample,209-FRIDGE-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00265,"Scallop Stages cDNA Libraries (Set A, Set B, Met A, Met B)",sample,209-FRIDGE-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00266,"Bay Scallop D-Hinge Libraries (D1, D2, etc.)",sample,209-FRIDGE-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00267,"Scallop gonad libraries (FR1+2, FR4)",sample,209-FRIDGE-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00268,EpiTect Spin Columns with Collection Tubes (48) (For Bisulfite kit),kit,209-FRIDGE-S02,,,,,,2009-11-03,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00269,MICROBExpress Bacterial mRNA Purification,other,209-FRIDGE-S02,,,,,,2008-12-10,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00270,EpiTect Spin Columns with Collection Tubes (48),consumable,209-FRIDGE-S02,,,,,,2014-05-15,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00271,RiboMinus Eukaryote Kit for RNA-Seq,kit,209-FRIDGE-S02,,,,,,2009-10-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00272,Ni-NTA Magnetic Agarose Beads (6 x 1 ml),equipment,209-FRIDGE-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00273,cDNA Synthesis Kit,kit,209-FRIDGE-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00274,SOLiD ePCR Kit (box 3 of 3),kit,209-FRIDGE-S02,,,,,,2010-04-22,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00275,mRNA Catcher PLUS,other,209-FRIDGE-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00276,High Sensitivity DNA Reagants (For use with the Agilent Bioanalyzer System),reagent,209-FRIDGE-S02,,,,,,2020-11-14,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00277,MicroPoly(A)Purist,other,209-FRIDGE-S02,,,,,,2011-02-09,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00278,MQ Binding Beads,reagent,209-FRIDGE-S03,,,,,,2015-01-16,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00279,DIthiothreitol (DTT),other,209-FRIDGE-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00280,Tris(2-carboxyethyl)phosphine hydrochloride,reagent,209-FRIDGE-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00281,ZOOM Carrier Ampholytes 3-10,other,209-FRIDGE-S03,,,,,,2008-09-19,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00282,TRIzol LS Reagant (100 ml),reagent,209-FRIDGE-S03,,,,,,2018-12-18,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00283,CellLytic MT Mammalian Tissue Lysis/ Extraction Reagant + Protease Incubator Cocktail,sample,209-FRIDGE-S03,,,,,,2009-04-24,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00284,BAN-Phase Separation Reagant: 4-Bromoanisole,reagent,209-FRIDGE-S03,,,,,,2008-03-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00285,10X Wash Buffer (28 mL),reagent,209-FRIDGE-S03,,,,,,2017-04-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00286,5X Bind/Wash Buffer (5 ml),reagent,209-FRIDGE-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00287,TRI Reagant,reagent,209-FRIDGE-S03,,,,,,2018-08-07,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00288,DIthiothreitol (DTT),other,209-FRIDGE-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00289,Iodoacetamide,other,209-FRIDGE-S03,,,,,,2016-11-23,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00290,8-well Assay Strips,consumable,209-FRIDGE-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00291,TRI Reagant Solution,reagent,209-FRIDGE-S03,,,,,,2016-09-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00292,invitrogen Qubit 1X dsDNA HS Assay Kit,kit,209-FRIDGE-S03,,,,,,2018-04-10,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00293,"invitrogen Qubit dsDNA BR Assay Kit, 100 Assays",kit,209-FRIDGE-S03,,,,,,2017-05-05,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00294,EpiGentek Developer Solution,reagent,209-FRIDGE-S03,,,,,,2017-04-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00295,EpiGentek Developer Solution,reagent,209-FRIDGE-S03,,,,,,2014-11-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00296,invitrogen Low-Salt Elution Buffer (50 ml),reagent,209-FRIDGE-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00297,Glocogen from oyster,other,209-FRIDGE-S03,,,,,,2013-10-07,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00298,Polyacryl Carrier,other,209-FRIDGE-S03,,,,,,2008-07-21,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00299,Fisher BioReagants Phenol/Chloroform/isoamyl Alcohol pH 6.7 +/- 0.2 (100 ml),reagent,209-FRIDGE-S03,,,,,,2009-06-15,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00300,TRI Reagant (in foil-wrapped tube),consumable,209-FRIDGE-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00301,10X Wash Buffer (14 ml),reagent,209-FRIDGE-S03,,,,,,2014-11-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00302,Agencourt Ampure XP,other,209-FRIDGE-S03,,,,,,2015-07-16,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00303,invitrogen TRI Reagant Solution (n silver bag),consumable,209-FRIDGE-S03,,,,,,2016-09-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00304,RNase-Free DNase Set (1500 Kunitz units),enzyme,209-FRIDGE-S03,,,,,,2017-06-29,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00305,RNAzol RT - expired!?,reagent,209-FRIDGE-S03,,,,,,2016-11-14,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00306,AMPure XP (Dungeness Crab Only),other,209-FRIDGE-S03,,,,,,2020-08-03,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00307,"Copper (II) nitrate hydrate, 99.999%",other,209-FRIDGE-S03,,,,,,2010-02-18,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00308,"Shellfish Diet - expired!? typically only good for 6 months, cannot be frozen",other,209-FRIDGE-S03,,,,,,2018-12-13,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00309,Buffer P1 Resuspension buffer (20ml),reagent,209-FRIDGE-S03,,,,,,2013-08-19,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00310,"Qubit RNA HS Assay Kit, 500 assays",kit,209-FRIDGE-S03,,,,,,2019-03-19,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00311,"Qubit RNA HS Assay Kit, 500 assays",kit,209-FRIDGE-S03,,,,,,2013-09-01,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00312,"Qubit RNA HS Assay Kit, 500 assays",kit,209-FRIDGE-S03,,,,,,2019-04-12,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00313,MethylMiner Methylated DNA enrichment kit (x5 boxes in yellow bag),kit,209-FRIDGE-S03,,,,,,2020-12-18,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00314,MethylMiner Methylated DNA enrichment kit (x2 boxes in CRAB ONLY yellow bag),kit,209-FRIDGE-S03,,,,,,2020-02-21,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00315,MethylMiner Methylated DNA enrichment kit (x8 boxes in clear bag),kit,209-FRIDGE-S03,,,,,,2020-12-11,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00316,RNase-Free DNase Set (1500 Kunitz units),enzyme,209-FRIDGE-S03,,,,,,2020-11-06,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00317,"Purification Module with Magnetic Beads, 200 rxn",equipment,209-FRIDGE-S03,,,,,,2019-08-09,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00318,MethylMiner Methylated DNA enrichment kit,kit,209-FRIDGE-S03,,,,,,2020-10-28,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00319,PAXgene RNA MinElute Spin Columns (x5 sheets),consumable,209-FRIDGE-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00320,TruSeq Stranded mRNA HT Kit,kit,209-FRIDGE-S03,,,,,,2016-08-24,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00321,April 2019 Salmon Skin samples from Christian Strigg,sample,209-FRIDGE-S04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00322,Stress Exp Gel Bands,other,209-FRIDGE-S04,,,,,,2010-06-01,,unverified,2021-08-29,,JH,legacy-xlsx,, +itm-00323,RNAzol RT - expired!?,reagent,209-FRIDGE-S04,,,,,,2017-11-30,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00324,4x Solubilization buffer (pH 7.9),reagent,209-FRIDGE-S04,,,,,,2008-10-29,,unverified,2021-08-29,,SJW,legacy-xlsx,, +itm-00325,Formamide (500 ml),other,209-FRIDGE-S04,,,,,,2007-02-01,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00326,Phenol: Chloroform Saturated Solution + Buffer (pH ~8.0),reagent,209-FRIDGE-S04,,,,,,2008-03-13,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00327,Dulbecco's Modified Eagle's Medium/Nutrient Mixture F-12 Ham,media,209-FRIDGE-S04,,,,,,2007-12-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00328,Phenol loose crystals,other,209-FRIDGE-S04,,,,,,2013-10-10,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00329,Diethyl pyrocarbonate approx. 97% NMR,other,209-FRIDGE-S04,,,,,,2008-07-18,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00330,RNAzol RT - expired!?,reagent,209-FRIDGE-S04,,,,,,2015-05-06,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00331,"RNAzol RT (x3, 100 ml bottles)",glassware,209-FRIDGE-S04,,,,,,2021-09-03,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00332,Ethidium Bromide (10 mg/mL),other,209-FRIDGE-S04,,,,,,2007-08-17,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00333,"Phenol: Chloroform, pH 6.7/8/0",other,209-FRIDGE-S04,,,,,,2007-07-31,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00334,Jay - Coral samples,sample,209-FRIDGE-S04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00335,Hydrogen Peroxide,other,209-FRIDGE-S04,,,,,,2008-08-19,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00336,C. gigas tissue in ethanol (Yaamini V.),sample,209-FRIDGE-S04,,,,,,2017-02-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00337,Olympia oyster tissue in ethanol (Laura Spencer),sample,209-FRIDGE-S04,,,,,,2017-02-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00338,Manchester Sampling,other,209-FRIDGE-S04,,,,,,2017-02-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00339,17x-Ethynylestradiol,other,209-FRIDGE-S04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00340,"invitrogen Qubit dsDNA BR Assay Kit, 100 Assays",kit,209-FRIDGE-S04,,,,,,2017-11-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00341,"molecular probes Qubit RNA HS Assay Kit, 100 Assays",kit,209-FRIDGE-S04,,,,,,2015-11-25,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00342,"molecular probes Qubit Protein Assay Kit, 100 Assays",kit,209-FRIDGE-S04,,,,,,2015-11-25,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00343,"tubes in purple tube rack (labeled DH, HT, MG1, MG2, TF)",consumable,209-FRIDGE-S04,,,,,,2015-10-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00344,SEI buffer - 500mL SJW,reagent,209-FRIDGE-S04,,,,,,2024-03-12,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00345,Epiquick Medip Ultra,other,209-FRIDGE-S05,,,,,,2019-01-25,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00346,M. Clark's samples,sample,209-FRIDGE-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00347,RNAzol RT - expired!?,reagent,209-FRIDGE-S05,,,,,,2015-11-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00348,Oly histology cassettes,consumable,209-FRIDGE-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00349,PAxgene DNA Mini Spin Columns,consumable,209-FRIDGE-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00350,Trichloroacetic acid,reagent,209-FRIDGE-S05,,,,,,2016-07-07,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00351,RiboPure,other,209-FRIDGE-S05,,,,,,,,unverified,2021-08-29,,Blood,legacy-xlsx,, +itm-00352,Unknown samples in blue cap BD Falcon tubes,sample,209-FRIDGE-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00353,"TRI Reagant in foil-wrapped tube, PicoBlue immunoscreening kit, LumiGLO Chemiluminescent Substrate A, LumiGLO Chemiluminescent Substrate B, ImmunoPure Immobilized Protein A , Electrophoresis Purity Reagant, horseradish peroxidase antibodies from donkey, Chitin beads, SC Spin Columns 2/pack, Piperazine Diacrylamide, ProBond Resin (12 mL)",kit,209-FRIDGE-S05,2,,,,,,,unverified,2021-08-29,,,legacy-xlsx,,"Very likely all expired; In cardboard box labeled ""HOLD""" +itm-00354,SEI buffer - 1L,reagent,209-FRIDGE-S05,,,,,,2021-09-13,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00355,mngeorge@uw.edu,other,209-FRIDGE-S05,,,,,,2021-09-13,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00356,MSTN Virus samples,sample,209-FRIDGE-DOOR-SHELF-1,,,,,,2006,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00357,poly(I:C),other,209-FRIDGE-DOOR-SHELF-1,,,,,,2026-06-02,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00358,Stratagene,other,209-FRIDGE-DOOR-SHELF-1,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00359,Coomassie (foil wrapped tube),consumable,209-FRIDGE-DOOR-SHELF-1,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00360,Sterile sea water,reagent,209-FRIDGE-DOOR-SHELF-1,,,,,,2008-07-16,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00361,Buffer P1 Resuspension Buffer,reagent,209-FRIDGE-DOOR-SHELF-1,,,,,,2011-04-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00362,Buffer P1 Resuspension Buffer,reagent,209-FRIDGE-DOOR-SHELF-1,,,,,,2012-07-23,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00363,Full Velocity SYBR Green Mix (foil wrapped tube),consumable,209-FRIDGE-DOOR-SHELF-1,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00364,10% Chelex,other,209-FRIDGE-DOOR-SHELF-1,,,,,,2015-02-25,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00365,10 mM Mg SO4*7H2,other,209-FRIDGE-DOOR-SHELF-1,,,,,,2008-05-10,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00366,Serotonin hydrochloride,other,209-FRIDGE-DOOR-SHELF-1,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00367,"Serotonin creatinine sulfate monohydrate, 99%",other,209-FRIDGE-DOOR-SHELF-1,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00368,1 M MgSO4*7H2O sterile filtered,other,209-FRIDGE-DOOR-SHELF-1,,,,,,2008-05-08,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00369,Jake's PCR product,other,209-FRIDGE-DOOR-SHELF-1,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00370,"NuPage 10 mL bottles (LDS sample buffer, Sample reducing agent, TBE-Urea sample buffer, antioxidant)",sample,209-FRIDGE-DOOR-SHELF-2,,,,,,2008,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00371,"Benzamidine hydrochloride hydrate, minimum 98%",other,209-FRIDGE-DOOR-SHELF-2,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00372,10x DNA Loading buffer,reagent,209-FRIDGE-DOOR-SHELF-2,,,,,,2008-11-26,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00373,Iodine + Potassium Iodide solution gylocgen assay,reagent,209-FRIDGE-DOOR-SHELF-2,,,,,,2013-12-10,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00374,Saturated calcium chloride + iodine | potassium iodide glycogen assay,reagent,209-FRIDGE-DOOR-SHELF-2,,,,,,2013-12-10,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00375,Histo blocks: oyster gonad (LS),other,209-FRIDGE-DOOR-SHELF-2,,,,,,2018-06-22,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00376,PAX gene DNA Mini Spin Columns (x4 sheets),consumable,209-FRIDGE-DOOR-SHELF-2,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00377,2x Sso Fast Aliquots,sample,209-FRIDGE-DOOR-SHELF-3,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00378,"Protein - Antibodies, ladder, reducing agent, samples",antibody,209-FRIDGE-DOOR-SHELF-3,,,,,,2005-06-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00379,"Gel loading supplies (ladders, loading dyes, etc.)",reagent,209-FRIDGE-DOOR-SHELF-3,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00380,Ethidium Bromide,other,209-FRIDGE-DOOR-SHELF-3,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00381,0.2 mM filtered seawater,other,209-FRIDGE-DOOR-SHELF-4,,,,,,2012-05-14,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00382,NZY Broth,media,209-FRIDGE-DOOR-SHELF-4,,,,,,2008-05-18,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00383,C. bairdi DNA in Zymo-Spin IC-XM Column from Zymo Quick Prep,consumable,209-FRIDGE-DOOR-SHELF-4,,,,,,2020-01-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00384,Ice pack,other,209-FRIDGE-DOOR-SHELF-4,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00385,Unknown samples in blue cap BD Falcon tubes,sample,209-FRIDGE-DOOR-SHELF-5,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00386,Benchtop Protectors,consumable,209-SHELF-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00387,Sterile TechniSat wipes,consumable,209-SHELF-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00388,15 mL tube storage boxes,consumable,209-SHELF-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00389,Variety of tube storage boxes,consumable,209-SHELF-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00390,"Microcentrifuge tubes, conical screw top tubes, screw caps, assorted microtubes",equipment,209-SHELF-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00391,Aluminum foil,consumable,209-SHELF-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00392,Plastic wrap,consumable,209-SHELF-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00393,Conical centrifuge tubes,equipment,209-SHELF-S04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00394,Glass tubes,consumable,209-SHELF-S04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00395,Cylindrical sponges,other,209-SHELF-S04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00396,1.7 mL syringes,consumable,209-SHELF-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00397,10 mL syringes,consumable,209-SHELF-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00398,20 mL syringes,consumable,209-SHELF-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00399,Tissue culture plates,consumable,209-SHELF-S05,3,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00400,100 x 15 mm petri dishes,glassware,209-SHELF-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00401,UV lightbulb,equipment,213-SHELF-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00402,Sterile filter units,consumable,213-SHELF-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00403,Storage containers (empty),consumable,213-SHELF-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,,In cardboard box +itm-00404,ZR-Duet DNA/RNA microprep,kit,213-SHELF-S01,,,,,,2020-12-01,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00405,ZR-Duet DNA/RNA microprep,kit,213-SHELF-S01,,,,,,2020-12-19,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00406,Extra zymo-spin columns,consumable,213-SHELF-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,,In ZR-Duet DNA/RNA microprep box at the back +itm-00407,Plastic containers with lids,consumable,213-SHELF-S01,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,,Resemble large to-go salad dressing containers +itm-00408,DNeasy Powersoil kit (50),kit,213-SHELF-S02,,,,,,2019-01-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00409,EpiTect Bisulfite Kit (48),kit,213-SHELF-S02,,,,,,2014-05-15,,unverified,2021-08-29,,,legacy-xlsx,,"MinElute DNA spin columns, Buffer BD, and DNA Protect Buffer at 2-8°C" +itm-00410,DNeasy blood & tissue kit (50),kit,213-SHELF-S02,,,,,,2016-08-15,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00411,EZ DNA methylation-Gold kit (50),kit,213-SHELF-S02,,,,,,2010-08-15,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00412,MehylFlash global DNA methylation ELISA Easy Kit,kit,213-SHELF-S02,,,,,,2018-09-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00413,MehylFlash global DNA methylation ELISA Easy Kit,kit,213-SHELF-S02,,,,,,2019-02-05,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00414,EpiQuik MeDIP Ultra kit,kit,213-SHELF-S02,,,,,,2019-01-25,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00415,MethylFlash methylated DNA quantification kit,kit,213-SHELF-S02,,,,,,2014-11-04,,unverified,2021-08-29,,,legacy-xlsx,,"Developer and 10x Wash Buffer @ 4˚C; Control DNA, Antibody, and Enhancer @ -20˚C" +itm-00416,Methylamp DNA modification kit,kit,213-SHELF-S02,,,,,,2015-01-16,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00417,Methylamp DNA modification kit,kit,213-SHELF-S02,,,,,,2014-11-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00418,DNA Chips,consumable,213-SHELF-S02,,,,,,2015-10-28,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00419,High Sensitivity DNA Chips,consumable,213-SHELF-S02,,,,,,2020-11-14,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00420,RNA Pico chips,consumable,213-SHELF-S02,,,,,,2018-01-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00421,RNA Pico chips,consumable,213-SHELF-S02,,,,,,2019-11-01,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00422,Kit instructions,kit,213-SHELF-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00423,Mini compostion (partly filled out),other,213-SHELF-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00424,Syringes and safe-lock tubes,consumable,213-SHELF-S02,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00425,RNeasy Plus Micro kit (50),kit,213-SHELF-S03,,,,,,2019-03-22,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00426,Direct-zol RNA MiniPrep Kit,kit,213-SHELF-S03,,,,,,2014-04-15,,unverified,2021-08-29,,,legacy-xlsx,,DNAse 1 @ -20˚C +itm-00427,Urine Collection Kit,kit,213-SHELF-S03,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00428,PAXgene tissue DNA kit (50),kit,213-SHELF-S03,,,,,,2018-07-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00429,PAXgene tissue RNA kit (50),kit,213-SHELF-S03,,,,,,2017-06-29,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00430,BCA Protein kit,kit,213-SHELF-S03,,,,,,2016-11-16,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00431,Qproteome FFPE tissue kit (20),kit,213-SHELF-S03,2,,,,,2015-01-17,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00432,QIAamp DNA stool mini kit (50),kit,213-SHELF-S04,,,,,,2008-01-23,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00433,DNeasy 96 blood & tissue kit (4),kit,213-SHELF-S04,,,,,,2014-10-09,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00434,ZR Fecal DNA MiniPrep kit,kit,213-SHELF-S04,,,,,,2014-05-05,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00435,QIAquick PCR purification kit (50),kit,213-SHELF-S04,,,,,,2015-08-11,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00436,Pico methyl-seq library prep kit,kit,213-SHELF-S04,,,,,,2019-05-10,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00437,QIAprep spin miniprep kit (50),kit,213-SHELF-S04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,,n.d. +itm-00438,DNeasy blood & tissue kit (50),kit,213-SHELF-S04,,,,,,2018-04-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00439,Extra Omega EZNA supplies,other,213-SHELF-S04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,,(1) HiBind DNA spin columns (2) Collection tubes +itm-00440,RNeasy Plus Micro kit (50),kit,213-SHELF-S05,,,,,,2018-08-06,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00441,RNeasy Plus Micro kit (50),kit,213-SHELF-S05,,,,,,2018-11-26,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00442,RNeasy Plus Mini kit (50),kit,213-SHELF-S05,,,,,,2013-12-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00443,PAXgene tissue DNA kit (50),kit,213-SHELF-S05,,,,,,2018-02-09,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00444,QIAquick nucleotide removal kit (250),kit,213-SHELF-S05,,,,,,2013-09-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00445,RNeasy Plus Micro kit (50),kit,213-SHELF-S05,,,,,,2019-03-22,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00446,RNeasy Plus Micro kit (50),kit,213-SHELF-S05,,,,,,2018-08-13,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00447,RNeasy Plus Micro kit (50),kit,213-SHELF-S05,,,,,,2019-08-22,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00448,RNeasy Plus Micro kit (50),kit,213-SHELF-S05,,,,,,2019-03-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00449,QIAshredder (50),other,213-SHELF-S05,,,,,,2018-08-13,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00450,QIAshredder (250),other,213-SHELF-S05,,,,,,2019-03-22,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00451,M-MLV Reverse Transcriptase,enzyme,213-F20-S02-D08,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00452,Oligo(dT)15 Primer,reagent,213-F20-S02-D08,2,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00453,dNTP mix,reagent,213-F20-S02-D08,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00454,Actin+ CYP1A,other,213-F20-S02-D08,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00455,Samples in snap cap tubes and cardboard boxes,sample,213-F20-S02-D10,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00456,Oxford Nanopore flow cell priming kit,kit,213-F20-S03-D09,,,,,,2019-11-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00457,Oxford Nanopore flow cell wash kit,kit,213-F20-S03-D09,,,,,,2019-11-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00458,Oxford Nanopore rapid sequencing kit,kit,213-F20-S03-D09,,,,,,2019-11-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00459,Oxford Nanopore control expansion,other,213-F20-S03-D09,,,,,,2019-11-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00460,DNAse 1 H2O in 6 uL alignots from Zymo quick prep,enzyme,213-F20-S03-D09,,,,,,2020-01-27,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00461,EpiNext Post Bisulfite DNA library prep kit part 2,kit,213-F20-S03-D09,,,,,,2015-01-16,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00462,EpiNext Post Bisulfite DNA library prep kit part 2,kit,213-F20-S03-D09,,,,,,2014-11-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00463,EpiNext Post Bisulfite DNA library prep kit part 1,kit,213-F20-S03-D09,,,,,,2014-11-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00464,EpiNext NGS barcode index part 1,other,213-F20-S03-D09,,,,,,2014-11-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00465,EpiNext NGS barcode index part 2,other,213-F20-S03-D09,,,,,,2014-11-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00466,"unknown labeled ""part 2 of 2""",other,213-F20-S03-D09,,,,,,2014-11-04,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00467,perishable components part 2,other,213-F20-S03-D09,,,,,,2009-09-02,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00468,Thermoscientific GeneRuler High Range DNA ladder,reagent,213-F20-S03-D04,,,,,,2018-11-20,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00469,PCR supplies,other,213-F20-S03-D04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00470,Reverse transcription supplies,other,213-F20-S03-D04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00471,Deoxyribonuclease 1,enzyme,213-F20-S03-D04,,,,,,2019-07-02,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00472,Native Bacillus Ilcheniformis Protease (opened 7/27/2021),other,213-F20-S03-D04,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00473,LumiProbe daGreen for real-time PCR,other,213-F20-S03-D04,,,,,,2019-08-30,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00474,Liberase TM research grade,other,213-F20-S03-D04,,,,,,2019-07-02,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00475,MethylMiner Methylated DNA enrichment kit,kit,213-F20-S03-D04,4,,,,,2020-12-17,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00476,SYBR Nucleic acid gel stain,reagent,213-F20-S03-D04,,,,,,2009-09-28,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00477,Penicillin-Streptomycin-amphotericin_B (Fungizone) 100mL x 2,other,213-F20-S04-D05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00478,Penicillin-Stretomycin solution (100mL),reagent,213-F20-S04-D05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00479,OmniPur X-GAL,other,213-F20-S04-D05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00480,Pepstatin A. from microbial source (5mg/mL),other,213-F20-S04-D05,,,,,,2007-08-09,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00481,"Lysosome, from chicken egg white",other,213-F20-S04-D05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00482,X-propto-GAL (4 mg/mL),other,213-F20-S04-D05,,,,,,2008-11-11,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00483,"Aprotinin, from bovine lung (10 mg/mL)",other,213-F20-S04-D05,,,,,,2007-08-09,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00484,50 mM MgCl,other,213-F20-S04-D05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00485,platinum qPCR supermix-UDG,other,213-F20-S04-D05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00486,Leupeptin hemisulfate salt 10.5 mM,reagent,213-F20-S04-D05,,,,,,2007-07-14,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00487,Native Bacillus Ilcheniformis Protease,other,213-F20-S04-D05,,,,,,2020-01-09,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00488,"Nitrogen-flushed 5-Azacytidine, 99% (opened 4/12/2010)",other,213-F20-S04-D05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00489,"5-Azacytidine, >= 98%",other,213-F20-S04-D05,,,,,,2010-06-07,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00490,Poly-D-lysine hydrobromide,other,213-F20-S04-D05,,,,,,2008-12-11,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00491,Mac gDNA *crab*,sample,213-F20-S04-D06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00492,2 tubes of samples,sample,213-F20-S04-D06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00493,"small jar labeled ""sample collected in May 2000""",sample,213-F20-S04-D06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00494,MethylMiner Methylated DNA enrichment kit *CRAB ONLY* (opened 10/28/2020),kit,213-F20-S04-D06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00495,CRC 471 gray whale blubber/skin,other,213-F20-S04-D06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00496,small bag with several small bivalve samples,sample,213-F20-S04-D06,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00497,2-Mercaptoethanol (opened,reagent,213-F20-S04-D07,,,,,,2018-08-06,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00498,"Misc. samples (crab, geoduck, etc.)",sample,213-F20-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, +itm-00499,SPUD IPC stock reagants,reagent,213-F20-S05,,,,,,,,unverified,2021-08-29,,,legacy-xlsx,, diff --git a/data/locations.csv b/data/locations.csv new file mode 100644 index 0000000..4f88df3 --- /dev/null +++ b/data/locations.csv @@ -0,0 +1,174 @@ +location_id,room,kind,number,parent_id,label,notes +209-CAB-01,209,cabinet,1,,Cabinet 1, +209-CAB-02,209,cabinet,2,,Cabinet 2, +209-CAB-09,209,cabinet,9,,Cabinet 9, +209-CAB-10,209,cabinet,10,,Cabinet 10, +209-CAB-11,209,cabinet,11,,Cabinet 11, +209-CAB-12,209,cabinet,12,,Cabinet 12, +209-CAB-13,209,cabinet,13,,Cabinet 13, +209-CAB-14,209,cabinet,14,,Cabinet 14,recorded empty as of 2021-08-29 +209-CAB-15,209,cabinet,15,,Cabinet 15, +209-CAB-16,209,cabinet,16,,Cabinet 16, +209-CAB-25,209,cabinet,25,,Cabinet 25,no contents recorded in legacy spreadsheet +209-CAB-26,209,cabinet,26,,Cabinet 26,no contents recorded in legacy spreadsheet +209-CAB-27,209,cabinet,27,,Cabinet 27, +209-CAB-38,209,cabinet,38,,Cabinet 38, +209-CAB-39,209,cabinet,39,,Cabinet 39, +209-CAB-48,209,cabinet,48,,Cabinet 48, +209-CAB-49,209,cabinet,49,,Cabinet 49, +209-CAB-50,209,cabinet,50,,Cabinet 50, +209-CAB-51,209,cabinet,51,,Cabinet 51, +209-CAB-52,209,cabinet,52,,Cabinet 52, +209-CAB-53,209,cabinet,53,,Cabinet 53, +209-CAB-54,209,cabinet,54,,Cabinet 54, +209-CAB-55,209,cabinet,55,,Cabinet 55, +209-DRW-03,209,drawer,3,,Drawer 3, +209-DRW-04,209,drawer,4,,Drawer 4, +209-DRW-05,209,drawer,5,,Drawer 5, +209-DRW-06,209,drawer,6,,Drawer 6, +209-DRW-07,209,drawer,7,,Drawer 7, +209-DRW-08,209,drawer,8,,Drawer 8, +209-DRW-17,209,drawer,17,,Drawer 17, +209-DRW-18,209,drawer,18,,Drawer 18, +209-DRW-19,209,drawer,19,,Drawer 19, +209-DRW-20,209,drawer,20,,Drawer 20, +209-DRW-21,209,drawer,21,,Drawer 21, +209-DRW-22,209,drawer,22,,Drawer 22,recorded empty as of 2021-08-29 +209-DRW-28,209,drawer,28,,Drawer 28, +209-DRW-29,209,drawer,29,,Drawer 29,recorded empty as of 2021-08-29 +209-DRW-30,209,drawer,30,,Drawer 30,recorded empty as of 2021-08-29 +209-DRW-31,209,drawer,31,,Drawer 31, +209-DRW-32,209,drawer,32,,Drawer 32,recorded empty as of 2021-08-29 +209-DRW-33,209,drawer,33,,Drawer 33, +209-DRW-34,209,drawer,34,,Drawer 34, +209-DRW-35,209,drawer,35,,Drawer 35, +209-DRW-36,209,drawer,36,,Drawer 36,recorded empty as of 2021-08-29 +209-DRW-37,209,drawer,37,,Drawer 37, +209-DRW-40,209,drawer,40,,Drawer 40, +209-DRW-41,209,drawer,41,,Drawer 41, +209-DRW-42,209,drawer,42,,Drawer 42, +209-DRW-43,209,drawer,43,,Drawer 43, +209-DRW-44,209,drawer,44,,Drawer 44, +209-DRW-45,209,drawer,45,,Drawer 45, +209-DRW-46,209,drawer,46,,Drawer 46, +209-DRW-47,209,drawer,47,,Drawer 47, +209-FRIDGE,209,refrigerator,,,Refrigerator Rm 209, +209-FRIDGE-DOOR-SHELF-1,209,shelf,Door Shelf 1,209-FRIDGE,Refrigerator Rm 209 shelf Door Shelf 1, +209-FRIDGE-DOOR-SHELF-2,209,shelf,Door Shelf 2,209-FRIDGE,Refrigerator Rm 209 shelf Door Shelf 2, +209-FRIDGE-DOOR-SHELF-3,209,shelf,Door Shelf 3,209-FRIDGE,Refrigerator Rm 209 shelf Door Shelf 3, +209-FRIDGE-DOOR-SHELF-4,209,shelf,Door Shelf 4,209-FRIDGE,Refrigerator Rm 209 shelf Door Shelf 4, +209-FRIDGE-DOOR-SHELF-5,209,shelf,Door Shelf 5,209-FRIDGE,Refrigerator Rm 209 shelf Door Shelf 5, +209-FRIDGE-S01,209,shelf,1,209-FRIDGE,Refrigerator Rm 209 shelf 1, +209-FRIDGE-S02,209,shelf,2,209-FRIDGE,Refrigerator Rm 209 shelf 2, +209-FRIDGE-S03,209,shelf,3,209-FRIDGE,Refrigerator Rm 209 shelf 3, +209-FRIDGE-S04,209,shelf,4,209-FRIDGE,Refrigerator Rm 209 shelf 4, +209-FRIDGE-S05,209,shelf,5,209-FRIDGE,Refrigerator Rm 209 shelf 5, +209-SHELF,209,shelf,,,Shelf unit Rm 209, +209-SHELF-S01,209,shelf,1,209-SHELF,Shelf unit Rm 209 shelf 1, +209-SHELF-S02,209,shelf,2,209-SHELF,Shelf unit Rm 209 shelf 2, +209-SHELF-S03,209,shelf,3,209-SHELF,Shelf unit Rm 209 shelf 3, +209-SHELF-S04,209,shelf,4,209-SHELF,Shelf unit Rm 209 shelf 4, +209-SHELF-S05,209,shelf,5,209-SHELF,Shelf unit Rm 209 shelf 5, +213-BIN-X01,213,bin,Storage 1,,Storage 1, +213-BIN-X02,213,bin,Storage 2,,Storage 2, +213-BIN-X03,213,bin,Storage 3,,Storage 3, +213-BIN-X04,213,bin,Storage 4,,Storage 4, +213-BIN-X05,213,bin,Storage 5,,Storage 5, +213-BIN-X06,213,bin,Pathobiology tote #1,,Pathobiology tote #1, +213-BIN-X07,213,bin,Pathobiology tote #2,,Pathobiology tote #2, +213-BIN-X08,213,bin,Pathobiology tote #3,,Pathobiology tote #3, +213-BIN-X09,213,bin,Unlabed light blue bin,,Unlabed light blue bin, +213-BIN-X10,213,bin,Long clear bin on top of VWR fridge (1),,Long clear bin on top of VWR fridge (1), +213-BIN-X11,213,bin,Long clear bin on top of VWR fridge (2),,Long clear bin on top of VWR fridge (2), +213-BIN-X12,213,bin,Long clear bin on top of VWR fridge (3),,Long clear bin on top of VWR fridge (3), +213-CAB-01,213,cabinet,1,,Cabinet 1, +213-CAB-02,213,cabinet,2,,Cabinet 2, +213-CAB-32,213,cabinet,32,,Cabinet 32, +213-CAB-33,213,cabinet,33,,Cabinet 33, +213-CAB-34,213,cabinet,34,,Cabinet 34,recorded empty as of 2021-08-29 +213-CAB-35,213,cabinet,35,,Cabinet 35, +213-CAB-44,213,cabinet,44,,Cabinet 44, +213-CAB-45,213,cabinet,45,,Cabinet 45, +213-CAB-X01,213,cabinet,Glass cabinets next to metal lab counter and sink,,Glass cabinets next to metal lab counter and sink, +213-DRW-03,213,drawer,3,,Drawer 3,recorded empty as of 2021-08-29 +213-DRW-04,213,drawer,4,,Drawer 4,recorded empty as of 2021-08-29 +213-DRW-05,213,drawer,5,,Drawer 5,recorded empty as of 2021-08-29 +213-DRW-06,213,drawer,6,,Drawer 6,recorded empty as of 2021-08-29 +213-DRW-07,213,drawer,7,,Drawer 7,no contents recorded in legacy spreadsheet +213-DRW-08,213,drawer,8,,Drawer 8,no contents recorded in legacy spreadsheet +213-DRW-09,213,drawer,9,,Drawer 9,no contents recorded in legacy spreadsheet +213-DRW-10,213,drawer,10,,Drawer 10,no contents recorded in legacy spreadsheet +213-DRW-11,213,drawer,11,,Drawer 11,no contents recorded in legacy spreadsheet +213-DRW-12,213,drawer,12,,Drawer 12,no contents recorded in legacy spreadsheet +213-DRW-13,213,drawer,13,,Drawer 13,no contents recorded in legacy spreadsheet +213-DRW-14,213,drawer,14,,Drawer 14,no contents recorded in legacy spreadsheet +213-DRW-15,213,drawer,15,,Drawer 15,recorded empty as of 2021-08-29 +213-DRW-16,213,drawer,16,,Drawer 16, +213-DRW-17,213,drawer,17,,Drawer 17, +213-DRW-18,213,drawer,18,,Drawer 18,recorded empty as of 2021-08-29 +213-DRW-19,213,drawer,19,,Drawer 19,no contents recorded in legacy spreadsheet +213-DRW-20,213,drawer,20,,Drawer 20,recorded empty as of 2021-08-29 +213-DRW-21,213,drawer,21,,Drawer 21,recorded empty as of 2021-08-29 +213-DRW-22,213,drawer,22,,Drawer 22,recorded empty as of 2021-08-29 +213-DRW-23,213,drawer,23,,Drawer 23,recorded empty as of 2021-08-29 +213-DRW-24,213,drawer,24,,Drawer 24,recorded empty as of 2021-08-29 +213-DRW-25,213,drawer,25,,Drawer 25,recorded empty as of 2021-08-29 +213-DRW-26,213,drawer,26,,Drawer 26,recorded empty as of 2021-08-29 +213-DRW-27,213,drawer,27,,Drawer 27, +213-DRW-28,213,drawer,28,,Drawer 28, +213-DRW-29,213,drawer,29,,Drawer 29, +213-DRW-30,213,drawer,30,,Drawer 30,recorded empty as of 2021-08-29 +213-DRW-31,213,drawer,31,,Drawer 31,recorded empty as of 2021-08-29 +213-DRW-36,213,drawer,36,,Drawer 36, +213-DRW-37,213,drawer,37,,Drawer 37, +213-DRW-38,213,drawer,38,,Drawer 38,recorded empty as of 2021-08-29 +213-DRW-39,213,drawer,39,,Drawer 39,recorded empty as of 2021-08-29 +213-DRW-40,213,drawer,40,,Drawer 40, +213-DRW-41,213,drawer,41,,Drawer 41,recorded empty as of 2021-08-29 +213-DRW-42,213,drawer,42,,Drawer 42, +213-DRW-43,213,drawer,43,,Drawer 43, +213-DRW-46,213,drawer,46,,Drawer 46, +213-DRW-47,213,drawer,47,,Drawer 47, +213-DRW-48,213,drawer,48,,Drawer 48, +213-DRW-49,213,drawer,49,,Drawer 49, +213-DRW-50,213,drawer,50,,Drawer 50, +213-DRW-51,213,drawer,51,,Drawer 51, +213-DRW-52,213,drawer,52,,Drawer 52, +213-DRW-53,213,drawer,53,,Drawer 53, +213-DRW-54,213,drawer,54,,Drawer 54, +213-DRW-55,213,drawer,55,,Drawer 55, +213-DRW-56,213,drawer,56,,Drawer 56, +213-DRW-57,213,drawer,57,,Drawer 57, +213-DRW-58,213,drawer,58,,Drawer 58, +213-DRW-59,213,drawer,59,,Drawer 59, +213-DRW-60,213,drawer,60,,Drawer 60, +213-DRW-61,213,drawer,61,,Drawer 61, +213-F20,213,freezer,,,-20C freezer Rm 213, +213-F20-S01,213,shelf,1,213-F20,-20C freezer Rm 213 shelf 1,recorded empty as of 2021-08-29 +213-F20-S02,213,shelf,2,213-F20,-20C freezer Rm 213 shelf 2, +213-F20-S02-D08,213,drawer,8,213-F20-S02,-20C freezer Rm 213 shelf 2 drawer 8, +213-F20-S02-D10,213,drawer,10,213-F20-S02,-20C freezer Rm 213 shelf 2 drawer 10, +213-F20-S03,213,shelf,3,213-F20,-20C freezer Rm 213 shelf 3, +213-F20-S03-D04,213,drawer,4,213-F20-S03,-20C freezer Rm 213 shelf 3 drawer 4, +213-F20-S03-D09,213,drawer,9,213-F20-S03,-20C freezer Rm 213 shelf 3 drawer 9, +213-F20-S04,213,shelf,4,213-F20,-20C freezer Rm 213 shelf 4, +213-F20-S04-D05,213,drawer,5,213-F20-S04,-20C freezer Rm 213 shelf 4 drawer 5, +213-F20-S04-D06,213,drawer,6,213-F20-S04,-20C freezer Rm 213 shelf 4 drawer 6, +213-F20-S04-D07,213,drawer,7,213-F20-S04,-20C freezer Rm 213 shelf 4 drawer 7, +213-F20-S05,213,shelf,5,213-F20,-20C freezer Rm 213 shelf 5, +213-OTH-X01,213,other,NA,,Floor next to the shelves to the left of entryway 1, +213-OTH-X02,213,other,NA,,Floor under lab bench next to metal sink 2, +213-SH-S01,213,shelf,top,,Black shelving unit top,recorded empty as of 2021-08-29 +213-SH-S02,213,shelf,2nd,,Black shelving unit 2nd, +213-SH-S03,213,shelf,3rd,,Black shelving unit 3rd, +213-SH-S99,213,shelf,bottom,,Black shelving unit bottom,recorded empty as of 2021-08-29 +213-SHELF,213,shelf,,,Shelves left of entryway Rm 213, +213-SHELF-S01,213,shelf,1,213-SHELF,Shelves left of entryway Rm 213 shelf 1, +213-SHELF-S02,213,shelf,2,213-SHELF,Shelves left of entryway Rm 213 shelf 2, +213-SHELF-S03,213,shelf,3,213-SHELF,Shelves left of entryway Rm 213 shelf 3, +213-SHELF-S04,213,shelf,4,213-SHELF,Shelves left of entryway Rm 213 shelf 4, +213-SHELF-S05,213,shelf,5,213-SHELF,Shelves left of entryway Rm 213 shelf 5, +230-SH-X01,230,shelf,White shelf on wall,,White shelf on wall, +230-SH-X02,230,shelf,Small brown bookshelf,,Small brown bookshelf, +M80-SH-X01,M80,shelf,NA,,Shelves 1, +M80-SH-X02,M80,shelf,NA,,Shelves 2, diff --git a/data/review_queue.csv b/data/review_queue.csv new file mode 100644 index 0000000..46a1276 --- /dev/null +++ b/data/review_queue.csv @@ -0,0 +1,17 @@ +reason,sheet,row,location_id,raw_text,suggestion,notes +comma-delimited-contents,DrawersCabinets updated 82921,4,209-DRW-03,"Qubit RNA and DNA buffer, qubit assay tubes, DNA/RNA dye",Qubit RNA and DNA buffer | qubit assay tubes | DNA/RNA dye,kept as one item; split by hand if these are distinct +comma-delimited-contents,DrawersCabinets updated 82921,83,213-DRW-27,"eDNA, gDNA, & cDNA samples, Primer stocks, misc. samples",eDNA | gDNA | & cDNA samples | Primer stocks | misc. samples,kept as one item; split by hand if these are distinct +coarse-duplicate-of-dedicated-sheet,DrawersCabinets updated 82921,118,213-SHELF,Carboard box containing dozens of cardboard sample boxes; germicidal lamp (UV lightbulb); sterile filter unit (x2); ZR-Duet DNA/RNA microprep (x3),Carboard box containing dozens of cardboard sample boxes | germicidal lamp (UV lightbulb) | sterile filter unit (x2) | ZR-Duet DNA/RNA microprep (x3),shelf 'Top'; reconcile against 213-SHELF-* rows before adding +coarse-duplicate-of-dedicated-sheet,DrawersCabinets updated 82921,119,213-SHELF,DNeasy Powersoil kit (50); EZ DNA methylation-gold kit; EpiTect Bisulfite kit; DNeasy blood and tissue kit; kit instructions; half-used mini composition book; EpiQuik meDIP ultra kit; MethylFlash global DNA methylation kit (x2); MethylFlash methylated DNA quantification kit; Methylamp DNA modification kit; RNA pico chips (2 boxes); high sensitivity DNA chips; DNA chips; syringes and tubes,DNeasy Powersoil kit (50) | EZ DNA methylation-gold kit | EpiTect Bisulfite kit | DNeasy blood and tissue kit | kit instructions | half-used mini composition book | EpiQuik meDIP ultra kit | MethylFlash global DNA methylation kit (x2) | MethylFlash methylated DNA quantification kit | Methylamp DNA modification kit | RNA pico chips (2 boxes) | high sensitivity DNA chips | DNA chips | syringes and tubes,shelf '2nd'; reconcile against 213-SHELF-* rows before adding +coarse-duplicate-of-dedicated-sheet,DrawersCabinets updated 82921,120,213-SHELF,Direct-zol RNA MiniPrep Kit; urine collection kit; RNeasy plus micro kit; PAXgene tissue DNA kit; PAXgene tissue RNA kit; BCA protein kit; Qproteome FFPE tissue kit (x2),Direct-zol RNA MiniPrep Kit | urine collection kit | RNeasy plus micro kit | PAXgene tissue DNA kit | PAXgene tissue RNA kit | BCA protein kit | Qproteome FFPE tissue kit (x2),shelf '3rd'; reconcile against 213-SHELF-* rows before adding +coarse-duplicate-of-dedicated-sheet,DrawersCabinets updated 82921,121,213-SHELF,QIAamp DNA stool mini kit; DNeasy 96 blood and tissue kit (4) & (50); ZR fecal DNA MiniPrep kit; QIAquick PCR purification kit; pico methyl-seq library prep kit; QIAprep spin miniprep kit; extra Omega EZNA supplies,QIAamp DNA stool mini kit | DNeasy 96 blood and tissue kit (4) & (50) | ZR fecal DNA MiniPrep kit | QIAquick PCR purification kit | pico methyl-seq library prep kit | QIAprep spin miniprep kit | extra Omega EZNA supplies,shelf '4th'; reconcile against 213-SHELF-* rows before adding +coarse-duplicate-of-dedicated-sheet,DrawersCabinets updated 82921,122,213-SHELF,RNeasy plus micro kit (x6); RNeasy plus mini kit; PAXgene tissue DNA kit; QIAquick nucleotide removal kit; QIA shredder (50) & (250),RNeasy plus micro kit (x6) | RNeasy plus mini kit | PAXgene tissue DNA kit | QIAquick nucleotide removal kit | QIA shredder (50) & (250),shelf 'Bottom'; reconcile against 213-SHELF-* rows before adding +comma-delimited-contents,DrawersCabinets updated 82921,144,230-SH-X01,"Misc. cables, computer mice, web cam, CDR, DVD, speakers, Mac Mini, external drives, backup HDD for OWL, smart switch, heat lamp",Misc. cables | computer mice | web cam | CDR | DVD | speakers | Mac Mini | external drives | backup HDD for OWL | smart switch | heat lamp,kept as one item; split by hand if these are distinct +comma-delimited-contents,DrawersCabinets updated 82921,145,230-SH-X02,"Assorted teas, tupperware, reusable shopping bags, extension cords",Assorted teas | tupperware | reusable shopping bags | extension cords,kept as one item; split by hand if these are distinct +comma-delimited-contents,Refrigerator Rm 209,28,209-FRIDGE-S02,"Scallop Stages cDNA Libraries (Set A, Set B, Met A, Met B)",Scallop Stages cDNA Libraries (Set A | Set B | Met A | Met B),kept as one item; split by hand if these are distinct +comma-delimited-contents,Refrigerator Rm 209,105,209-FRIDGE-S04,"tubes in purple tube rack (labeled DH, HT, MG1, MG2, TF)",tubes in purple tube rack (labeled DH | HT | MG1 | MG2 | TF),kept as one item; split by hand if these are distinct +comma-delimited-contents,Refrigerator Rm 209,115,209-FRIDGE-S05,"TRI Reagant in foil-wrapped tube, PicoBlue immunoscreening kit, LumiGLO Chemiluminescent Substrate A, LumiGLO Chemiluminescent Substrate B, ImmunoPure Immobilized Protein A (x2), Electrophoresis Purity Reagant, horseradish peroxidase antibodies from donkey, Chitin beads, SC Spin Columns 2/pack, Piperazine Diacrylamide, ProBond Resin (12 mL)",TRI Reagant in foil-wrapped tube | PicoBlue immunoscreening kit | LumiGLO Chemiluminescent Substrate A | LumiGLO Chemiluminescent Substrate B | ImmunoPure Immobilized Protein A (x2) | Electrophoresis Purity Reagant | horseradish peroxidase antibodies from donkey | Chitin beads | SC Spin Columns 2/pack | Piperazine Diacrylamide | ProBond Resin (12 mL),kept as one item; split by hand if these are distinct +comma-delimited-contents,Refrigerator Rm 209,132,209-FRIDGE-DOOR-SHELF-2,"NuPage 10 mL bottles (LDS sample buffer, Sample reducing agent, TBE-Urea sample buffer, antioxidant)",NuPage 10 mL bottles (LDS sample buffer | Sample reducing agent | TBE-Urea sample buffer | antioxidant),kept as one item; split by hand if these are distinct +comma-delimited-contents,Refrigerator Rm 209,140,209-FRIDGE-DOOR-SHELF-3,"Protein - Antibodies, ladder, reducing agent, samples",Protein - Antibodies | ladder | reducing agent | samples,kept as one item; split by hand if these are distinct +comma-delimited-contents,Refrigerator Rm 209,141,209-FRIDGE-DOOR-SHELF-3,"Gel loading supplies (ladders, loading dyes, etc.)",Gel loading supplies (ladders | loading dyes | etc.),kept as one item; split by hand if these are distinct +comma-delimited-contents,Shelf Rm 209 updated 9521,6,209-SHELF-S03,"Microcentrifuge tubes, conical screw top tubes, screw caps, assorted microtubes",Microcentrifuge tubes | conical screw top tubes | screw caps | assorted microtubes,kept as one item; split by hand if these are distinct diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..289437c --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +openpyxl>=3.1 diff --git a/scripts/migrate_legacy.py b/scripts/migrate_legacy.py new file mode 100644 index 0000000..93a86be --- /dev/null +++ b/scripts/migrate_legacy.py @@ -0,0 +1,565 @@ +#!/usr/bin/env python3 +"""One-time migration: legacy/Roberts Lab Inventory.xlsx -> data/*.csv + +Emits three files: + data/locations.csv controlled vocabulary of physical places + data/items.csv one row per item, canonical inventory + data/review_queue.csv rows a human needs to look at before they become items + +Design rule: never guess silently. Where the legacy data is ambiguous +(comma-delimited contents, suspected duplicates, unparseable dates) the row +goes to review_queue.csv with a stated reason instead of into items.csv. + +Usage: + python3 scripts/migrate_legacy.py [--xlsx PATH] [--out DIR] +""" + +from __future__ import annotations + +import argparse +import csv +import datetime as dt +import re +import sys +from collections import Counter +from pathlib import Path + +try: + import openpyxl +except ImportError: + sys.exit("openpyxl required: pip install -r requirements.txt") + +# The spreadsheet's own claim about when it was last updated (sheet names say +# 82921 / 9521 -> Aug 29 2021 / Sep 5 2021). Every migrated item inherits this +# as last_verified so the staleness is queryable rather than a footnote. +LEGACY_VERIFIED = "2021-08-29" +LEGACY_SOURCE = "legacy-xlsx" + +ITEM_FIELDS = [ + "item_id", "name", "category", "location_id", "quantity", "unit", + "vendor", "catalog_no", "lot", "received", "expires", "status", + "last_verified", "verified_by", "owner", "source", "photo_id", "notes", +] +LOCATION_FIELDS = [ + "location_id", "room", "kind", "number", "parent_id", "label", "notes", +] +REVIEW_FIELDS = [ + "reason", "sheet", "row", "location_id", "raw_text", "suggestion", "notes", +] + +# Rooms get short codes so location_ids stay ID-safe. "-80˚C room" would make +# a hostile identifier. +ROOM_CODES = { + "209": "209", + "213": "213", + "230": "230", + "-80˚c room": "M80", +} + +KIND_MAP = { + "cabinet": "cabinet", + "cabinets": "cabinet", + "drawer": "drawer", + "shelf": "shelf", + "shelves": "shelf", + "refrigerator": "refrigerator", + "-20 freezer": "freezer", + "storage bin": "bin", + "black shelving unit": "shelf", +} + +# Short, fixed codes keep location_ids compact. These end up in QR payloads and +# URL fragments, so they must stay short and stable -- never derive them by +# truncating the kind name. +KIND_CODES = { + "cabinet": "CAB", "drawer": "DRW", "shelf": "SH", "bin": "BIN", + "refrigerator": "FRIDGE", "freezer": "FRZ", "bench": "BNCH", + "floor": "FLR", "other": "OTH", +} + +# Ordinal shelf labels used inconsistently across sheets ("Top", "2nd", ...). +ORDINALS = { + "top": 1, "1st": 1, "2nd": 2, "3rd": 3, "4th": 4, "5th": 5, + "6th": 6, "bottom": 99, +} + +# Order matters: first match wins. Tuned against the legacy contents so that +# the site's category filter is actually useful; ~1 in 6 items still lands in +# `other`, which is honest for a drawer containing a Nintendo. +CATEGORY_RULES = [ + ("kit", r"\bkits?\b|mini ?prep|midi ?prep|maxi ?prep|micro ?prep|\bduet\b"), + ("enzyme", r"polymerase|ligase|transcriptase|nuclease|dnase|rnase|proteinase|" + r"restriction enzyme|\btaq\b|exonuclease|phosphatase|lysozyme|" + r"\benzymes?\b"), + ("antibody", r"antibod|antisera|\bigg\b|\bighm\b|\bserum\b|immunoglobulin"), + ("media", r"\bagar\b|\bbroth\b|\bmedia\b|\bmedium\b|\bfbs\b|\bsera\b|" + r"\bfood\b|algae paste"), + ("sample", r"\bsamples?\b|\bgdna\b|extractions?|\baliquots?\b|libraries|" + r"snap cap|histology slides|\bcdna\b|\brna\b(?!ase)\s+from|" + r"\bblood\b|tissue in|in 100% ethanol|\bbcs plates?\b"), + ("glassware", r"\bbottles?\b|\bflasks?\b|\bbeakers?\b|graduated cylinders?|" + r"\bjars?\b|\bcuvettes?\b|cover ?slips?|\bpetri\b|" + r"weigh (boats?|paper)|\bpipette? bulbs?\b|\bglassware\b"), + ("equipment", r"centrifuge|thermocycler|shaker|manifold|incubator|" + r"spectrophotometer|\bbalance\b|\bpump\b|magnet|dynamag|" + r"nucleomag|\bfridge\b|freezer|\blamps?\b|light ?bulbs?|" + r"satellite tags?|mac mini|external drives?|\bhdd\b|" + r"web ?cam|keyboards?|computer (mouse|mice)|monitor|" + r"microscope|cellometers?|homogenizer|pestle|electrode|" + r"pipettors?|repeater pipets?|\bsonicator\b|hot ?plate|" + r"vortex|nanodrop|\bqubit\b(?! (rna|dna))"), + ("tool", r"forceps|tweezers|scissors|scalpel|wrench|screwdriver|pliers|" + r"\btools?\b|zip ties|\bclamps?\b|spatulas?|\bscoops?\b|" + r"\bbrush(es)?\b|paintbrush|\bsiphon\b|thermometer|\btimer\b|" + r"stir bars?|\bfunnels?\b|\bprobe\b|\bpaddles?\b|\bkeys?\b|" + r"switchblade|\bmesh\b|razor"), + ("consumable", r"\btips?\b|\btubes?\b|\bplates?\b|gloves|wipes|\bfoil\b|" + r"parafilm|syringes?|needles?|\bcolumns?\b|\bslides?\b|" + r"pipets?\b|pipettes?\b|dishes|\bboxes\b|\bbags?\b|" + r"\bracks?\b|\bfilters?\b|plastic wrap|\bcaps?\b|" + r"\bcontainers?\b|cassettes?|\bchips?\b|\bstrips?\b|" + r"benchtop protectors|\bswabs?\b|\bvials?\b|" + r"incubation chambers|\bsheaths?\b|\bcords?\b|batteries"), + ("reagent", r"buffers?|reagants?|reagents?|ethanol|methanol|isopropanol|" + r"\btris\b|\bedta\b|\bsds\b|\bacid\b|\bnaoh\b|\bhcl\b|" + r"\bstain\b|\bdyes?\b|\bladder\b|primers?|\bdntp|\bbsa\b|" + r"glycerol|agarose|acrylamide|\bresin\b|\bbeads?\b|trizol|" + r"tri reagant|substrate|\bsolutions?\b|\bassay\b|rnazol|" + r"\bcontrol dna\b|\bstandards?\b|\bwater\b|\bsalts?\b|" + r"formalin|formaldehyde|\bpbs\b|\bdmso\b"), + ("office", r"\bpens?\b|\bpencils?\b|sharpies?|\bstaples?\b|post-?its?|" + r"\bpaper\b|notebooks?|composition book|file folders?|" + r"white-?out|\bglue\b|paper clips?|\btacks?\b|stickers?|" + r"\btape\b|report covers?|clipboard|calculator|handbooks?|" + r"manuals?|business card|tough-?spots?|\bl?ab?els?\b|" + r"command strips|\bbinders?\b|\bnotepad\b|\bmarkers?\b"), +] + +# " - Goetz" / " -Goetz" trailing owner attribution used in the storage bins. +OWNER_RE = re.compile(r"\s+-\s*([A-Z][A-Za-z'-]+)\s*$") +# "(11/27/2019)" embedded date +PAREN_DATE_RE = re.compile(r"\((\d{1,2})/(\d{1,2})/(\d{2,4})\)") +# "(x2)" / "(X6)" multiplicity +XQTY_RE = re.compile(r"\(\s*[xX]\s*(\d+)\s*\)") +# "In cardboard box labeled "HOLD": a, b, c" container prefix +PREFIX_RE = re.compile(r'^([^;:]{4,70}):\s+(.*\S)$') + + +def slug(text: str) -> str: + """Turn an arbitrary label into an ID-safe fragment.""" + s = re.sub(r"[^A-Za-z0-9]+", "-", str(text).strip()).strip("-").upper() + return re.sub(r"-{2,}", "-", s) + + +def clean(value) -> str: + """Normalize a cell to a trimmed string. 209.0 -> '209', 7.0 -> '7'.""" + if value is None: + return "" + if isinstance(value, float) and value.is_integer(): + return str(int(value)) + if isinstance(value, dt.datetime): + return value.date().isoformat() + return str(value).strip() + + +def room_code(raw: str) -> str: + key = clean(raw).lower() + return ROOM_CODES.get(key, slug(key) or "UNK") + + +def norm_kind(raw: str) -> str: + key = clean(raw).lower().rstrip(" .") + return KIND_MAP.get(key, "other") + + +DATE_FORMATS = ("%Y-%m-%d", "%m/%d/%Y", "%m/%d/%y", "%m/%Y", "%Y-%m", "%Y") +PARTIAL_LEN = {"%m/%Y": 7, "%Y-%m": 7, "%Y": 4} + + +def parse_date(value, sheet: str, row: int, warnings: list) -> tuple[str, str]: + """Parse a legacy date cell. + + Returns (iso_date, residue). Partial dates keep their precision + ('08/2010' -> '2010-08') rather than being invented into a day. Text that + isn't a date at all ('Very likely all expired', 'n.d.') is returned as + residue for the caller to preserve in notes -- the legacy sheet used the + date column for commentary and that commentary is worth keeping. + """ + if value in (None, ""): + return "", "" + if isinstance(value, dt.datetime): + d, precision = value.date(), 10 + else: + text = clean(value) + # Leading date with trailing commentary: '08/2010 from FHL' + residue = "" + head = text + match = re.match(r"^([\d/.-]+)\s+(.*\S)$", text) + if match: + head, residue = match.group(1), match.group(2) + for fmt in DATE_FORMATS: + try: + d = dt.datetime.strptime(head, fmt).date() + precision = PARTIAL_LEN.get(fmt, 10) + break + except ValueError: + continue + else: + warnings.append((sheet, row, f"date column held non-date text: {text!r}" + " -> moved to notes")) + return "", text + if residue: + warnings.append((sheet, row, + f"kept date {head!r}, moved {residue!r} to notes")) + if not (dt.date(1990, 1, 1) <= d <= dt.date.today()): + warnings.append((sheet, row, f"implausible date: {d.isoformat()}")) + return d.isoformat()[:precision], residue + + if not (dt.date(1990, 1, 1) <= d <= dt.date.today()): + warnings.append((sheet, row, f"implausible date: {d.isoformat()}")) + return d.isoformat()[:precision], "" + + +def categorize(name: str) -> str: + low = name.lower() + for category, pattern in CATEGORY_RULES: + if re.search(pattern, low): + return category + return "other" + + +class Migration: + def __init__(self, xlsx: Path, outdir: Path): + self.wb = openpyxl.load_workbook(xlsx, data_only=True) + self.outdir = outdir + self.locations: dict[str, dict] = {} + self.items: list[dict] = [] + self.review: list[dict] = [] + self.warnings: list[tuple] = [] + self.skipped = 0 + self._next_id = 1 + + # -- registries --------------------------------------------------------- + + def location(self, location_id: str, room: str, kind: str, number: str = "", + parent_id: str = "", label: str = "", notes: str = "") -> str: + existing = self.locations.get(location_id) + if existing: + if notes and not existing["notes"]: + existing["notes"] = notes + return location_id + self.locations[location_id] = { + "location_id": location_id, "room": room, "kind": kind, + "number": number, "parent_id": parent_id, "label": label, + "notes": notes, + } + return location_id + + def add_item(self, name: str, location_id: str, **kw) -> None: + name = name.strip(" ;,.") + if not name: + return + row = {f: "" for f in ITEM_FIELDS} + row.update({ + "item_id": f"itm-{self._next_id:05d}", + "name": name, + "category": categorize(name), + "location_id": location_id, + "status": "unverified", + "last_verified": LEGACY_VERIFIED, + "source": LEGACY_SOURCE, + }) + row.update({k: v for k, v in kw.items() if v not in (None, "")}) + self._next_id += 1 + self.items.append(row) + + def flag(self, reason: str, sheet: str, row: int, location_id: str, + raw_text: str, suggestion: str = "", notes: str = "") -> None: + self.review.append({ + "reason": reason, "sheet": sheet, "row": row, + "location_id": location_id, "raw_text": raw_text, + "suggestion": suggestion, "notes": notes, + }) + + # -- content parsing ---------------------------------------------------- + + def explode(self, contents: str, location_id: str, sheet: str, rownum: int, + base_notes: str = "", received: str = "") -> None: + """Split a legacy 'Contents' cell into items. + + Semicolons are a reliable delimiter in this data and are split + automatically. Commas are not (`Microcentrifuge tubes, conical screw + top tubes` is two items; `DNeasy 96 blood and tissue kit (4) & (50)` is + one), so comma-only cells become a single item plus a review_queue + entry carrying the suggested split. + """ + text = contents.strip() + if not text: + return + + if text.lower().strip(" .") in ("empty", "none", "nothing"): + loc = self.locations.get(location_id) + if loc is not None and not loc["notes"]: + loc["notes"] = f"recorded empty as of {LEGACY_VERIFIED}" + return + + notes = base_notes + + # "In cardboard box labeled "HOLD": a, b, c" -> container becomes a note + prefix_match = PREFIX_RE.match(text) + if prefix_match and prefix_match.group(2).count(",") >= 2: + container, text = prefix_match.group(1).strip(), prefix_match.group(2) + notes = f"{notes}; {container}".strip("; ") if notes else container + + parts = [p.strip() for p in text.split(";") if p.strip()] + multi = len(parts) > 1 + + for part in parts: + if not multi and part.count(",") >= 2: + suggestion = " | ".join( + s.strip() for s in part.split(",") if s.strip()) + self.flag("comma-delimited-contents", sheet, rownum, + location_id, part, suggestion, + "kept as one item; split by hand if these are distinct") + + item_notes = notes + item_received = received + name = part + + date_match = PAREN_DATE_RE.search(name) + if date_match: + month, day, year = (int(g) for g in date_match.groups()) + year += 2000 if year < 100 else 0 + try: + item_received = dt.date(year, month, day).isoformat() + except ValueError: + self.warnings.append( + (sheet, rownum, f"bad embedded date in {part!r}")) + name = PAREN_DATE_RE.sub("", name).strip() + + quantity = "" + qty_match = XQTY_RE.search(name) + if qty_match: + quantity = qty_match.group(1) + name = XQTY_RE.sub("", name).strip() + + owner = "" + owner_match = OWNER_RE.search(name) + if owner_match: + owner = owner_match.group(1) + name = OWNER_RE.sub("", name).strip() + elif name.strip() in ("Goetz",): + owner, name = name.strip(), "unspecified contents" + + self.add_item(name, location_id, quantity=quantity, owner=owner, + received=item_received, notes=item_notes) + + # -- sheet handlers ----------------------------------------------------- + + def rows(self, sheet_name: str): + ws = self.wb[sheet_name] + for rownum, values in enumerate(ws.iter_rows(min_row=2, values_only=True), + start=2): + if any(v not in (None, "") for v in values): + yield rownum, values + + def drawers_cabinets(self, sheet: str) -> None: + """Room / Drawer-Cabinet / Number / (unnamed contents column).""" + # These free-text 'kind' values are coarse restatements of dedicated + # sheets; their contents duplicate the per-item rows there. + crossref_kinds = { + "shelves to the left of entry-way (reference other tab)": "213-SHELF", + } + unnamed_seq: Counter = Counter() + + for rownum, values in self.rows(sheet): + room_raw, kind_raw, number_raw, contents_raw = ( + clean(values[0]), clean(values[1]), clean(values[2]), + clean(values[3]) if len(values) > 3 else "") + room = room_code(room_raw) + kind_key = kind_raw.lower().rstrip(" .") + kind = norm_kind(kind_raw) + + # Pure cross-references to other sheets — no data of their own. + if contents_raw.lower().startswith("see other tab"): + self.skipped += 1 + continue + + # Coarse duplicates of a dedicated sheet: send contents to review + # rather than creating ~40 duplicate items. + if kind_key in crossref_kinds: + canonical = crossref_kinds[kind_key] + self.flag("coarse-duplicate-of-dedicated-sheet", sheet, rownum, + canonical, contents_raw, + " | ".join(p.strip() for p in contents_raw.split(";")), + f"shelf {number_raw!r}; reconcile against {canonical}-* " + "rows before adding") + continue + + # Build the location id. The 'Number' column is sometimes a real + # number, sometimes an ordinal ('Top', '2nd'), and sometimes prose + # ('Long clear bin on top of VWR fridge (1)'). Prose gets a + # sequential id and keeps its text in `label` -- slugifying it + # produced 60-character ids that are hostile as QR payloads. + code = KIND_CODES.get(kind, "OTH") + ordinal = ORDINALS.get(number_raw.lower()) + if number_raw.isdigit(): + suffix, label = f"{int(number_raw):02d}", f"{kind_raw.strip()} {number_raw}" + elif ordinal: + suffix, label = f"S{ordinal:02d}", f"{kind_raw.strip()} {number_raw}" + else: + # 'X' prefix keeps prose-numbered places from ever colliding + # with numerically-numbered ones (Cabinet 1 vs the first + # prose-described cabinet). + unnamed_seq[(room, kind)] += 1 + seq = unnamed_seq[(room, kind)] + suffix = f"X{seq:02d}" + label = (number_raw if number_raw and number_raw.upper() != "NA" + else f"{kind_raw.strip()} {seq}") + location_id = f"{room}-{code}-{suffix}" + + # Every row in this sheet is a distinct physical place, so an id + # collision here means two rows collapsed -- disambiguate loudly + # rather than silently merging their contents. + if location_id in self.locations: + seq = 2 + while f"{location_id}-{seq}" in self.locations: + seq += 1 + self.warnings.append( + (sheet, rownum, + f"duplicate location id {location_id} -> {location_id}-{seq}")) + location_id = f"{location_id}-{seq}" + + self.location(location_id, room, kind, number_raw, "", + label.strip() or location_id) + + if not contents_raw: + loc = self.locations[location_id] + if not loc["notes"]: + loc["notes"] = "no contents recorded in legacy spreadsheet" + continue + + self.explode(contents_raw, location_id, sheet, rownum) + + def shelved(self, sheet: str, room: str, unit_kind: str, unit_id: str, + unit_label: str, date_col: int | None = None, + notes_col: int | None = None) -> None: + """Sheets shaped: Shelf # / Contents / [Date] / [Notes].""" + self.location(unit_id, room, unit_kind, "", "", unit_label) + for rownum, values in self.rows(sheet): + shelf_raw = clean(values[0]) + contents = clean(values[1]) if len(values) > 1 else "" + if not contents: + continue + # Numeric/ordinal shelves become S01..S10 (zero-padded so they sort + # correctly as strings). Named shelves ('Door Shelf 2') keep their + # slug without the S prefix, which would read as 'SDOOR-SHELF-2'. + ordinal = (int(shelf_raw) if shelf_raw.isdigit() + else ORDINALS.get(shelf_raw.lower())) + if ordinal: + location_id = f"{unit_id}-S{ordinal:02d}" + elif shelf_raw: + location_id = f"{unit_id}-{slug(shelf_raw)}" + else: + location_id = unit_id + self.location(location_id, room, "shelf", shelf_raw, unit_id, + f"{unit_label} shelf {shelf_raw}") + + received, residue = "", "" + if date_col is not None and len(values) > date_col: + received, residue = parse_date(values[date_col], sheet, rownum, + self.warnings) + notes = "" + if notes_col is not None and len(values) > notes_col: + notes = clean(values[notes_col]) + if residue: + notes = f"{notes}; {residue}".strip("; ") if notes else residue + self.explode(contents, location_id, sheet, rownum, notes, received) + + def freezer_drawers(self, sheet: str, room: str, unit_id: str, + unit_label: str) -> None: + """Shelf Number / Drawer Number / Contents.""" + self.location(unit_id, room, "freezer", "", "", unit_label) + for rownum, values in self.rows(sheet): + shelf_raw, drawer_raw = clean(values[0]), clean(values[1]) + contents = clean(values[2]) if len(values) > 2 else "" + if not contents: + continue + shelf_no = f"{int(shelf_raw):02d}" if shelf_raw.isdigit() else slug(shelf_raw) + shelf_id = self.location( + f"{unit_id}-S{shelf_no}", room, "shelf", shelf_raw, unit_id, + f"{unit_label} shelf {shelf_raw}") + if drawer_raw and drawer_raw.upper() != "NA": + drawer_no = (f"{int(drawer_raw):02d}" if drawer_raw.isdigit() + else slug(drawer_raw)) + location_id = self.location( + f"{shelf_id}-D{drawer_no}", room, "drawer", drawer_raw, + shelf_id, f"{unit_label} shelf {shelf_raw} drawer {drawer_raw}") + else: + location_id = shelf_id + self.explode(contents, location_id, sheet, rownum) + + # -- driver ------------------------------------------------------------- + + def run(self) -> None: + names = self.wb.sheetnames + pick = lambda frag: next(n for n in names if frag in n) + + self.drawers_cabinets(pick("DrawersCabinets")) + self.shelved(pick("Refrigerator Rm 209"), "209", "refrigerator", + "209-FRIDGE", "Refrigerator Rm 209", date_col=2) + self.shelved(pick("Shelf Rm 209"), "209", "shelf", "209-SHELF", + "Shelf unit Rm 209", date_col=2) + self.shelved(pick("Shelf RM 213"), "213", "shelf", "213-SHELF", + "Shelves left of entryway Rm 213", date_col=2, notes_col=3) + self.freezer_drawers(pick("-20"), "213", "213-F20", "-20C freezer Rm 213") + + self.write() + self.report() + + def write(self) -> None: + self.outdir.mkdir(parents=True, exist_ok=True) + + def dump(filename: str, fields: list[str], rows: list[dict]) -> None: + with (self.outdir / filename).open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + + dump("locations.csv", LOCATION_FIELDS, + sorted(self.locations.values(), key=lambda r: r["location_id"])) + dump("items.csv", ITEM_FIELDS, self.items) + dump("review_queue.csv", REVIEW_FIELDS, self.review) + + def report(self) -> None: + print(f"locations {len(self.locations):>5}") + print(f"items {len(self.items):>5}") + print(f"review_queue {len(self.review):>5}") + print(f"skipped rows {self.skipped:>5} (cross-references only)") + print("\ncategories:") + for category, count in Counter( + i["category"] for i in self.items).most_common(): + print(f" {category:<12} {count:>4}") + print("\nreview reasons:") + for reason, count in Counter( + r["reason"] for r in self.review).most_common(): + print(f" {reason:<34} {count:>4}") + if self.warnings: + print(f"\ndata warnings ({len(self.warnings)}):") + for sheet, rownum, message in self.warnings: + print(f" {sheet}:{rownum} {message}") + + +def main() -> None: + root = Path(__file__).resolve().parent.parent + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--xlsx", + default=root / "legacy" / "Roberts Lab Inventory.xlsx", + type=Path) + parser.add_argument("--out", default=root / "data", type=Path) + args = parser.parse_args() + Migration(args.xlsx, args.out).run() + + +if __name__ == "__main__": + main() diff --git a/scripts/validate.py b/scripts/validate.py new file mode 100644 index 0000000..929fc9d --- /dev/null +++ b/scripts/validate.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +"""Validate data/*.csv. Runs in CI on every pull request. + +This is the guardrail that lets several people edit the inventory by hand (or +via generated PRs) without the data quietly rotting the way the spreadsheet +did. Errors fail the build; warnings are reported but allowed. + +Usage: + python3 scripts/validate.py [--data DIR] [--strict] + + --strict treat warnings as errors too +""" + +from __future__ import annotations + +import argparse +import csv +import datetime as dt +import re +import sys +from collections import Counter +from pathlib import Path + +ITEM_FIELDS = [ + "item_id", "name", "category", "location_id", "quantity", "unit", + "vendor", "catalog_no", "lot", "received", "expires", "status", + "last_verified", "verified_by", "owner", "source", "photo_id", "notes", +] +LOCATION_FIELDS = [ + "location_id", "room", "kind", "number", "parent_id", "label", "notes", +] +REVIEW_FIELDS = [ + "reason", "sheet", "row", "location_id", "raw_text", "suggestion", "notes", +] + +CATEGORIES = { + "kit", "reagent", "enzyme", "consumable", "sample", "equipment", "tool", + "media", "antibody", "glassware", "office", "other", +} +STATUSES = {"present", "low", "consumed", "discarded", "missing", "unverified"} +KINDS = { + "cabinet", "drawer", "shelf", "refrigerator", "freezer", "bench", "floor", + "bin", "other", +} +SOURCES = {"legacy-xlsx", "manual", "photo-llm"} + +ITEM_ID_RE = re.compile(r"^itm-\d{5}$") +LOCATION_ID_RE = re.compile(r"^[A-Z0-9][A-Z0-9-]*$") +# Full or partial ISO dates: 2019, 2019-11, 2019-11-27 +ISO_DATE_RE = re.compile(r"^\d{4}(-\d{2}(-\d{2})?)?$") + + +class Validator: + def __init__(self, datadir: Path): + self.datadir = datadir + self.errors: list[str] = [] + self.warnings: list[str] = [] + + def error(self, filename: str, row: int, message: str) -> None: + self.errors.append(f"{filename}:{row}: {message}") + + def warn(self, filename: str, row: int, message: str) -> None: + self.warnings.append(f"{filename}:{row}: {message}") + + def load(self, filename: str, expected: list[str]) -> list[dict]: + path = self.datadir / filename + if not path.exists(): + self.errors.append(f"{filename}: missing") + return [] + with path.open(newline="") as handle: + reader = csv.DictReader(handle) + if reader.fieldnames != expected: + self.errors.append( + f"{filename}: header mismatch\n" + f" expected: {','.join(expected)}\n" + f" found: {','.join(reader.fieldnames or [])}") + return [] + return list(reader) + + def check_date(self, filename: str, row: int, field: str, value: str, + allow_future: bool = False) -> None: + if not value: + return + if not ISO_DATE_RE.match(value): + self.error(filename, row, + f"{field}={value!r} is not ISO 8601 (YYYY, YYYY-MM, or YYYY-MM-DD)") + return + try: + parts = [int(p) for p in value.split("-")] + date = dt.date(parts[0], *(parts[1:] or [1]), *([1] if len(parts) < 3 else [])) + except (ValueError, TypeError): + self.error(filename, row, f"{field}={value!r} is not a real date") + return + if not allow_future and date > dt.date.today(): + self.warn(filename, row, f"{field}={value!r} is in the future") + + def run(self) -> int: + locations = self.load("locations.csv", LOCATION_FIELDS) + items = self.load("items.csv", ITEM_FIELDS) + self.load("review_queue.csv", REVIEW_FIELDS) + if self.errors: + return self.report() + + location_ids = self.check_locations(locations) + self.check_items(items, location_ids) + self.summarize(items, locations) + return self.report() + + def check_locations(self, locations: list[dict]) -> set[str]: + seen: set[str] = set() + for offset, row in enumerate(locations): + line = offset + 2 + lid = row["location_id"] + if not lid: + self.error("locations.csv", line, "empty location_id") + continue + if not LOCATION_ID_RE.match(lid): + self.error("locations.csv", line, + f"location_id {lid!r} must be uppercase alphanumerics " + "and hyphens (it becomes a URL fragment and QR payload)") + if lid in seen: + self.error("locations.csv", line, f"duplicate location_id {lid!r}") + seen.add(lid) + if row["kind"] not in KINDS: + self.error("locations.csv", line, + f"kind {row['kind']!r} not in {sorted(KINDS)}") + if not row["room"]: + self.error("locations.csv", line, "empty room") + if not row["label"]: + self.warn("locations.csv", line, + f"{lid} has no label; QR stickers will be unreadable") + + # Referential integrity and cycle detection on the parent chain. + parents = {r["location_id"]: r["parent_id"] for r in locations} + for offset, row in enumerate(locations): + line = offset + 2 + parent = row["parent_id"] + if not parent: + continue + if parent not in seen: + self.error("locations.csv", line, + f"parent_id {parent!r} does not exist") + continue + if parent == row["location_id"]: + self.error("locations.csv", line, "location is its own parent") + continue + chain, node = {row["location_id"]}, parent + while node: + if node in chain: + self.error("locations.csv", line, + f"parent chain cycles at {node!r}") + break + chain.add(node) + node = parents.get(node, "") + return seen + + def check_items(self, items: list[dict], location_ids: set[str]) -> None: + seen: set[str] = set() + for offset, row in enumerate(items): + line = offset + 2 + iid = row["item_id"] + if not ITEM_ID_RE.match(iid): + self.error("items.csv", line, + f"item_id {iid!r} must match itm-00000") + if iid in seen: + self.error("items.csv", line, f"duplicate item_id {iid!r}") + seen.add(iid) + + if not row["name"].strip(): + self.error("items.csv", line, "empty name") + elif row["name"] != row["name"].strip(): + self.error("items.csv", line, + f"name {row['name']!r} has leading/trailing whitespace") + + if row["category"] not in CATEGORIES: + self.error("items.csv", line, + f"category {row['category']!r} not in {sorted(CATEGORIES)}") + if row["status"] not in STATUSES: + self.error("items.csv", line, + f"status {row['status']!r} not in {sorted(STATUSES)}") + if row["source"] not in SOURCES: + self.error("items.csv", line, + f"source {row['source']!r} not in {sorted(SOURCES)}") + + if not row["location_id"]: + self.error("items.csv", line, "empty location_id") + elif row["location_id"] not in location_ids: + self.error("items.csv", line, + f"location_id {row['location_id']!r} is not in locations.csv") + + if row["quantity"] and not re.match(r"^\d+(\.\d+)?$", row["quantity"]): + self.error("items.csv", line, + f"quantity {row['quantity']!r} must be a number " + "(put ranges or notes in `notes`)") + + self.check_date("items.csv", line, "received", row["received"]) + self.check_date("items.csv", line, "last_verified", row["last_verified"]) + self.check_date("items.csv", line, "expires", row["expires"], + allow_future=True) + + # A machine-written row must always be traceable to its photo. + if row["source"] == "photo-llm" and not row["photo_id"]: + self.error("items.csv", line, + "source=photo-llm requires a photo_id for provenance") + + def summarize(self, items: list[dict], locations: list[dict]) -> None: + """Non-fatal health signals -- the anti-staleness dashboard in text form.""" + if not items: + return + cutoff = (dt.date.today() - dt.timedelta(days=365)).isoformat() + stale = [i for i in items if (i["last_verified"] or "0000") < cutoff] + unverified = [i for i in items if i["status"] == "unverified"] + empty_locations = [ + l for l in locations + if l["location_id"] not in {i["location_id"] for i in items}] + + print(f"items {len(items)}") + print(f"locations {len(locations)}") + print(f"unverified {len(unverified)} " + f"({100 * len(unverified) // len(items)}%)") + print(f"not verified in 1y {len(stale)} " + f"({100 * len(stale) // len(items)}%)") + print(f"locations w/o items {len(empty_locations)}") + print("by category: " + ", ".join( + f"{c}={n}" for c, n in Counter( + i["category"] for i in items).most_common())) + + def report(self) -> int: + if self.warnings: + print(f"\n{len(self.warnings)} warning(s):") + for warning in self.warnings: + print(f" WARN {warning}") + if self.errors: + print(f"\n{len(self.errors)} error(s):") + for error in self.errors: + print(f" FAIL {error}") + return 1 + print("\nvalidation passed") + return 0 + + +def main() -> None: + root = Path(__file__).resolve().parent.parent + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--data", default=root / "data", type=Path) + parser.add_argument("--strict", action="store_true", + help="treat warnings as errors") + args = parser.parse_args() + + validator = Validator(args.data) + code = validator.run() + if args.strict and validator.warnings: + code = 1 + sys.exit(code) + + +if __name__ == "__main__": + main() From 58c530ddf700b4ab72757e9053a8b96a724fb3d2 Mon Sep 17 00:00:00 2001 From: Steven Roberts Date: Mon, 3 Aug 2026 11:21:30 -0700 Subject: [PATCH 3/3] Declare inventory scope in rooms.csv, adding room 228 Scope is five rooms: 209, 213, 228, 230, and the -80C room. Declaring them in data/rooms.csv makes scope an explicit, enforced fact rather than a side effect of whatever the legacy spreadsheet happened to contain -- validate.py now fails if a location names an undeclared room. Room 228 appears nowhere in the legacy spreadsheet, so there is nothing to migrate for it and it starts with zero locations; it gets populated by walking in with a phone. Rather than let an in-scope-but-unrecorded room quietly be forgotten, validate.py emits a standing warning on every run until it has locations. Coverage is uneven and now visible: 209=70, 213=99, 228=0, 230=2, M80=2. Room labels are deliberately generic ("Room 228") rather than guessing at a purpose the spreadsheet never stated. Co-Authored-By: Claude Opus 5 --- PLAN.md | 15 ++++++++++++-- README.md | 3 +++ data/rooms.csv | 6 ++++++ scripts/migrate_legacy.py | 23 +++++++++++++++++++++ scripts/validate.py | 43 +++++++++++++++++++++++++++++++++++---- 5 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 data/rooms.csv diff --git a/PLAN.md b/PLAN.md index b8383d2..4b705ab 100644 --- a/PLAN.md +++ b/PLAN.md @@ -244,10 +244,21 @@ phone photo ──► photos/inbox/ (drag-drop into a PR, or an upload issue) 2. **Maintainers: `@sr320` and `@kubu4`**, both with merge rights. Implemented as a `CODEOWNERS` file requiring review from either. Two people is the right number — enough that nobody is a single point of failure, few enough that review actually happens. +3. **Scope is five rooms: 209, 213, 228, 230, and the -80˚C room.** Declared explicitly in `data/rooms.csv` rather than left implicit in whatever the spreadsheet happened to contain, and enforced — `validate.py` fails if a location names an undeclared room. + + **Room 228 appears nowhere in the legacy spreadsheet**, so it starts with zero locations. There is nothing to migrate for it; it gets populated by walking in with a phone. Until then `validate.py` emits a standing warning on every run so an in-scope-but-unrecorded room can't quietly be forgotten: + + ``` + WARN rooms.csv: room '228' (Room 228) is in scope but has no locations yet -- needs an inventory pass + ``` + + Current coverage: `209=70, 213=99, 228=0, 230=2, M80=2` locations. Rooms 230 and M80 have only 2 each, so they're really placeholders too — the shelves are recorded, their contents barely. + + Room labels are deliberately generic (`Room 228`, not a guessed purpose). Fill them in with something more useful when convenient. + ### Still open (not blocking Phase 1) -3. **Freezer boxes: in or out of scope?** Sample-level tracking (-80 boxes, positions, individual DNA extractions) is a genuinely different problem — thousands of rows, needs plate/position coordinates, and usually wants its own tool. I recommend **v1 tracks the box, not the tube**, with a hook to add sample-level later. -4. **Rooms 230 and the -80 room** appear in the spreadsheet with 2 rows each. In scope, or someone else's space? Migrated for now; trivial to drop. +4. **Freezer boxes: in or out of scope?** Sample-level tracking (-80 boxes, positions, individual DNA extractions) is a genuinely different problem — thousands of rows, needs plate/position coordinates, and usually wants its own tool. I recommend **v1 tracks the box, not the tube**, with a hook to add sample-level later. 5. **Item owners.** The spreadsheet tags several storage bins "- Goetz". An `owner` column is in the schema and populated where the legacy data made it explicit. Worth confirming this is a concept you want to keep tracking. --- diff --git a/README.md b/README.md index 9e5e770..5cbcace 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,11 @@ Development plan and architecture: **[PLAN.md](PLAN.md)** · Photo rules (public |---|---| | [`data/items.csv`](data/items.csv) | One row per item. The inventory. | | [`data/locations.csv`](data/locations.csv) | Controlled vocabulary of physical places, nested via `parent_id`. | +| [`data/rooms.csv`](data/rooms.csv) | The declared scope: rooms 209, 213, 228, 230, and the -80˚C room. | | [`data/review_queue.csv`](data/review_queue.csv) | Legacy rows too ambiguous to migrate automatically. Each has a stated reason. | +Coverage today is uneven — `209=70, 213=99, 228=0, 230=2, M80=2` locations. **Room 228 is in scope but appears nowhere in the legacy spreadsheet**, so it has nothing to migrate and needs an inventory pass; `validate.py` warns about it on every run until it does. + `location_id` is the stable key — `209-CAB-01`, `213-F20-S02-D08`, `209-FRIDGE-DOOR-SHELF-1`. It's built to be short because it becomes a URL fragment and a QR-sticker payload. Two fields carry more weight than they look like they do: diff --git a/data/rooms.csv b/data/rooms.csv new file mode 100644 index 0000000..1f068aa --- /dev/null +++ b/data/rooms.csv @@ -0,0 +1,6 @@ +room,label,notes +209,Room 209, +213,Room 213, +228,Room 228,in scope; no locations inventoried yet +230,Room 230, +M80,-80˚C room,shared cold room diff --git a/scripts/migrate_legacy.py b/scripts/migrate_legacy.py index 93a86be..24674bd 100644 --- a/scripts/migrate_legacy.py +++ b/scripts/migrate_legacy.py @@ -46,6 +46,7 @@ REVIEW_FIELDS = [ "reason", "sheet", "row", "location_id", "raw_text", "suggestion", "notes", ] +ROOM_FIELDS = ["room", "label", "notes"] # Rooms get short codes so location_ids stay ID-safe. "-80˚C room" would make # a hostile identifier. @@ -56,6 +57,18 @@ "-80˚c room": "M80", } +# The declared scope of the inventory. Rooms are listed here even when the +# legacy spreadsheet has nothing for them -- 228 is in scope but was never +# recorded, so it starts with zero locations and gets populated by walking in +# with a phone (Phase 4) rather than by migration. +ROOMS = { + "209": ("Room 209", ""), + "213": ("Room 213", ""), + "228": ("Room 228", "in scope; no locations inventoried yet"), + "230": ("Room 230", ""), + "M80": ("-80˚C room", "shared cold room"), +} + KIND_MAP = { "cabinet": "cabinet", "cabinets": "cabinet", @@ -526,11 +539,21 @@ def dump(filename: str, fields: list[str], rows: list[dict]) -> None: writer.writeheader() writer.writerows(rows) + dump("rooms.csv", ROOM_FIELDS, + [{"room": room, "label": label, "notes": notes} + for room, (label, notes) in sorted(ROOMS.items())]) dump("locations.csv", LOCATION_FIELDS, sorted(self.locations.values(), key=lambda r: r["location_id"])) dump("items.csv", ITEM_FIELDS, self.items) dump("review_queue.csv", REVIEW_FIELDS, self.review) + # A room in scope with nothing in it is a to-do, not an error. + used = {loc["room"] for loc in self.locations.values()} + for room in sorted(set(ROOMS) - used): + print(f"note: room {room} is in scope but has no locations yet") + for room in sorted(used - set(ROOMS)): + print(f"WARNING: room {room} has locations but is not declared in ROOMS") + def report(self) -> None: print(f"locations {len(self.locations):>5}") print(f"items {len(self.items):>5}") diff --git a/scripts/validate.py b/scripts/validate.py index 929fc9d..902ed8c 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -32,6 +32,7 @@ REVIEW_FIELDS = [ "reason", "sheet", "row", "location_id", "raw_text", "suggestion", "notes", ] +ROOM_FIELDS = ["room", "label", "notes"] CATEGORIES = { "kit", "reagent", "enzyme", "consumable", "sample", "equipment", "tool", @@ -95,18 +96,35 @@ def check_date(self, filename: str, row: int, field: str, value: str, self.warn(filename, row, f"{field}={value!r} is in the future") def run(self) -> int: + rooms = self.load("rooms.csv", ROOM_FIELDS) locations = self.load("locations.csv", LOCATION_FIELDS) items = self.load("items.csv", ITEM_FIELDS) self.load("review_queue.csv", REVIEW_FIELDS) if self.errors: return self.report() - location_ids = self.check_locations(locations) + room_ids = self.check_rooms(rooms) + location_ids = self.check_locations(locations, room_ids) self.check_items(items, location_ids) - self.summarize(items, locations) + self.summarize(items, locations, rooms) return self.report() - def check_locations(self, locations: list[dict]) -> set[str]: + def check_rooms(self, rooms: list[dict]) -> set[str]: + seen: set[str] = set() + for offset, row in enumerate(rooms): + line = offset + 2 + if not row["room"]: + self.error("rooms.csv", line, "empty room") + continue + if row["room"] in seen: + self.error("rooms.csv", line, f"duplicate room {row['room']!r}") + if not row["label"]: + self.warn("rooms.csv", line, + f"room {row['room']!r} has no label") + seen.add(row["room"]) + return seen + + def check_locations(self, locations: list[dict], room_ids: set[str]) -> set[str]: seen: set[str] = set() for offset, row in enumerate(locations): line = offset + 2 @@ -126,6 +144,10 @@ def check_locations(self, locations: list[dict]) -> set[str]: f"kind {row['kind']!r} not in {sorted(KINDS)}") if not row["room"]: self.error("locations.csv", line, "empty room") + elif row["room"] not in room_ids: + self.error("locations.csv", line, + f"room {row['room']!r} is not declared in rooms.csv " + "(add it there if it is in scope)") if not row["label"]: self.warn("locations.csv", line, f"{lid} has no label; QR stickers will be unreadable") @@ -203,10 +225,19 @@ def check_items(self, items: list[dict], location_ids: set[str]) -> None: self.error("items.csv", line, "source=photo-llm requires a photo_id for provenance") - def summarize(self, items: list[dict], locations: list[dict]) -> None: + def summarize(self, items: list[dict], locations: list[dict], + rooms: list[dict]) -> None: """Non-fatal health signals -- the anti-staleness dashboard in text form.""" if not items: return + # A declared room with no locations is an inventorying to-do, surfaced + # on every run so it doesn't get forgotten. + occupied = {l["room"] for l in locations} + for room in rooms: + if room["room"] not in occupied: + self.warn("rooms.csv", 0, + f"room {room['room']!r} ({room['label']}) is in scope " + "but has no locations yet -- needs an inventory pass") cutoff = (dt.date.today() - dt.timedelta(days=365)).isoformat() stale = [i for i in items if (i["last_verified"] or "0000") < cutoff] unverified = [i for i in items if i["status"] == "unverified"] @@ -216,6 +247,10 @@ def summarize(self, items: list[dict], locations: list[dict]) -> None: print(f"items {len(items)}") print(f"locations {len(locations)}") + print(f"rooms {len(rooms)}") + print("locations per room: " + ", ".join( + f"{r['room']}={sum(1 for l in locations if l['room'] == r['room'])}" + for r in rooms)) print(f"unverified {len(unverified)} " f"({100 * len(unverified) // len(items)}%)") print(f"not verified in 1y {len(stale)} "