Skip to content
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
69c6b65
renamed the parameter for clarity
peterlavalle-nhs Sep 28, 2026
ba6742d
always check sheets we're checking (for hidden dates et al)
peterlavalle-nhs Sep 28, 2026
ff2f4a2
detects the problem
peterlavalle-nhs Sep 28, 2026
1789a14
more detail for the error message
peterlavalle-nhs Sep 28, 2026
920b301
validates what it should
peterlavalle-nhs Sep 28, 2026
0547569
fixes ignores to skip temp xlsx files
peterlavalle-nhs Sep 28, 2026
34469d5
passes all tests again
peterlavalle-nhs Sep 28, 2026
45ab14d
checks for padded cell values
peterlavalle-nhs Sep 29, 2026
5250972
updates it to handle ignored values with spaces around them
peterlavalle-nhs Sep 29, 2026
d727465
Update log message and comments for clarity
g-pechorin Sep 29, 2026
592c130
might do the dates
peterlavalle-nhs Sep 29, 2026
32bc10c
Merge remote-tracking branch 'origin/shift_ignore' into shift_ignore
peterlavalle-nhs Sep 29, 2026
4b4479a
more corrections
peterlavalle-nhs Sep 29, 2026
19ff976
raises a specific error for missing patient columns
peterlavalle-nhs Oct 1, 2026
af53890
(WIP) need to digest/adapt the patient_id loading
peterlavalle-nhs Sep 30, 2026
3fa2186
updates the rest of the date shift
peterlavalle-nhs Oct 1, 2026
38ece50
improved function and comments
peterlavalle-nhs Oct 1, 2026
7247526
uses the new exception
peterlavalle-nhs Oct 1, 2026
db26a0c
changes to allow pip to access book_page
peterlavalle-nhs Oct 2, 2026
65de773
small logging messages
peterlavalle-nhs Oct 2, 2026
f470909
Merge remote-tracking branch 'origin/book_page' into book_page
peterlavalle-nhs Oct 2, 2026
77245b8
more logging
peterlavalle-nhs Oct 2, 2026
9e98b1e
log during id generation
peterlavalle-nhs Oct 2, 2026
dc1b873
remove the pd call
peterlavalle-nhs Oct 2, 2026
651c457
restored it - it wasn't the problem
peterlavalle-nhs Oct 2, 2026
57e1bd2
ignore the coverage database
peterlavalle-nhs Oct 5, 2026
117a386
removes duplicate parameters from _inplace
peterlavalle-nhs Oct 5, 2026
3fc3d9d
update docs (oops)
peterlavalle-nhs Oct 5, 2026
68309d8
removes duplicate parameters from _inplace
peterlavalle-nhs Oct 5, 2026
700a568
update docs (oops)
peterlavalle-nhs Oct 5, 2026
f3dcf66
Merge branch 'shift_ignore' into shift_ignore_yaml
peterlavalle-nhs Oct 5, 2026
83c87ef
shift ignore reading from YAML
peterlavalle-nhs Oct 5, 2026
5091e8e
detects old (bad) configs
peterlavalle-nhs Oct 5, 2026
9441cf4
don't count tests/ when checking coverage
peterlavalle-nhs Oct 5, 2026
be338b1
debug logging for the ignores
peterlavalle-nhs Oct 5, 2026
4215f13
tests all pass with the new yaml file approach
peterlavalle-nhs Oct 5, 2026
6e70a83
Merge branch 'shift_ignore' into book_page
peterlavalle-nhs Oct 5, 2026
de2be87
just-enough to see if it helps
peterlavalle-nhs Oct 5, 2026
1451435
test/uv updates
peterlavalle-nhs Oct 8, 2026
184230e
Merge branch 'book_page' into book_stream
peterlavalle-nhs Oct 8, 2026
77b5476
pointed to the live copy
peterlavalle-nhs Oct 8, 2026
cdc970d
WIP 93/111 tests pass
peterlavalle-nhs Oct 8, 2026
817acb3
implements "row is blank" test and skipping
peterlavalle-nhs Oct 9, 2026
e15626d
"cheats" and switches the ambiguous date to iso8061
peterlavalle-nhs Oct 9, 2026
9266d43
corrected overlap detection and testing
peterlavalle-nhs Oct 9, 2026
8e04437
fixes missing patient id column detection
peterlavalle-nhs Oct 9, 2026
1206f2d
added comments and checks for "skip holes"
peterlavalle-nhs Oct 12, 2026
357067c
Merge remote-tracking branch 'origin/main' into book_page
peterlavalle-nhs Oct 12, 2026
7801176
corrected the result of a rename
peterlavalle-nhs Oct 12, 2026
bc74743
fix again
peterlavalle-nhs Oct 12, 2026
a17e990
allow header default to 0 in patient page, and, allow date values in …
peterlavalle-nhs Oct 12, 2026
17de1f4
refactored the existing functions (as is) into separate files
peterlavalle-nhs Oct 12, 2026
4a5e407
brings book_page (and tests) into the project
peterlavalle-nhs Oct 12, 2026
8dd5540
raise different exception
peterlavalle-nhs Oct 12, 2026
cde02e7
refactors the shift_delta generation to be simpler and move checking …
peterlavalle-nhs Oct 12, 2026
7266555
try to separate the sheet/row checks to be a standalone thing
peterlavalle-nhs Oct 12, 2026
bca28c5
refactored the loop control and parameter lookups into a scanner class
peterlavalle-nhs Oct 12, 2026
1fc05bf
extracts the ignore rules to a subroutine
peterlavalle-nhs Oct 12, 2026
7f75ba7
idea for how to fixit
peterlavalle-nhs Oct 12, 2026
0f06412
more cleanup
peterlavalle-nhs Oct 13, 2026
93da71d
comments and removed a redundant check
peterlavalle-nhs Oct 13, 2026
fe70510
"orchestration" changes
peterlavalle-nhs Oct 14, 2026
e15b3c5
csv and excel workbooks now both try to parse dates to be consistent …
peterlavalle-nhs Oct 14, 2026
54d8c3e
adds the hidden-date bypass to book_page
peterlavalle-nhs Oct 16, 2026
09e2e8a
always allow "NULL" in date columns
peterlavalle-nhs Oct 16, 2026
a4eb73b
option to skip dating date
peterlavalle-nhs Oct 16, 2026
c7d8776
add "skip" to sheet/page types
peterlavalle-nhs Oct 16, 2026
c17e023
cut out normalise in a different way
peterlavalle-nhs Oct 16, 2026
a17deb2
renamed and documented the bypasses
peterlavalle-nhs Oct 16, 2026
c623097
raise exceptions on abstract invocation
peterlavalle-nhs Oct 16, 2026
24bd638
Potential fix for pull request finding 'CodeQL / Clear-text logging o…
g-pechorin Oct 16, 2026
780682a
renamed self to self
peterlavalle-nhs Oct 16, 2026
10c96df
Merge remote-tracking branch 'origin/book_page' into book_page
peterlavalle-nhs Oct 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions nuh_helper/book_page/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from nuh_helper.book_page.csv_book import CSVBook
from nuh_helper.book_page.generic import Book, Page, book_open
from nuh_helper.book_page.xlsx_book import XLSXBook

__all__ = [ # noqa: PLE0604
Comment thread
g-pechorin marked this conversation as resolved.
Resolved
CSVBook,
XLSXBook,
Book,
Page,
book_open,
]
37 changes: 37 additions & 0 deletions nuh_helper/book_page/book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from abc import ABC, abstractmethod
from collections.abc import Generator as gen
from collections.abc import Iterable as iterator
from functools import cached_property
from pathlib import Path


class Page(ABC):
def __init__(self, name: str, page: object) -> None:
self._name = name
self._page = page

@abstractmethod
def stream_rows(self) -> gen[tuple[int, list[str]]]: ...
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

@cached_property
def name(self) -> str:
return self._name


class Book(ABC):
def __contains__(self, name: str) -> bool:
return name in self.keys()

@abstractmethod
def __getitem__(self, name: str) -> gen[Page]: ...
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

@abstractmethod
def keys(self) -> iterator[str]: ...
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

@abstractmethod
def stream_copy(self, into: Path) -> gen[tuple[str, int, list[str]]]: ...
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

def __iter__(self) -> gen[Page]:
for name in self.keys():
with self[name] as page:
yield page
114 changes: 114 additions & 0 deletions nuh_helper/book_page/csv_book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import contextlib
import csv
from _collections_abc import dict_keys
from collections.abc import Generator as gen
from pathlib import Path

from nuh_helper.book_page.book import Book, Page


class CSVBook(Book):
def __init__(self, path: str | Path | list[Path]) -> None:
self._page_files = {page.stem.lower(): page for page in csv_files(path)}

def keys(self) -> dict_keys:
return self._page_files.keys()

@contextlib.contextmanager
def __getitem__(self, name: str) -> gen[Page]:

class CSVPage(Page):
def stream_rows(self) -> gen[tuple[int, list[str]]]:
assert isinstance(self._page, Path)
with self._page.open() as file:
for row, line in enumerate(
csv.reader(
file,
delimiter="\t" if file.name.endswith(".tsv") else ",",
)
):
# strip cell strings
line = [
cell.strip() if isinstance(cell, str) else cell
for cell in line
]

# convert empty cells to None
line = [cell if cell else None for cell in line]

# strip bom
if (
row == 0
and isinstance(line[0], str)
and line[0][:1] == "\ufeff"
):
line[0] = line[0][1:]

yield (
row,
line,
)

assert isinstance(self._page_files[name], Path)
yield CSVPage(name, self._page_files[name])

def stream_copy(self, into: str | Path) -> gen[tuple[str, int, list[str]]]:
if not isinstance(into, Path):
into = Path(into)

if into.is_file():
raise RuntimeError(f"can't write csvs to {into} because it's a file")

into.mkdir(parents=True, exist_ok=True)

for page in self:
name = page.name
file = self._page_files[name].name
with (into / file).open("w") as data:
writer = csv.writer(
data, delimiter="\t" if file.endswith(".tsv") else ","
)

for row, cell in page.stream_rows():
yield name, row, cell
writer.writerow(cell)


def csv_files(path: str | Path | list[Path]) -> list[Path]:
"""process whatever form the path takes into a list of Path objects"""
if isinstance(path, str):
path = Path(path)

# get it to a list
if isinstance(path, Path):
if path.is_file():
raise RuntimeError(f"need a dir or list of files, but, got a file {path=}")
path = sorted(
list(path.glob("*.csv")) + list(path.glob("*.tsv")),
key=lambda file: file.stem.lower(),
)

# compute dupes
collisions = sorted(
[
page
for page in path
if len([them for them in path if them.stem.lower() == page.stem.lower()])
!= 1
],
key=lambda file: file.name.lower(),
)
# raise error if there are dupes
if collisions:
raise ValueError(
f"the following csv files have name collisions\n {
'\n '.join(
[
(file.stem.lower() + '\n ' + str(file))
for file in collisions
]
)
}"
)

return path
25 changes: 25 additions & 0 deletions nuh_helper/book_page/generic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathlib import Path

from .book import Page
from .csv_book import CSVBook
from .xlsx_book import XLSXBook

# use this or-type instead of the "real" base class since we only have/support these two
Book = CSVBook | XLSXBook


def book_open(src: str | Path | list[Path]) -> Book:
if isinstance(src, str):
src = Path(src)

if isinstance(src, Path) and src.name.endswith(".xlsx"):
return XLSXBook(src)
else:
return CSVBook(src)


__all__ = [ # noqa: PLE0604
Book,
Page,
book_open,
]
61 changes: 61 additions & 0 deletions nuh_helper/book_page/xlsx_book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import contextlib
import shutil
from collections.abc import Generator as gen
from pathlib import Path

import openpyxl

from .book import Book, Page


class XLSXBook(Book):
def __init__(self, file: str | Path, read_only: bool = True) -> None:
if isinstance(file, str):
file = Path(file)
if not file.is_file():
raise RuntimeError(f"xlsx file not found {file}")
self._file = file
self._book = openpyxl.load_workbook(file, read_only=read_only)

def keys(self) -> list[str]:
return [page.title for page in self._book.worksheets]

def stream_copy(self, into: str | Path) -> gen[tuple[str, int, list[str]]]:
if not isinstance(into, Path):
into = Path(into)

if not into.name.endswith(".xlsx"):
into = into / self._file.name

shutil.copy2(self._file, into)
the_copy = openpyxl.load_workbook(into)

for name in the_copy.sheetnames:
page = the_copy[name]

for r in range(page.max_row):
data = [page.cell(r + 1, c + 1).value for c in range(page.max_column)]
yield name, r, data
for c, val in enumerate(data):
if val != page.cell(r + 1, c + 1).value:
page.cell(r + 1, c + 1).value = val
the_copy.save(into)

@contextlib.contextmanager
def __getitem__(self, name: str) -> gen[Page]:
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

if len(name) > 31:
raise KeyError("excel limits you to 31 character page names")

class XLSXPage(Page):
def stream_rows(self) -> gen[tuple[int, list[str]]]:
for row in range(self._page.max_row):
yield (
row,
[
self._page.cell(row + 1, col + 1).value
for col in range(self._page.max_column)
],
)

yield XLSXPage(name, self._book[name])
Loading
Loading