Repository navigation
feat: csv shifting #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: csv shifting #149
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 ba6742d
always check sheets we're checking (for hidden dates et al)
peterlavalle-nhs ff2f4a2
detects the problem
peterlavalle-nhs 1789a14
more detail for the error message
peterlavalle-nhs 920b301
validates what it should
peterlavalle-nhs 0547569
fixes ignores to skip temp xlsx files
peterlavalle-nhs 34469d5
passes all tests again
peterlavalle-nhs 45ab14d
checks for padded cell values
peterlavalle-nhs 5250972
updates it to handle ignored values with spaces around them
peterlavalle-nhs d727465
Update log message and comments for clarity
g-pechorin 592c130
might do the dates
peterlavalle-nhs 32bc10c
Merge remote-tracking branch 'origin/shift_ignore' into shift_ignore
peterlavalle-nhs 4b4479a
more corrections
peterlavalle-nhs 19ff976
raises a specific error for missing patient columns
peterlavalle-nhs af53890
(WIP) need to digest/adapt the patient_id loading
peterlavalle-nhs 3fa2186
updates the rest of the date shift
peterlavalle-nhs 38ece50
improved function and comments
peterlavalle-nhs 7247526
uses the new exception
peterlavalle-nhs db26a0c
changes to allow pip to access book_page
peterlavalle-nhs 65de773
small logging messages
peterlavalle-nhs f470909
Merge remote-tracking branch 'origin/book_page' into book_page
peterlavalle-nhs 77245b8
more logging
peterlavalle-nhs 9e98b1e
log during id generation
peterlavalle-nhs dc1b873
remove the pd call
peterlavalle-nhs 651c457
restored it - it wasn't the problem
peterlavalle-nhs 57e1bd2
ignore the coverage database
peterlavalle-nhs 117a386
removes duplicate parameters from _inplace
peterlavalle-nhs 3fc3d9d
update docs (oops)
peterlavalle-nhs 68309d8
removes duplicate parameters from _inplace
peterlavalle-nhs 700a568
update docs (oops)
peterlavalle-nhs f3dcf66
Merge branch 'shift_ignore' into shift_ignore_yaml
peterlavalle-nhs 83c87ef
shift ignore reading from YAML
peterlavalle-nhs 5091e8e
detects old (bad) configs
peterlavalle-nhs 9441cf4
don't count tests/ when checking coverage
peterlavalle-nhs be338b1
debug logging for the ignores
peterlavalle-nhs 4215f13
tests all pass with the new yaml file approach
peterlavalle-nhs 6e70a83
Merge branch 'shift_ignore' into book_page
peterlavalle-nhs de2be87
just-enough to see if it helps
peterlavalle-nhs 1451435
test/uv updates
peterlavalle-nhs 184230e
Merge branch 'book_page' into book_stream
peterlavalle-nhs 77b5476
pointed to the live copy
peterlavalle-nhs cdc970d
WIP 93/111 tests pass
peterlavalle-nhs 817acb3
implements "row is blank" test and skipping
peterlavalle-nhs e15626d
"cheats" and switches the ambiguous date to iso8061
peterlavalle-nhs 9266d43
corrected overlap detection and testing
peterlavalle-nhs 8e04437
fixes missing patient id column detection
peterlavalle-nhs 1206f2d
added comments and checks for "skip holes"
peterlavalle-nhs 357067c
Merge remote-tracking branch 'origin/main' into book_page
peterlavalle-nhs 7801176
corrected the result of a rename
peterlavalle-nhs bc74743
fix again
peterlavalle-nhs a17e990
allow header default to 0 in patient page, and, allow date values in …
peterlavalle-nhs 17de1f4
refactored the existing functions (as is) into separate files
peterlavalle-nhs 4a5e407
brings book_page (and tests) into the project
peterlavalle-nhs 8dd5540
raise different exception
peterlavalle-nhs cde02e7
refactors the shift_delta generation to be simpler and move checking …
peterlavalle-nhs 7266555
try to separate the sheet/row checks to be a standalone thing
peterlavalle-nhs bca28c5
refactored the loop control and parameter lookups into a scanner class
peterlavalle-nhs 1fc05bf
extracts the ignore rules to a subroutine
peterlavalle-nhs 7f75ba7
idea for how to fixit
peterlavalle-nhs 0f06412
more cleanup
peterlavalle-nhs 93da71d
comments and removed a redundant check
peterlavalle-nhs fe70510
"orchestration" changes
peterlavalle-nhs e15b3c5
csv and excel workbooks now both try to parse dates to be consistent …
peterlavalle-nhs 54d8c3e
adds the hidden-date bypass to book_page
peterlavalle-nhs 09e2e8a
always allow "NULL" in date columns
peterlavalle-nhs a4eb73b
option to skip dating date
peterlavalle-nhs c7d8776
add "skip" to sheet/page types
peterlavalle-nhs c17e023
cut out normalise in a different way
peterlavalle-nhs a17deb2
renamed and documented the bypasses
peterlavalle-nhs c623097
raise exceptions on abstract invocation
peterlavalle-nhs 24bd638
Potential fix for pull request finding 'CodeQL / Clear-text logging o…
g-pechorin 780682a
renamed self to self
peterlavalle-nhs 10c96df
Merge remote-tracking branch 'origin/book_page' into book_page
peterlavalle-nhs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| CSVBook, | ||
| XLSXBook, | ||
| Book, | ||
| Page, | ||
| book_open, | ||
| ] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]]]: ... | ||
|
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]: ... | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
|
||
|
|
||
| @abstractmethod | ||
| def keys(self) -> iterator[str]: ... | ||
|
github-code-quality[bot] marked this conversation as resolved.
Fixed
github-code-quality[bot] marked this conversation as resolved.
Fixed
|
||
|
|
||
| @abstractmethod | ||
| def stream_copy(self, into: Path) -> gen[tuple[str, int, list[str]]]: ... | ||
|
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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]: | ||
|
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]) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.