Compare commits

...

2 Commits

  1. 55
      pyproject.toml
  2. BIN
      work_helpers/Vorlage UK-Abschlag, v2023-01.pdf
  3. 2
      work_helpers/__init__.py
  4. 5
      work_helpers/_natural_sort.py
  5. 14
      work_helpers/excel2changelog.py
  6. 9
      work_helpers/excel2pylist.py
  7. 230
      work_helpers/fill_forms.py
  8. 3
      work_helpers/nice_path.py
  9. 3
      work_helpers/password.py
  10. 4
      work_helpers/random_int.py
  11. 30
      work_helpers/sensospot_rename.py
  12. 12
      work_helpers/sg_mbp_build.py

55
pyproject.toml

@ -14,12 +14,18 @@ license = "Beerware" @@ -14,12 +14,18 @@ license = "Beerware"
requires = [
"pyperclip >=1.8.0",
"click >= 7.1.2",
"black",
"pandas",
"openpyxl",
"ruff",
"py-gitea",
"PyPDFForm",
]
[tool.flit.scripts]
form_inspect = "work_helpers.fill_forms:inspect"
form_prepare_payments = "work_helpers.fill_forms:prepare_payments"
form_fill_payments = "work_helpers.fill_forms:payments"
nice_path = "work_helpers.nice_path:make_nice_path"
random_password = "work_helpers.password:get_random_password"
random_ints = "work_helpers.random_int:generate_random_number_list"
@ -30,3 +36,50 @@ sg_mbp_issue_ref = "work_helpers.sg_mbp_issue:sg_mbp_issue_ref" @@ -30,3 +36,50 @@ sg_mbp_issue_ref = "work_helpers.sg_mbp_issue:sg_mbp_issue_ref"
xls2changelog = "work_helpers.excel2changelog:cli"
xls2markdown = "work_helpers.excel2markdown:cli"
xls2pylist = "work_helpers.excel2pylist:cli"
[tool.ruff]
# see https://docs.astral.sh/ruff/configuration/
line-length = 80
indent-width = 4
fix = true
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
fixable = ["ALL"]
select = ["ALL"]
ignore = [
# ignored for now, should be activated in the future
# docstrings
"D",
# don't complain about not annotating self or cls
"ANN101",
"ANN102",
# ignored, "ruff format" will handle this
"COM812",
"ISC001",
# ignored, due to Windows / WSL2 setup
# flake8-executable
"EXE",
]
[tool.ruff.lint.pydocstyle]
convention = "pep257" # Accepts: "google", "numpy", or "pep257".
[tool.ruff.lint.per-file-ignores]
# see https://github.com/charliermarsh/ruff
"tests/*" = ["FBT003", "INP001", "PLR2004", "S101", "SLF001", "ANN"]
"noxfile.py" = ["ANN"]
[tool.ruff.format]
indent-style = "space"

BIN
work_helpers/Vorlage UK-Abschlag, v2023-01.pdf

Binary file not shown.

2
work_helpers/__init__.py

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
""" Some helper scripts for the day to day work with Ubuntu in WSL2 """
"""Some helper scripts for the day to day work with Ubuntu in WSL2"""
__version__ = "0.0.2"

5
work_helpers/_natural_sort.py

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
""" Sort the content of an list in a natural way
"""Sort the content of an list in a natural way
> l = ["A2", "A10", "A1", "A3"]
> sorted(l)
@ -21,7 +21,8 @@ def _nartural_sort_convert(text): @@ -21,7 +21,8 @@ def _nartural_sort_convert(text):
def _nartural_sort_alphanum_key(text):
return tuple(
_nartural_sort_convert(part) for part in _NATURAL_SORT_REGEX_DIGITS.split(text)
_nartural_sort_convert(part)
for part in _NATURAL_SORT_REGEX_DIGITS.split(text)
)

14
work_helpers/excel2changelog.py

@ -13,13 +13,13 @@ sheet = "Input_Data_*" @@ -13,13 +13,13 @@ sheet = "Input_Data_*"
col_start = "A"
row_start = 8
def xls_formula_spacing(formula):
for c in "();=":
formula = formula.replace(c, f"{c} ")
return formula
def text_to_changelog(
raw_text, sheet, start_column, start_row, compact=False, keep=False
):
@ -55,9 +55,13 @@ def text_to_changelog( @@ -55,9 +55,13 @@ def text_to_changelog(
prev = current
def clipboard_to_changelog(sheet, start_column, start_row, compact=False, keep=False):
def clipboard_to_changelog(
sheet, start_column, start_row, compact=False, keep=False
):
xls = pyperclip.paste()
result = list(text_to_changelog(xls, sheet, start_column, start_row, compact, keep))
result = list(
text_to_changelog(xls, sheet, start_column, start_row, compact, keep)
)
line_count = len(result)
if line_count == 1:
print(f"Copied one line to the clipboard")
@ -74,7 +78,9 @@ def clipboard_to_changelog(sheet, start_column, start_row, compact=False, keep=F @@ -74,7 +78,9 @@ def clipboard_to_changelog(sheet, start_column, start_row, compact=False, keep=F
@click.command()
@click.option("-s", "--sheet", prompt=True, required=True, default="Input_Data_*")
@click.option(
"-s", "--sheet", prompt=True, required=True, default="Input_Data_*"
)
@click.option("-w", "--well", prompt=True, required=True, default="A1")
@click.option("-c", "--compact", is_flag=True)
@click.option("-k", "--keep", is_flag=True)

9
work_helpers/excel2pylist.py

@ -1,12 +1,15 @@ @@ -1,12 +1,15 @@
import click
import pyperclip
def _strip_parts(iterable):
return [item.strip() for item in iterable]
def _replace_empty_strings(iterable, replacement="None"):
return [i or replacement for i in iterable]
def prepare(text):
lines = text.splitlines()
@ -31,13 +34,17 @@ def pad_field(index, t): @@ -31,13 +34,17 @@ def pad_field(index, t):
else:
return value.ljust(length)
def pad_fields(iterable, lengths):
return [pad_field(i, t) for i, t in enumerate(zip(iterable, lengths))]
def build_list(table, lengths):
padded = (pad_fields(l, lengths) for l in table)
padded_lines = (", ".join(l) for l in padded)
lines_as_list = (f" [{l}], # noqa: E201, E202, E203, E501," for l in padded_lines)
lines_as_list = (
f" [{l}], # noqa: E201, E202, E203, E501," for l in padded_lines
)
list_content = "\n".join(lines_as_list)
return f"[\n{list_content}\n]\n"

230
work_helpers/fill_forms.py

@ -0,0 +1,230 @@ @@ -0,0 +1,230 @@
import click
from datetime import datetime
import pathlib
import sys
import shutil
import pandas as pd
import warnings
import re
from PyPDFForm import FormWrapper, PdfWrapper
warnings.filterwarnings("ignore")
Pathlike = pathlib.Path | str
WINHOME = pathlib.Path("/mnt/c/Users/Holgi/")
DESKTOP = WINHOME / "Desktop"
TODAY = datetime.now().strftime("%Y-%m-%d")
def _iso_date_to_german(date: str) -> str:
return ".".join(reversed(date.split("-")))
def _search_files(
folder: Pathlike, partial_name: str, suffix: str
) -> list[pathlib.Path]:
parent = pathlib.Path(folder)
if not suffix.startswith("."):
suffix = f".{suffix}"
files = (item for item in parent.iterdir() if item.is_file())
non_hidden = (item for item in files if not item.stem.startswith("."))
non_tempfile = (item for item in files if not item.stem.startswith("~"))
types = (
item for item in non_tempfile if item.suffix.lower() == suffix.lower()
)
return [item for item in types if partial_name.lower() in item.stem.lower()]
def _get_latest_file(
folder: Pathlike, partial_name: str, suffix: str
) -> pathlib.Path | None:
results = _search_files(folder, partial_name, suffix)
if not results:
return None
creation_times = [item.stat().st_ctime for item in results]
by_creation_time = sorted(zip(creation_times, results))
newest_with_time = by_creation_time[-1] # latest entry
return newest_with_time[1] # the path entry of the tuple
def _get_form_path(partial_name: str) -> pathlib.Path:
own_parent = pathlib.Path(__file__).parent
matches = _search_files(own_parent, partial_name, ".pdf")
if len(matches) == 1:
return matches[0]
counts = len(matches)
msg = f"Found {counts} matching pdf forms for '{partial_name}'"
raise IOError(msg)
def _get_unique(data: pd.DataFrame, column: str) -> str | int | float:
uniques = data[column].unique()
if len(uniques) != 1:
msg = f"Found multiple unique values for '{column}'"
raise ValueError(msg)
return uniques[0]
def _extract_travel_number(data: pd.DataFrame, column: str) -> str:
belege = data[column]
travel_nr = belege.apply(
lambda x: re.search("(5\d{7,8})", x.replace(" ", ""))
)
match_result = travel_nr[travel_nr.first_valid_index()]
return match_result[0]
@click.command()
@click.argument(
"form", type=click.Path(exists=True, file_okay=True, dir_okay=False)
)
@click.option(
"-o",
"--output",
default=None,
help="Output file path, defaults to desktop folder",
)
def inspect(form, output):
form = pathlib.Path(form)
if not output:
new_name = f"inspected {form.stem}{form.suffix}"
output = DESKTOP / new_name
preview_stream = PdfWrapper(str(form)).preview
with output.open("wb+") as output_stream:
output_stream.write(preview_stream)
def payments():
downloads = WINHOME / "Downloads"
forms = DESKTOP / "Formulare"
latest_export = _get_latest_file(downloads, "Buchungen_SAP", ".xlsx")
if not latest_export:
sys.exit("Could not find an SuperX export file, aborting.")
fields = ["BelegNr", "VorgängerBelegNr", "Kostenstelle", "Fonds", "Projekt"]
converters = {field: str for field in fields}
data = pd.read_excel(latest_export, skiprows=3, converters=converters)
travel_nr = _extract_travel_number(data, "BelegNr")
mask = data["Werttyp"] == "Zahlung"
payments = data[mask].copy()
summary = (
payments.groupby("BelegNr")
.agg({"Betrag": "sum", "BuDat": "first"})
.reset_index()
.sort_values("BelegNr")
)
try:
cost_center = _get_unique(payments, "Kostenstelle")
fonds = _get_unique(payments, "Fonds")
project = _get_unique(payments, "Projekt")
if not project or len(project) <= 4:
project = ""
except ValueError as e:
sys.exit(str(e))
form_data = {
"Projekt": project,
"Kostenstelle": cost_center,
"Mittelbindung": travel_nr,
"Fonds": fonds,
}
print(f"Projekt: {project}")
print(f"Kostenstelle: {cost_center}")
print(f"Mittelbindung: {travel_nr}")
print(f"Fonds: {fonds}")
print("")
print(f" Datum Betrag SuperX")
print("")
for i, row in summary.iterrows():
index = i + 1
form_data.update(
{
f"Datum{index}": row["BuDat"],
f"BelegNr aus SuberX{index}": row["BelegNr"],
# no field "Euro{index}"
# the automatic form calculation would not work.
}
)
betrag = f"{row['Betrag']:0.2f}".replace(".", ",")
print(f" {row['BuDat']} {betrag:>7} {row['BelegNr']}")
source_path = forms / "Vorlage UK-Abschlag, v2023-01.pdf"
destination_path = DESKTOP / f"{TODAY} UK-Abschlag.pdf"
form = FormWrapper(str(source_path))
filled = form.fill(form_data, flatten=False)
destination_path.write_bytes(filled.read())
@click.command()
@click.option("-l", "--search_last_name", prompt=True, required=True)
@click.option("-d", "--iso_date", prompt=True, required=True)
@click.option("-p", "--place", prompt=True, required=True)
def prepare_payments(search_last_name: str, iso_date: str, place: str):
forms_path = DESKTOP / "Formulare"
templates_path = forms_path / "vorbereitet UK-As"
templates = _search_files(templates_path, f" {search_last_name}", ".pdf")
if len(templates) == 0:
sys.exit(
f"Could not find a UK-A template for search '{search_last_name}'"
)
if len(templates) > 1:
sys.exit(
f"Found multiple UK-A templates for search '{search_last_name}'"
)
template = templates[0]
rest, first_name = template.stem.rsplit(",", maxsplit=1)
*_, last_name = rest.split()
first_name = first_name.strip()
last_name = last_name.strip()
first_last = f"{first_name} {last_name}"
last_first = f"{last_name}, {first_name}"
travel_short = f"{last_first}, {place}"
travel_name = f"{iso_date} {travel_short}"
date = _iso_date_to_german(iso_date)
folder = DESKTOP / f"{travel_name} (abgerechnet)"
folder.mkdir()
rk_path = folder / f" {travel_short}, Reisekostenabrechnung.txt"
rk_path.write_text(rk_path.stem)
uka_hint_path = (
folder / f" UK-A {last_first}, Dienstreise {place}, Schlusszahlung.txt"
)
content = "\t".join(
[
_iso_date_to_german(TODAY),
first_last,
f"Schlusszahlung Dienstreise {place}, {date}",
]
)
uka_hint_path.write_text(content)
source_path = forms_path / "Vorlage UK-Abschlag, v2023-01.pdf"
destination_path = folder / f"{TODAY} {travel_short}, UK-Abschlag.pdf"
shutil.copy(source_path, destination_path)
form = FormWrapper(str(template))
form_data = {
"Verwendungszweck": f"Schlusszahlung Dienstreise nach {place}",
"Begründung": f"Schlusszahlung Dienstreise {first_last} nach {place} am {date}",
"Datum_Feststellung": _iso_date_to_german(TODAY),
"Datum_Anordnung": _iso_date_to_german(TODAY),
}
filled = form.fill(form_data, flatten=False)
uka_path = folder / f"{TODAY} {travel_short}, UK-A Schlusszahlung.pdf"
uka_path.write_bytes(filled.read())

3
work_helpers/nice_path.py

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
""" create a nice path representation from a copied windows path """
"""create a nice path representation from a copied windows path"""
import click
import pyperclip
@ -7,6 +7,7 @@ REPLACEMENTS = { @@ -7,6 +7,7 @@ REPLACEMENTS = {
"G:": "Google Drive",
}
def replace(segment, replacements=REPLACEMENTS):
return replacements.get(segment, segment)

3
work_helpers/password.py

@ -15,7 +15,7 @@ characters = list(lowers + uppers + decimals) @@ -15,7 +15,7 @@ characters = list(lowers + uppers + decimals)
@click.command()
@click.option("--length", default=16, type=int)
def get_random_password(length=16):
""" generates a random password and copies it to the clipboard """
"""generates a random password and copies it to the clipboard"""
choice = [random.choice(characters) for i in range(length)]
groups = [iter(choice)] * 4
grouped = ("".join(g) for g in itertools.zip_longest(*groups, fillvalue=""))
@ -23,4 +23,3 @@ def get_random_password(length=16): @@ -23,4 +23,3 @@ def get_random_password(length=16):
pyperclip.copy(password)
click.echo("Copied to clipboard:", err=True)
click.echo(password)

4
work_helpers/random_int.py

@ -2,14 +2,14 @@ import click @@ -2,14 +2,14 @@ import click
import random
import pyperclip
@click.command()
@click.argument("length", default=100, type=int)
def generate_random_number_list(length=100):
""" generates a new line separated list of integers and copies it to the clipboard """
"""generates a new line separated list of integers and copies it to the clipboard"""
numbers = list(range(1, length + 1))
random.shuffle(numbers)
integer_list = "\n".join(str(i) for i in numbers)
pyperclip.copy(integer_list)
click.echo("Copied to clipboard:", err=True)
click.echo(integer_list)

30
work_helpers/sensospot_rename.py

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
""" rename sensospot images """
"""rename sensospot images"""
import click
import pathlib
@ -9,26 +9,28 @@ RENAME_MAP_NAME = "rename_map.txt" @@ -9,26 +9,28 @@ RENAME_MAP_NAME = "rename_map.txt"
IMAGE_SUFFIXES = {".tif", ".jpg"}
def get_map_file_path(directory:str) -> pathlib.Path:
def get_map_file_path(directory: str) -> pathlib.Path:
return pathlib.Path(directory) / RENAME_MAP_NAME
def list_images(directory:str) -> list[pathlib.Path]:
def list_images(directory: str) -> list[pathlib.Path]:
parent = pathlib.Path(directory)
non_hidden = (i for i in parent.iterdir() if not i.name.startswith("."))
return [i for i in non_hidden if i.suffix.lower() in IMAGE_SUFFIXES]
def get_image_parts(image_path:pathlib.Path) -> tuple[str, str, str]:
def get_image_parts(image_path: pathlib.Path) -> tuple[str, str, str]:
return image_path.stem.rsplit("_", maxsplit=2)
def get_unique_parts(images:list[pathlib.Path]) -> list[list[str], list[str], list[str]]:
def get_unique_parts(
images: list[pathlib.Path],
) -> list[list[str], list[str], list[str]]:
parts = [get_image_parts(p) for p in images]
return [sorted(set(items)) for items in zip(*parts)]
def write_rename_map(map_file:pathlib.Path) -> None:
def write_rename_map(map_file: pathlib.Path) -> None:
images = list_images(map_file.parent)
parts = get_unique_parts(images)
headers = ["stems", "wells", "exposures"]
@ -43,7 +45,7 @@ def write_rename_map(map_file:pathlib.Path) -> None: @@ -43,7 +45,7 @@ def write_rename_map(map_file:pathlib.Path) -> None:
map_file.write_text("\n".join(lines))
def _parse_rename_map(content:str) -> tuple[str, str]:
def _parse_rename_map(content: str) -> tuple[str, str]:
lines = [line.strip() for line in content.splitlines()]
for i, line in enumerate(lines, start=1):
if not line or line.startswith("#"):
@ -53,19 +55,23 @@ def _parse_rename_map(content:str) -> tuple[str, str]: @@ -53,19 +55,23 @@ def _parse_rename_map(content:str) -> tuple[str, str]:
yield line.split("\t", maxsplit=1)
def read_rename_map(map_file:pathlib.Path) -> dict[str, str]:
def read_rename_map(map_file: pathlib.Path) -> dict[str, str]:
content = map_file.read_text()
return {k: v for k, v in _parse_rename_map(content)}
def prepare_rename(images:list[pathlib.Path], rename_map:dict[str, str], sep="_") -> list[tuple[pathlib.Path, pathlib.Path]]:
def prepare_rename(
images: list[pathlib.Path], rename_map: dict[str, str], sep="_"
) -> list[tuple[pathlib.Path, pathlib.Path]]:
for path in images:
renamed_parts = [rename_map[p] for p in get_image_parts(path)]
yield path, path.with_stem(sep.join(renamed_parts))
@click.command()
@click.argument("directory", type=click.Path(exists=True, file_okay=False, dir_okay=True))
@click.argument(
"directory", type=click.Path(exists=True, file_okay=False, dir_okay=True)
)
def sensospot_rename(directory):
images = list_images(directory)
if not images:
@ -85,7 +91,9 @@ def sensospot_rename(directory): @@ -85,7 +91,9 @@ def sensospot_rename(directory):
try:
prepared = list(prepare_rename(images, rename_map))
except Exception:
raise click.UsageError("Could not rename images. Please check the image directory and rename map file.")
raise click.UsageError(
"Could not rename images. Please check the image directory and rename map file."
)
for src, dst in prepared:
click.echo(f"renaming: {src} -> {dst}")

12
work_helpers/sg_mbp_build.py

@ -122,7 +122,9 @@ def _extract_changes_from_log( @@ -122,7 +122,9 @@ def _extract_changes_from_log(
def _get_group_from_folder(folder: Pathlike) -> str:
name = pathlib.Path(folder).name
middle = name.removeprefix(GROUP_FOLDER_PREFIX).removesuffix(GROUP_FOLDER_SUFFIX)
middle = name.removeprefix(GROUP_FOLDER_PREFIX).removesuffix(
GROUP_FOLDER_SUFFIX
)
if middle in GROUPS:
return middle
msg = f"Folder '{name}' is not an MBP group folder"
@ -166,7 +168,9 @@ class Version: @@ -166,7 +168,9 @@ class Version:
## functions for `sg_mbp_build`
def copy_workbooks(group: str, destination: Pathlike, build_version: str) -> None:
def copy_workbooks(
group: str, destination: Pathlike, build_version: str
) -> None:
source = _get_workbook_folder(group)
all_xls_files = _files_in_folder(source, ".xlsx")
mbp_files = (f for f in all_xls_files if f.name.lower().startswith("mbp"))
@ -198,7 +202,9 @@ def copy_workbook_changelogs( @@ -198,7 +202,9 @@ def copy_workbook_changelogs(
shutil.copyfile(log_file, new_path)
def copy_changelog(cwd: Pathlike, destination: Pathlike, build_version: str) -> None:
def copy_changelog(
cwd: Pathlike, destination: Pathlike, build_version: str
) -> None:
changelog = _get_changelog_path(cwd)
new_path = pathlib.Path(destination) / f"CHANGELOG {build_version}.txt"
print(changelog.name, "->", new_path)

Loading…
Cancel
Save