Changelog¶
All notable changes to this project are documented in this file.
The format follows Keep a Changelog, and this project uses Semantic Versioning.
Every pull request must bump the version in pyproject.toml and add a
matching entry below; this is enforced by .github/workflows/pr-checks.yml.
[Unreleased]¶
[0.16.0] - 2026-10-14¶
Added¶
samples.manually_checked(schema/009_manually_checked.sql), a boolean that marks samples whose metadata has been checked by hand. Every existing sample starts asFALSE. Run the migration onccr_metadatabefore using this release.samples.updatetakesmanually_checked=True/False, so the change goes through the audit log.queries.samples_for_projectreturnsmanually_checkedas abool, so it is also insamples_with_metadata,project_tidy_tableand themetadata.csv/metadata.xlsxexport.
[0.15.0] - 2026-10-08¶
Added¶
- Raw FASTQ QC (
schema/008_fastq_qc.sql). A newsample_filesfile typefastq_qc(tierwork) for each sample's QC JSON written by noxqc, and a new tablesample_fastq_qc, one row per sample: read counts,pair_ok(MD5, gzip, R1/R2 counts and IDs), sequencing run, lanes, main index and its purity,barcode_matchagainst the run sheet, %Q30, mean quality, GC, duplication, adapter / poly-G / N maxima, depth relative to the plate, the worst flag and the list of flags. Run the migration onccr_metadatabefore using this release. noxdb.fastq_qc:upsert(idempotent; returnsinserted/updated/unchanged),get,deleteandfor_project(a project's samples with their QC, unchecked samples included).
[0.14.3] - 2026-10-08¶
Changed¶
docs/schema.mdexplainstimepoint = 'baseline': it is the default for projects without timepoint information and for plate controls, and means "single sample, no timepoint known", not a confirmed baseline visit.
[0.14.2] - 2026-10-08¶
Fixed¶
- The connection test in the install docs called
init_pool().get_connection(), butinit_pool()returnsNone. It now usesexecute()andclose_pool().
Changed¶
- The install docs recommend a mamba/conda environment from conda-forge
(
python,mariadb-connector-c,mariadb), so nothing has to be compiled; the system-library route is kept as a fallback. They namemariadb.liscas the database host, mention the LiSC firewall page as a way to get network access, explain that[noxdb-ssh]is needed on a laptop for both the tunnel and file downloads, and warn against settinglocal_portwithout your own tunnel.
[0.14.0] - 2026-09-26¶
Added¶
- Sequencing barcodes (
schema/007_sample_barcodes.sql).samplesgainsi7_index,i7_index_id,i5_indexandi5_index_id: the index sequences and the kit's names for them (e.g.TAACTTGGTC/IDT10_i7_1), from the same run sheet asSQR/SQRP. All four are nullable, and a case-sensitive CHECK allows only upper-case A/C/G/T/N in the sequences. Run the migration onccr_metadatabefore using this release. samples.create/get_or_create/updatetake the barcodes and upper-case the sequences (samples.canonical_index).samples.csv(folder and bulk import) takes them as optional columns; they are validated, and a top-up is checked against stored barcodes like the other sample values.queries.samples_for_project,controls_for_projectandlist_inputsreturn them.samples.set_sequencingsetsSQR/SQRPand the barcodes of a sample that already exists. Empty columns are filled in and equal ones left alone; a different stored value is refused unlessoverwrite=True.samples.sequencing_changesreports what a call would change without writing.noxdb.run_sheetreads the run sheet (Overview_SQRs,All_SQRsexport) by column name, matches its rows to samples by name and then by IP plate and well (the matchingscripts/backfill_sequencing_coords.pyused, now shared), and applies the result all or nothing.scripts/apply_run_sheet.pydoes this from the command line. It only reports unless given--commit, can write the planned changes to a CSV (--out), limit itself to one project (--project) and replace differing values (--overwrite). After migrating, run it on the full sheet to fill in the barcodes of the existing samples.
Changed¶
- An import warning about a sample's empty
SQR/SQRPor barcodes now says they can be set withscripts/apply_run_sheet.py.
[0.13.0] - 2026-09-26¶
Added¶
validate_bundle(noxdb._import) runs every import check without writing and returns aValidationResult(errors,warnings). Without a cursor it needs no database, which suits a submission form; with one it also checks the bundle against what is stored.load_project_dirand the row dataclasses are exported too, so a bundle can be built in memory.import_project_from_dirandscripts/bulk_import.pyboth use it, so the bulk import now also reports the plate-id normalization warnings it skipped, and prints its warnings instead of only logging them.- Additions to an existing project are checked against the database before anything is written. The import reuses stored subjects, visits and samples as they are, so a bundle value that differs from the stored one (a sex or origin, a visit's group or age, a sample's visit, type, IPR/IPRP/SQR/SQRP, library or antibody class) is now an error. Before, it was dropped without a word, or, for a subject, failed halfway through the commit. A value for a field the database has empty is a warning (the import does not fill it in), and so is metadata the import will overwrite.
Changed¶
meta_*columns insubjects.csvare refused, in the folder and the bulk format. There is no subject-level metadata, and these columns used to be dropped silently. Put such fields invisits.csv.- The "project already exists" error now reads "pass force=True (--force on the command line) to add to it" in both importers.
Fixed¶
docs/data-preparation.md:- The control wells follow the real plate layout: 81–84 Mock_1–4, 85–86 Anchor_1–2, 87–88 NC_1–2, 89–92 Mock_5–8, 93–94 Anchor_3–4, 95–96 NC_3–4. Well numbers count down each column.
- The
samples.csvexamples have the requiredipr/iprpcolumns. - Plate controls get their own
subjects.csvandvisits.csvrows, as the importer has always required. The page said to leave them out. - A test now imports the page's complete example, so the two can't drift apart again.
[0.12.1] - 2026-09-26¶
Changed¶
- The quickstart shows invented example data. Every output in
docs/quickstart.md(project list, summaries, subjects, visits, samples, files, controls, inputs, export) now uses the fictionalIBD_Viennaproject from the data-preparation guide instead of real database output, since the docs are public. The examples also show the current file types (fastq_r1/fastq_r2,zigp_loose) and thearchive_member/archive_offsetcolumns for FASTQs stored inside tars. - The data-preparation and schema pages, two code comments in
scripts/sweep/and the unit tests use invented sample, patient and project names instead of real ones. The tests check the same cases as before.
[0.12.0] - 2026-09-24¶
Added¶
- Files inside a tar (
schema/006_sample_files_archive_member.sql).sample_filesgainsarchive_member(the file's name inside the tar atfile_path,''for plain files) andarchive_offset(byte position of its data). The unique key moves fromfile_pathto (file_path,archive_member). This lets every FASTQ in the per-run tars underccr/mariaDB/fastq_tar/be registered as its own row, with the member's size and MD5. Run the migration onccr_metadatabefore registering any such row. files.register/get_or_registertakearchive_member,archive_offsetandfile_size_bytes;get_by_pathandexistsmatch on the member too.restatrefuses tar members.fetch.download_files_for_projectextracts a tar member on its own (read atarchive_offset, or found by name when the offset is unknown), locally or over SFTP, and checks it againstchecksum_md5; a mismatch is reported as failed and the partial copy removed.- In every download layout a tar member is saved under its own name inside
the tar (e.g.
<sample>/<name>_R1.fastq.gzforby_sample), so the runs of a resequenced sample no longer collide on<sample>/fastq_r1.fastq.gz. Plain files keep the existing naming. queries.files_for_projectandfind_db_files_missing_on_diskreturnarchive_memberandarchive_offset.
[0.11.0] - 2026-09-24¶
Changed¶
- The built-in SSH tunnel is now an OpenSSH
ssh -N -Lsubprocess instead ofsshtunnel/paramiko. The paramiko tunnel intermittently hung on results of ~100+ rows (surfacing as "Lost connection to server during query" / "Server has gone away"); the same queries over OpenSSH return in milliseconds.init_pool()still reuses a tunnel already listening onlocal_port, andclose_pool()stops the one it started. sshtunnelis no longer a dependency, and theparamiko<4cap (needed only bysshtunnel) is dropped; paramiko stays for SFTP downloads.
Removed¶
init_pool(ssh_password=..., ssh_pkey_password=...). The tunnel needs a login without a prompt: a key without a passphrase or one held byssh-agent. Both settings are still read from[noxdb-ssh]for SFTP downloads.
[0.10.0] - 2026-09-24¶
Added¶
zigp_loosefile type (schema/005_zigp_loose_file_type.sql). The unweighted ZIGP output kept down to neglogp >= 2, with the fitted lambda, theta and p_i per peptide, stored inccr/mariaDB/zigp_loose_cutoff/as<name>.csv.gz. It is the permissive list IDR needs to tune the enrichment cutoff;zigp_normstays the Holm padj <= 0.05 cut. Tierwork. Run the migration onccr_metadatabefore registering any such file.
[0.9.1] - 2026-09-24¶
Added¶
- The sweep's
db_sizesection also reports the size of each data folder underccr/mariaDBon the work and archive roots (counts, zigp, FASTQ and BAM tars) and how much each changed since the previous run.
Changed¶
docs/quickstart.mdnumbers regenerated from production (62 projects).
[0.9.0] - 2026-09-24¶
Changed¶
IPR/IPRPcome from the "Overview of IP runs" sheet, never from the sample name. Some plates' files were labelled with the wrongRxxPxx(CORSA ran as IP run 04, not 02; PREDICTS P5/P6 are R08P04/P05, not the R08P01/P02 that PCa_Innsbruck also uses), so the name is not a reliable source. Columns N (IP run #) and O (Plate #) of the sheet are, and are unique per plate.- Breaking:
samples.create,samples.get_or_createandworkflows.register_sample_with_filestakeiprandiprpas required keyword arguments. Import bundles neediprandiprpcolumns insamples.csv. Plate-control auto-linking at import keys on those columns. scripts/prepare_migration.pyandscripts/add_controls.pytake--ip-runsand writeipr/iprpfrom the sheet.- The sweep's near-duplicate check compares the stored
IPR/IPRPinstead of the name's prefix.
Added¶
noxdb.ip_runs: reads the sheet (load_plates), and translates the old label a record was named under to the sheet's plate through itsCombined*column (by_old_label,coords_for_old_name), settling a label two plates share by project.scripts/set_ip_coords_from_overview.py: generates the SQL that rewrites every storedIPR/IPRPfrom the sheet. On production it moves 1,117 rows on 16 plates (CORSA, COVID_NED, PSC-IBD-HC_UMCG, FRAmAbs and unlinked PREDICTS/KielmAbs controls); inputs and the SQR01 pilot plates R02P05/R02P06, which the sheet does not list, keep their stored values.
Removed¶
samples.ip_coords_from_name.
[0.8.1] - 2026-09-23¶
Added¶
- Sequencing coverage check in the monitoring sweep (
weekly,monthly,manual).scripts/sweep/sqr_coverage.pycompares a master sheet of every well the lab sequenced with noxDB and reports samples in noxDB per sequencing run, plus the run-sheet projects that are complete, partial or absent, with the recorded reason for anything missing. Run sheets spell names differently from what was sequenced (separators,Sample_prefix, well zero-padding), so matching falls back to a normalised name. Gaps are informational; the check warns only when the sheet is missing (NOXDB_MASTER_SHEET, default/lisc/data/work/ccr/mariaDB/master_sequenced_samples.csv) or noxDB holds samples the sheet lacks. Also runnable on its own for the full report. - Metadata completeness check in the sweep (
weekly,monthly,manual), fromscripts/sweep/metadata_completeness.py. Per project, over study samples: share of subjects with sex, of visits with an age and a real group, whether the project is longitudinal, which extra metadata fields exist and how complete they are, and the share of samples with a counts file. Least complete projects first; informational only.
[0.8.0] - 2026-09-10¶
SQR/SQRP never held the sequencing run. They held the IP run, and
0.7.3 put it there. This separates the two coordinate systems and
backfills the sequencing side from the run sheet.
The RxxPxx in a sample name is the immunoprecipitation run and
plate. SQR/SQRP are the sequencing run and plate, which come
from the run sheet and are encoded nowhere in the name. 0.7.3 read the
name into SQR/SQRP on the belief that they were the same thing, and
backfilled production accordingly: all 6722 rows held an IP coordinate
in a column named for sequencing. Anything that grouped or filtered on
SQR between 0.7.3 and this release was grouping by IP run.
Added¶
schema/004_ip_and_sequencing_coords.sqladdssamples.IPRandsamples.IPRPand moves the existing values into them. Because every currentSQR/SQRPwas written straight from the sample name, this is a rename of the data rather than a re-derivation. It leavesSQR/SQRPpopulated so the table is never half-described; the backfill below overwrites them.scripts/backfill_sequencing_coords.pygenerates the SQL that rewritesSQR/SQRPfrom theOverview_SQRs(All_SQRs)sheet. It reads only, and refuses to emit a partial backfill. Matching takes two passes: exact sample name (6018 rows), then IP plate and well (704 rows) for the names the sheet spells differently — hyphens for underscores inside a subject id, a different project label, a_REPEATsuffix.samples.ip_coords_from_name, the renamedplate_coords_from_name. Same parsing, a name that says what it returns. The old name is gone rather than aliased: it was the ambiguity that caused this.- Tests for the derivation, for
updatecorrecting a stored pair, and a regression test that a control on a shared IP plate is auto-linked even when its sequencing run differs from the study sample's.
Changed¶
- Control auto-linking keys on
IPR/IPRP. A control occupies wells 81-96 of the plate it controls for, so this was always an IP relationship. Production links are unaffected, because the columns it used to read already held IP values. The change matters going forward: on IP platesR08P01andR08P02the study wells and the control wells were sequenced separately, so keying on the sequencing pair would now miss them. samples.createderivesIPR/IPRPfromsample_nameinstead of taking them as arguments, so no caller can store a pair that contradicts the name.samples.updatecan still set them to correct a stored row.scripts/prepare_migration.pyreadsSQR/SQRPfrom the meta CSV again. 0.7.4 had it override those with the name, which was right when the columns were believed to be one system and wrong now.queries.samples_for_project,controls_for_projectandlist_inputsreturnIPR/IPRPalongsideSQR/SQRP.controls_for_projectorders by the IP plate it selected on.docs/schema.mdanddocs/data-preparation.mddocument both systems. The worked example now gives an IP plate and a sequencing plate with different numbers, since the old one used the same digits for both and taught the confusion this release fixes.docs/quickstart.mdregenerated fromscripts/probe_quickstart.pyagainst the backfilled database. Every count is unchanged; what moved is the coordinates.R14P02_77_..now reads IPR 14, IPRP 02, SQR 07, SQRP 02 where it used to read SQR 14, SQRP 02. Themetadata.csvexport grew from 24,096 to 25,155 bytes, being two columns wider.
Notes¶
- 32 control wells appear twice in the sheet, sequenced in two runs,
while only one measurement per well exists in the database. The
backfill takes the later run. On
R08P01andR08P02that puts those controls on a different sequencing plate from the study wells of their own IP plate; which of the two runs produced the stored counts file is not recorded anywhere. - Applied to
ccr_metadataon 2026-09-10. All 6722 rows verified afterwards: everySQR/SQRPmatches the intended backfill, everyIPR/IPRPmatches its sample name, and every control link still shares an IP plate with a study sample of its project. Sequencing runs now span 02-31 across 23 distinct runs, where the IP numbering spanned 02-47 across 40.
[0.7.5] - 2026-09-10¶
Monitoring sweep gains the checks it was missing. Closes #2.
Added¶
- Row counts per table, compared with the previous run. A table that
shrank at all warns; one that lost 10% or more errors. This runs in the
nightly heartbeat as well as weekly, so data loss is caught within a day
instead of a week. It is deliberately separate from the population
snapshot, which sums per-project figures and therefore double-counts a
shared sample and cannot see one linked to no project — the population
figure read 11582 files where
sample_filesheld 13345 rows. - Orphan check for rows that hang off nothing: samples in no project, subjects with no visit, visits with no sample. Foreign-key orphans cannot happen, so these are the semantic kind that queries silently skip past. Controls awaiting a study sample on their plate are reported but not flagged, since that state is legitimate.
- Duplicate detection, in two passes: rows identical on every column but
the surrogate key and
created_at, and samples whose plate, well and identity match while their names are padded differently — the shape that had one specimen registered twice asR05P01_01_..andR05P01_1_... - Activity summary from
created_at: new rows per table and new samples per project over the last 7 days. The existing audit-log check only sees writes made through noxdb on the sweep's own host, so it misses anything done from another machine. - Database size on disk, per table, largest first.
- Run metadata in every report and log line: start time, duration, CPU seconds, peak memory, the commit the script ran from, and the Python and host it ran on.
[0.7.4] - 2026-09-10¶
Closes the loop on the plate-coordinate work: 0.7.3 shipped the tooling fixes, the production data was backfilled by hand, and this stops the importer from reintroducing the drift on the next run.
Added¶
samples.plate_coords_from_name: reads the(SQR, SQRP)plate coordinates out of a sample name.R42P02_09_..is run 42 plate 02, and a run-only name such asR31_input1_01_..is run 31 with no plate.
Changed¶
scripts/prepare_migration.pyandscripts/add_controls.pynow derive the plate coordinates from the sample name instead of reading them from the manifest CSV. The filename is the authoritative plate identity: it matches the physical 96-file plates on disk, while manifest coordinates were found to collide, putting two different plates on one key. Both scripts report rows where the CSV disagrees, and fall back to the CSV only when a name carries no coordinates at all.samples.canonical_plate_idnow zero-pads a purely numeric identifier to two characters. Padding used to be preserved verbatim, so5and05described one physical plate as two; because control linking matches by exact string, that split left PIC_MUW, SAR_MUW and SNMG with no controls and misattributed others. Non-numeric and wider values are unchanged.scripts/add_controls.py's plate key runs through the same canonicalization as the study samples rather than its own verbatim strip, so the two sides cannot drift apart again.
[0.7.3] - 2026-09-10¶
Fixed¶
queries.find_disk_files_missing_in_dbnow defaults to walking only the directories that already hold registered files, exposed as the newqueries.registered_file_dirs. It previously walked the tier roots, and on this cluster/lisc/data/workis the entire institute's work filesystem: the sweep spent its whole 30-minute budget enumerating other groups' data and counted every readable file it found as unregistered. Passingroots=explicitly still walks whatever you name.- The monitoring sweep's disk-to-DB check now reports the directories it scanned, so the emailed report states its own scope.
Changed¶
docs/quickstart.mdregenerated again. Production data changed underneath it:samples.SQR/samples.SQRPwere backfilled from the sample name, so the plate coordinates in the examples now match theRxxPxxin the filenames, and rebuilding the control links moved project 7 from 239 samples to 175. The input series listing also changed, since the eight undeliverableR01P02rows were deleted and 96R31_inputsamples added.
[0.7.2] - 2026-09-10¶
Dependency and documentation fixes found while re-running the quickstart
probe against the migrated ccr_metadata.
Fixed¶
paramiko>=3.0,<4.0moved from theanalysisextra into the core dependencies.sshtunnel0.4.0 referencesparamiko.DSSKey, which paramiko 4.x removed, so a plain install of noxDB picked up paramiko 4 and every tunneledinit_pool()failed withmodule 'paramiko' has no attribute 'DSSKey'.
Changed¶
- Repository links updated from the old
phiper-dbname tonoxDB: the clone URL inscripts/sweep/README.mdand two links indocs/monitoring.md. docs/quickstart.mdregenerated from a freshscripts/probe_quickstart.pyrun againstccr_metadata. Every figure moved: project 7 now has 239 samples and 476 files, the project list is 20 entries, and the example file paths carry themariaDB/segment added by the September path backfill. The claim thatinit_pool()errors out when no tunnel is listening was also wrong and is now corrected.scripts/probe_quickstart.pyextended to coverinclude_controls=False, the column subset used in §14, and a metadata-onlyfetch.export_project, so the export byte sizes in §14 are now measured rather than approximate. Its tunnel hint no longer prints a hardcoded database IP.
[0.7.1] - 2026-09-09¶
Follow-up to the 2026-09-06 weekly sweep, which reported 5792 integrity issues and 13302 registered files missing on disk. Neither was data loss.
Added¶
scripts/sweep/noxdb_sweep.py: standalone monitoring script for the ccr-lab VM — liveness, schema fingerprint, population snapshot with week-over-week deltas, per-project integrity, DB↔disk drift, and audit log summary, emailed as an HTML report. Deployment instructions inscripts/sweep/README.md, check-by-check rationale indocs/monitoring.md.
Changed¶
countsis now a work-tier file type instead of archive.docs/schema.mdalready documented it that way and every counts row in production was already stored as work, sofiles.pywas the outlier._resolve_tier()now enforces thefile_type → tierinvariant it was documented to enforce: anarchive↔workoverride that disagrees with the type-derived tier is rejected.scratch/externaloverrides are unaffected. This also makesfiles.update()'s existing guard real rather than a no-op.DEFAULT_LISC_ROOTinscripts/prepare_migration.pyandscripts/add_controls.pyis now/lisc/data/work/ccr/mariaDB; themariaDBpath segment was missing, so generated manifests pointed at paths that do not exist.
Fixed¶
_setup_audit_logger()skipped setup whenever thenoxdb.auditlogger already had any handler attached, so it never installed its own file handler if something else got there first — pytest's logging plugin, or any application with its own handler. The audit log was silently never written. Setup and teardown now act only on the handler the module owns, which also stopsclose_pool()from tearing down a caller's logging setup.get_connection()rolls back onBaseExceptionrather thanException, so aKeyboardInterruptorSystemExitmid-transaction no longer returns a connection to the pool still holding its locks.- The project importer's pre-flight validator enforced the pre-003 schema
and rejected samples shared across projects. It now blocks only on a
file_pathbound to a different sample, the one global UNIQUE that re-use cannot resolve.
[0.7.0] - 2026-05-16¶
Many-to-many project↔sample schema. Applied to production ccr_metadata
via schema/003_cross_project_samples.sql.
Added¶
schema/003_cross_project_samples.sql: newproject_samplesjunction table — the sole source of truth for project↔sample membership (a sample can belong to several projects). Backfills study/input/control links and canonicalizes existingSQR/SQRP.samples.link_to_project(): idempotent project↔sample link helper.samples.canonical_plate_id(): single SQR/SQRP canonicalization chokepoint (strip whitespace,NA/N/A/empty →"", padding preserved); applied on every write and reused by the importer, which now also validates plate ids and reports normalizations.subjects.get_or_create()raises on a reusedsubject_codewith a conflictingsex/origin(loud cross-study collision guard).
Changed¶
- Project membership lives entirely in
project_samples;subjectsno longer carriesproject_idandsubject_codeis now globally unique.subject → visit → sampleis pure lineage with no project affiliation. - Controls (mockIP/anchor/NC) are linked to every study project that shares their plate (SQR+SQRP) at import/migration time instead of living in dedicated control projects.
queries.*project-scoped helpers,workflows.register_subject_with_visit, and the project importer rewritten to the junction model;project_summary.n_samplesnow counts every linked sample (controls included).scripts/add_controls.pyredesigned to emit controls into the plate-sharing study bundles rather than dedicated control projects.- Docs (
schema.md,index.md,quickstart.md,reference/index.md) updated to the junction model;quickstart.mdregenerated from a live run against the migrated database.
Removed¶
- Dedicated
mockIP/anchor/NCprojects (deleted by migration 003; theinputumbrella project is retained). scripts/fix_controls_projects.py(obsolete under the new model).
Security¶
users/revoke_readwrite.sql: revoked INSERT/UPDATE/DELETE from the two analyst accounts (SELECT-only like other non-admin users) — ad-hoc writes would bypass theproject_samplesinvariant.
[0.5.2] - 2026-05-15¶
Fixed¶
README.md: corrected all documentation site links fromnoxdbtonoxDBto match the renamed GitHub Pages URL.README.md: corrected codecov badge and link to point to the renamed repository (noxDB).
[0.5.1] - 2026-05-15¶
Changed¶
docs/index.md: replaced CLI card with cards for Preparing data, Testing, and Contributing; added Gabriel Innocenti's email to the contact section.README.md: restored WIP status, codecov, and version badges that were removed in a previous documentation pass.
[0.5.0] - 2026-05-15¶
Added¶
docs/quickstart.mdsection 14: fullfetchmodule example with live output — project structure (projects.get,project_summary), file manifest (files_for_project), andfetch.export_projectproducing a local folder withmetadata.csvandREADME.txt. Also shows theinclude_files=True/layout/file_typespath for on-LiSC use.docs/data-preparation.md: new data-preparation section covering how raw exports are transformed before import.
Changed¶
docs/schema.mdfully rewritten: every table now has a complete column-by-column reference (type, nullability, constraints). Controls design documented in a dedicated section — mockIP, anchor, NC, and input each live in their own project (ids 61, 64, 67, 58 respectively) and are linked back to study projects via theSQR/SQRPplate coordinates. Nullablesex/ageand theNCENUM addition from migration002are called out inline. Previously wrong column names (meta_key→key_name,value_float→value_numeric) corrected.docs/cli.mdremoved; CLI is no longer part of the package.mkdocs.ymlupdated to reflect the new documentation structure.- DB renamed to
noxDB.
[0.4.5] - 2026-05-15¶
Added¶
scripts/add_controls.py: recovers anchor, mock, NC, and input control samples fromOverview_SQRs.csv(absent from per-project metadata CSVs) and appends them to the four master CSVs in the import directory. Input samples are placed in a standalone"input"project. Safe to re-run — already-presentsample_nameentries are silently skipped.
Fixed¶
subjects.create/subjects.get_or_create: thesex or Nonenormalisation now also converts the literal strings'NA'/'N/A'toNULL, preventing a DB-sideCHECKconstraint violation whenprepare_migration.pywrites those placeholders.- Import validation (
runner._validate_schema,runner._commit): sex and age values of'NA'/'N/A'(written byprepare_migration.pyfor real samples with missing demographics) are now treated as nullish, consistent with the DB schema allowingNULLfor both columns.
[0.4.4] - 2026-05-15¶
Added¶
schema/002_controls_support.sql: migration that makessubjects.sexandvisits.agenullable (non-null values still constrained to'M'/'F'and>= 0respectively) and adds'NC'to thesamples.sample_typeENUM.scripts/prepare_migration.py:_detect_sample_type()inferssample_typefrom substrings inSampleName(Anchor→anchor,Mock→mockIP,NC→NC,input→input). Control samples no longer generate spurious sex/age warnings and are written with blank sex/age (stored as NULL) rather than a placeholder0.users/revoke_readwrite.sql: one-time script to downgradelovro.trgovec-greifandmelanie.prinzensteinertoSELECT-only.
Changed¶
subjects.create/subjects.get_or_create:sexparameter is nowstr | None; empty string is normalised toNULLon insert.visits.create/visits.get_or_create:ageparameter is nowint | None.- Import validation (
runner._validate_schema): sex and age checks are skipped when the value is empty/null rather than raising an error. users/users.sql:lovro.trgovec-greifandmelanie.prinzensteinerreduced toSELECT-only, consistent with all other non-admin users.
[0.4.3] - 2026-05-14¶
Added¶
scripts/prepare_migration.pyandscripts/bulk_import.py: two CLI scripts for bulk-loading legacy data.prepare_migration.pytransforms a raw export directory into the canonical import folder layout expected byimport_project_from_dir;bulk_import.pywraps it to drive multiple projects in one run.
Changed¶
countsfiles may now be stored in theworktier as well asarchive. The hardarchive ↔ workflip rejection infiles._resolve_tieris removed; thescratch/externalescape hatches are unchanged.files.register/files.get_or_registergain askip_disk_checkparameter (forwarded from_inspect_file): whenTrue, path-prefix and on-disk stat/MD5 checks are skipped andfile_size_bytesis stored asNULL. Useful when registering files that are not yet mounted on the current host.
Fixed¶
paramikopinned to<4.0in theanalysisoptional-dependency group.paramiko4+ removedDSSKey;sshtunnel0.4.0 still references it, causing anAttributeErroroninit_pool()when the SSH tunnel path is taken.
Maintenance¶
migration_import/,migrations/, andnotebooks/added to.gitignore.
[0.4.2] - 2026-05-11¶
Changed¶
README.mdrewritten as a plain, copy-pasteable onboarding page: short summary of the repo's purpose, step-by-step install (system deps →git clone→ venv →pip install -e ".[analysis]"→ smoke test), minimal~/.my.cnfexample, links to the docs site for everything else. The access-tier section is dropped (lives on the docs site / is managed off-repo); the Contact section now lists Gabriel Innocenti alongside Mateusz Kołek.
[0.4.1] - 2026-05-11¶
Changed¶
- Converted all public docstrings in
noxdb(connection,projects,subjects,visits,samples,metadata,files,queries,workflows,fetch) and the_importsubpackage to Google style with explicitArgs:/Returns:/Raises:sections. No code behavior changes. The API-reference pages on the docs site now render structured parameter tables. docs.ymlrunsmkdocs buildwithout--strict. Griffe warns on the deliberately-untypedcurparameter (cursor can be either amariadbcursor or a_LoggingCursorwrapper); addingcur: Anyeverywhere would be churn without semantic gain.
[0.4.0] - 2026-05-11¶
Added¶
- Documentation site scaffold built with MkDocs + Material theme +
mkdocstrings (
mkdocs.yml,docs/{index,install,quickstart,schema, cli,contributing,changelog}.md, and per-module pages underdocs/reference/). Auto-generates API reference from package docstrings;NEWS.mdandCONTRIBUTING.mdare surfaced viamkdocs-include-markdown-plugin. CONTRIBUTING.mddocumenting dev setup, branching, the version +NEWS.mdrequirement enforced bypr-checks.yml, docstring style, and how to preview the docs locally..github/workflows/docs.yml— builds the site withmkdocs build --stricton every PR and deploys to GitHub Pages on every push tomain. Concurrency group keeps deploys serialized.docsoptional-dependency group inpyproject.toml(mkdocs-material,mkdocstrings[python],mkdocs-include-markdown-plugin).pip install -e ".[docs]"thenmkdocs serveis the local preview path.
[0.3.0] - 2026-05-11¶
Added¶
- Master import: load a whole project folder (
project.yaml,subjects.csv,visits.csv,samples.csv,files/manifest.csv) into the database in one atomic transaction. noxdb._import.import_project_from_dir(root, *, dry_run, force, compute_md5, skip_disk_check, log_dir)is the library entry point; it raisesProjectImportErrorwith an exhaustive list of collected errors when validation fails.scripts/import_project.pyis the matching CLI, exits 0 / 2 / 3 for success / validation failure / unexpected error and writes a JSON report to stdout plus<log_dir>/<ts>_<project>.log.- CSV columns prefixed with
meta_are treated as typed metadata keys; values are coerced int → float → bool → str. - Re-running on the same folder requires
--force(orforce=True). With--forcethe importer is idempotent: re-uses every existing row viaget_or_create/set_*and reportsinsertedvsexisting/unchangedper table. - Cross-project collisions on
sample_name/file_pathare blocked even with--force(those UNIQUEs are global by design). pyyaml>=6.0added as a hard dependency (required to readproject.yaml).
[0.2.3] - 2026-05-11¶
Added¶
fetchmodule that materializes a project from the database to a local folder:export_metadata_table— write the project tidy table to CSV and/or XLSX (requirespandas, plusopenpyxlforxlsx).download_files_for_project— copy every registered file for a project into a target directory. Uses paramiko SFTP when anssh_hostis configured (via~/.my.cnf [noxdb-ssh],NOXDB_SSH_*env vars, or kwargs), and falls back toshutil.copyfilewhen running on LiSC with the storage mounted locally. Supportsby_sample,by_type, andflatoutput layouts; resumes by skipping destinations that already exist.export_project— one-shot snapshot:metadata.csv(and optionallymetadata.xlsx),files/<layout>/..., and aREADME.txtsummary.paramiko>=3.0added to theanalysisoptional-dependency group.
[0.2.2] - 2026-05-11¶
Fixed¶
get_connection()now forces server-sideSET autocommit=0on every pool checkout instead of relying on the Python-sideconn.autocommitsetter. The setter elides theSETcommand when its cached flag already matches, so after a pool reset the server could be left in autocommit=1 while Python thought it was 0. Inserts then auto-committed androllback()was a silent no-op. Triggered intermittently for later-session tests; surfaced by a regression test against thesubjectstable.
Added¶
workflowsmodule bundling atomic, idempotent high-level operations on top of the per-table CRUD helpers:register_subject_with_visit—subjects.get_or_create+visits.get_or_create+ optionalvisit_metadataupsert, all in one transaction.register_sample_with_files—samples.get_or_create+ optionalsample_metadataupsert + per-filefiles.get_or_register, all in one transaction. A disk-validation error on any file rolls back the sample and its metadata too.- Both workflows take an optional
cur=None; when None they open their owntransaction(), otherwise they piggyback on the caller's cursor viacontextlib.nullcontext, so they compose cleanly inside a larger atomic block.
[0.2.1] - 2026-05-11¶
Added¶
- Composite read-only
queriesmodule innoxdb: samples_for_project— joinprojects → subjects → visits → sampleswith optional filters onfile_type,sample_type, andhas_files.samples_with_metadata/project_tidy_table— EAV pivot fromsample_metadata(and optionallyvisit_metadata) into a wide-formpandas.DataFrame. Visit-level keys colliding with sample-level keys are renamed with avisit_prefix. Metadata key names are filtered against^[A-Za-z_][A-Za-z0-9_]*$for safe DataFrame columns.files_for_project—sample_filesjoined with parent identifiers, filtered byfile_typeand/orstorage_tier.project_summary— counts of subjects/visits/samples/files plus per-file_typebreakdown (returns adict, no pandas dependency).find_db_files_missing_on_disk— DB rows whosefile_pathis gone from disk; suitable for a cron sweep.find_disk_files_missing_in_db— regular files underNOXDB_ARCHIVE_ROOT/NOXDB_WORK_ROOT(or caller-provided roots) that are not registered insample_files.integrity_check— per-project report covering samples without files, archive files without MD5, and files outside their tier root.analysisoptional-dependency group inpyproject.tomlforpandas+openpyxl. Tests depend onpandasand skip DataFrame assertions when it is missing.
[0.2.0] - 2026-05-09¶
Added¶
- Per-table CRUD modules in
noxdb: projects—create,get,get_by_name,get_or_create,list_all,update,delete,exists(id/name XOR),count.subjects— same shape keyed on the composite(project_id, subject_code), pluslist_for_project/count_for_project.visits— keyed on(subject_id, timepoint);get_or_createrejects NULL timepoints because the UNIQUE doesn't deduplicate them.samples—sample_nameis globally UNIQUE; module mirrors theprojectsshape withlist_for_visit/count_for_visit.metadata— shared EAV wrapper forvisit_metadataandsample_metadatawith explicitset_visit / get_visit / list_for_visit / delete_visit(and_samplemirrors). Idempotent viaINSERT … ON DUPLICATE KEY UPDATE;set_*returns"inserted" | "updated" | "unchanged". Bool stored in BOOLEAN is coerced back to Pythonboolon read.files— filesystem-validating registration forsample_files: absolute path + regular-file check, fastq/bam extension validation, file-type → tier derivation (archive vs work),os.path.realpathprefix check that blocks symlink escapes and sibling-prefix paths, optional chunked MD5 (compute_md5) or caller-supplied checksum,register,get_or_register(idempotent onfile_pathand UNIQUE-violation race-safe),restat, plus the standard CRUD helpers. Roots configurable viaNOXDB_ARCHIVE_ROOT/NOXDB_WORK_ROOT(defaults/lisc/archive,/lisc/work).update(storage_tier=…)enforces the samefile_type → tierinvariant asregister()— flippingarchive↔workis rejected;scratch/externaloverrides are still allowed.
Changed¶
- CI now runs
pytest --covagainstsrc/noxdb(branch coverage), prints a missing-lines report in the workflow log, and uploadscoverage.xmlas a build artifact.pytest-covis a new optionaltestdependency; coverage settings live under[tool.coverage.*]inpyproject.toml. _LoggingCursorandexecute()passparamsthrough unchanged when the caller provides an empty container; onlyparams is Noneis substituted with()._log_if_writenow strips leading SQL comments and an optionalWITH …CTE before classifying the statement, so audited writes are no longer missed when the query starts with comments or a CTE.get_connection()cleanup wrapsrollback()andclose()in their own try/except so the original DB exception is preserved; rollback / close failures are logged instead of masking it.
Fixed¶
seed/load_fake_data.load()now closes its cursor in atry/finally, preventing cursor leaks back into the pool..github/workflows/pr-checks.ymlversion-extraction commands no longer abort the step ongrepmisses; the explicit-zguard now reports the error as intended.
[0.1.1] - 2026-05-08¶
Added¶
- Initial schema in
schema/001_initial.sql: project → subject → visit → sample hierarchy with EAV metadata tables (visit_metadata,sample_metadata) andsample_filesfor tracking file pointers. - User and role definitions in
users/users.sql(admin / read-write / read-only tiers, restricted tolisc.%hosts). noxdbPython package with:- Connection pool (
init_pool,close_pool,get_connection). transaction()context manager with audit-logging cursor wrapper.execute()helper returning rows aslist[dict].- Audit log of write statements at
~/.noxdb/audit.log(override viaNOXDB_AUDIT_LOG). - Credentials read from
~/.my.cnf[noxdb]section by default; overridable per-call viainit_pool(...)keyword arguments and via theNOXDB_DATABASEenv var. - Fake-data seed script (
seed/load_fake_data.py) covering all four EAV value types and a longitudinal subject example. - CI workflow running pytest against MariaDB 10.11.