Compare commits
1 Commits
main
...
multiproce
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c96606ac2 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1 @@
|
|||||||
.venv
|
.venv
|
||||||
/**/*.pyc
|
|
||||||
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -8,7 +8,7 @@
|
|||||||
"name": "Rings",
|
"name": "Rings",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "fledermaus_formatter",
|
"program": "${workspaceFolder}/__init__.py",
|
||||||
"args": ["-i", "data/ringe_short.xlsx", "-o", "ringe_short_out.xlsx", "-t", "ring"],
|
"args": ["-i", "data/ringe_short.xlsx", "-o", "ringe_short_out.xlsx", "-t", "ring"],
|
||||||
"console": "integratedTerminal"
|
"console": "integratedTerminal"
|
||||||
},
|
},
|
||||||
@ -16,7 +16,7 @@
|
|||||||
"name": "Countings",
|
"name": "Countings",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"module": "fledermaus_formatter",
|
"program": "${workspaceFolder}/__init__.py",
|
||||||
"args": ["-i", "data/zählungen_short.xlsx", "-o", "zählungen_short_out.xlsx", "-t", "counting"],
|
"args": ["-i", "data/zählungen_short.xlsx", "-o", "zählungen_short_out.xlsx", "-t", "counting"],
|
||||||
"console": "integratedTerminal"
|
"console": "integratedTerminal"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
# Fledermaus Formater
|
|
||||||
|
|
||||||
The core of this project is an interpreter which takes instructions, defined in [/instructions](/instructions/), and uses them to apply operations upon an xlsx table.
|
|
||||||
For this was developed to serve the specific use case for formatting Bat Data, the instructions needed for said operation are monolithically included in this repository at this early version of the formatter.
|
|
||||||
|
|
||||||
## Installing
|
|
||||||
|
|
||||||
1. Download and run: [install.sh](install.sh)
|
|
||||||
2. Execute the application using `fledermaus-formater` from the terminal; the app is self-documented
|
|
||||||
62
__init__.py
Normal file
62
__init__.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import argparse
|
||||||
|
from table_handler.serializer import Transformer, XslxReader
|
||||||
|
from logger import logger
|
||||||
|
|
||||||
|
import instructions.countings_formater as countings_formater
|
||||||
|
import instructions.ring_formater as ring_formater
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Serialize Excel tables.")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-i", "--input",
|
||||||
|
required=True,
|
||||||
|
help="Input Excel file"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-o", "--output",
|
||||||
|
required=True,
|
||||||
|
help="Output Excel file"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-t", "--type",
|
||||||
|
required=True,
|
||||||
|
choices=["ring", "counting"],
|
||||||
|
help="Type of table to process"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Select formatter based on type
|
||||||
|
if args.type == "ring":
|
||||||
|
formatter = ring_formater
|
||||||
|
row_instructions = ring_formater.ROW_INSTRUCTIONS
|
||||||
|
else:
|
||||||
|
formatter = countings_formater
|
||||||
|
row_instructions = countings_formater.ROW_INSTRUCTION
|
||||||
|
|
||||||
|
logger.info("Reading file, building serializer...")
|
||||||
|
data = XslxReader(args.input).serialize_table(formatter.EXPECTED_INPUT_TABLE_SCHEMA)
|
||||||
|
|
||||||
|
serializer = Transformer(
|
||||||
|
data,
|
||||||
|
formatter.EXPECTED_OUTPUT_TABLE_SCHEMA or formatter.EXPECTED_INPUT_TABLE_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Starting transformation")
|
||||||
|
serializer.run(
|
||||||
|
formatter.COL_INSTRUCTIONS,
|
||||||
|
row_instructions
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Writing transformations")
|
||||||
|
serializer.write_output(args.output)
|
||||||
|
|
||||||
|
logger.info("Done")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
__pycache__/logger.cpython-312.pyc
Normal file
BIN
__pycache__/logger.cpython-312.pyc
Normal file
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
,someone,nixLap,30.03.2026 10:01,file:///home/someone/.config/yolk/eggs/libreoffice/4;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
,someone,nixMain,31.03.2026 11:57,file:///home/someone/.config/yolk/eggs/libreoffice/4;
|
|
||||||
BIN
data/ringe.xlsx
BIN
data/ringe.xlsx
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +0,0 @@
|
|||||||
from fledermaus_formatter.main import main
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import logging
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import argparse
|
|
||||||
import os
|
|
||||||
from fledermaus_formatter.logger import logger
|
|
||||||
|
|
||||||
from fledermaus_formatter.table_handler.serializer import TableSerializer
|
|
||||||
import fledermaus_formatter.instructions.countings_formater as countings_formater
|
|
||||||
import fledermaus_formatter.instructions.ring_formater as ring_formater
|
|
||||||
|
|
||||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def process_file(input_file, output_file, xlsx_type):
|
|
||||||
# Select formatter
|
|
||||||
if xlsx_type == "ring":
|
|
||||||
formatter = ring_formater
|
|
||||||
row_instructions = ring_formater.ROW_INSTRUCTIONS
|
|
||||||
else:
|
|
||||||
formatter = countings_formater
|
|
||||||
row_instructions = countings_formater.ROW_INSTRUCTION
|
|
||||||
|
|
||||||
logger.info(f"Processing: {input_file}")
|
|
||||||
|
|
||||||
serializer = TableSerializer(
|
|
||||||
input_file,
|
|
||||||
formatter.EXPECTED_INPUT_TABLE_SCHEMA,
|
|
||||||
formatter.EXPECTED_OUTPUT_TABLE_SCHEMA
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer.run(
|
|
||||||
formatter.COL_INSTRUCTIONS,
|
|
||||||
row_instructions
|
|
||||||
)
|
|
||||||
|
|
||||||
serializer.write_output(output_file)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description="Serialize Excel tables.")
|
|
||||||
|
|
||||||
# Single file mode (optional fallback)
|
|
||||||
parser.add_argument("-i", "--input", help="Input Excel file")
|
|
||||||
parser.add_argument("-o", "--output", help="Output Excel file")
|
|
||||||
parser.add_argument("-v", help="Version", action="store_true")
|
|
||||||
|
|
||||||
# Directory mode
|
|
||||||
parser.add_argument("--input-dir", help="Input directory")
|
|
||||||
parser.add_argument("--output-dir", help="Output directory")
|
|
||||||
parser.add_argument("-s", help="Serial mode", action="store_true")
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"-t", "--type",
|
|
||||||
required=False,
|
|
||||||
choices=["ring", "counting"],
|
|
||||||
help="Type of table to process"
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.v:
|
|
||||||
print("1.5")
|
|
||||||
return
|
|
||||||
if not args.type:
|
|
||||||
parser.error("The --type argument is required.")
|
|
||||||
retur
|
|
||||||
|
|
||||||
# directory
|
|
||||||
if args.input_dir and args.output_dir:
|
|
||||||
logger.info("Running in directory mode")
|
|
||||||
|
|
||||||
tasks = []
|
|
||||||
|
|
||||||
for root, _, files in os.walk(args.input_dir):
|
|
||||||
for file in files:
|
|
||||||
if not file.endswith((".xlsx", ".xls")):
|
|
||||||
continue
|
|
||||||
|
|
||||||
input_path = os.path.join(root, file)
|
|
||||||
|
|
||||||
relative_path = os.path.relpath(root, args.input_dir)
|
|
||||||
output_root = os.path.join(args.output_dir, relative_path)
|
|
||||||
os.makedirs(output_root, exist_ok=True)
|
|
||||||
|
|
||||||
output_path = os.path.join(output_root, file)
|
|
||||||
|
|
||||||
tasks.append((input_path, output_path, args.type))
|
|
||||||
|
|
||||||
max_workers = os.cpu_count() or 1 if not args.s else 1
|
|
||||||
logger.info(f"Processing {len(tasks)} files using {max_workers} workers")
|
|
||||||
|
|
||||||
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
|
||||||
futures = [executor.submit(process_file, *task) for task in tasks]
|
|
||||||
|
|
||||||
for future in as_completed(futures):
|
|
||||||
try:
|
|
||||||
future.result()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error processing file: {e}")
|
|
||||||
# single file
|
|
||||||
elif args.input and args.output:
|
|
||||||
logger.info("Running in single file mode")
|
|
||||||
process_file(args.input, args.output, args.type)
|
|
||||||
|
|
||||||
else:
|
|
||||||
parser.error("You must provide either input/output files or input-dir/output-dir")
|
|
||||||
|
|
||||||
logger.info("Done")
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Entry:
|
|
||||||
value: str|int|float|bool|None
|
|
||||||
bg_color: tuple[str, str, str] = (255,255,255)
|
|
||||||
fg_color: tuple[str, str, str] = (255,255,255)
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
from fledermaus_formatter.table_handler.Entry import Entry
|
|
||||||
|
|
||||||
class Copy:
|
|
||||||
def __init__(self, source: str, target: str, replace = False):
|
|
||||||
self.source = source
|
|
||||||
self.target = target
|
|
||||||
self.replace = replace
|
|
||||||
|
|
||||||
def execute_on_row(self, row=dict[str, str], source_col=None):
|
|
||||||
return {
|
|
||||||
**({} if not self.replace else {self.source_col: Entry(value="")}),
|
|
||||||
self.target: Entry(row[self.source].value, bg_color=row[self.source].bg_color, fg_color=row[self.source].fg_color)
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
from fledermaus_formatter.table_handler.Entry import Entry
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
class FindAndReplace:
|
|
||||||
def __init__(self, map: dict[str, list[str]], reqire_full_match: bool = False):
|
|
||||||
self.map = map
|
|
||||||
self.require_full_match = reqire_full_match
|
|
||||||
|
|
||||||
def execute_on_cell(self, entry) -> str:
|
|
||||||
res = deepcopy(entry.value)
|
|
||||||
|
|
||||||
for correct_name, bad_names in self.map.items():
|
|
||||||
for bad_name in bad_names:
|
|
||||||
if self.require_full_match and bad_name == res:
|
|
||||||
res = correct_name
|
|
||||||
break
|
|
||||||
elif not self.require_full_match:
|
|
||||||
prior_val = res
|
|
||||||
res = res.replace(bad_name, correct_name)
|
|
||||||
if prior_val != res or correct_name in res:
|
|
||||||
break
|
|
||||||
return Entry(value=res, fg_color=entry.fg_color, bg_color=entry.bg_color)
|
|
||||||
22
install.sh
22
install.sh
@ -1,22 +0,0 @@
|
|||||||
#!/usr/share/env bash
|
|
||||||
|
|
||||||
# Target dir is config vecause this will never include binary blops
|
|
||||||
TARGET_DIR="$HOME/.config/fledermaus-formatter"
|
|
||||||
if [ -d $TARGET_DIR ]; then
|
|
||||||
rm -rf "${TARGET_DIR}"
|
|
||||||
fi
|
|
||||||
mkdir -p $TARGET_DIR
|
|
||||||
git clone git@git.urmel.de:Kyattsukuro/fledermaus.git "$TARGET_DIR"
|
|
||||||
python3 -m venv $TARGET_DIR/.venv
|
|
||||||
source $TARGET_DIR/.venv/bin/activate
|
|
||||||
pip install -r $TARGET_DIR/requirements.txt
|
|
||||||
|
|
||||||
mkdir -p $HOME/.local/share/bin/
|
|
||||||
|
|
||||||
rm -r $HOME/.local/share/bin/fledermaus-formater > /dev/zero
|
|
||||||
ln -s $TARGET_DIR/run.sh $HOME/.local/share/bin/fledermaus-formater
|
|
||||||
|
|
||||||
if [[ ":$PATH:" != *":$HOME/.local/share/bin:"* ]]; then
|
|
||||||
echo 'export PATH="$HOME/.local/share/bin:$PATH"' >> ~/.bashrc
|
|
||||||
export PATH="$HOME/.local/share/bin:$PATH"
|
|
||||||
fi
|
|
||||||
BIN
instructions/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
instructions/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
instructions/__pycache__/countings_formater.cpython-312.pyc
Normal file
BIN
instructions/__pycache__/countings_formater.cpython-312.pyc
Normal file
Binary file not shown.
BIN
instructions/__pycache__/mappings.cpython-312.pyc
Normal file
BIN
instructions/__pycache__/mappings.cpython-312.pyc
Normal file
Binary file not shown.
BIN
instructions/__pycache__/ring_formater.cpython-312.pyc
Normal file
BIN
instructions/__pycache__/ring_formater.cpython-312.pyc
Normal file
Binary file not shown.
@ -19,40 +19,31 @@ EXPECTED_OUTPUT_TABLE_SCHEMA.remove("Geschlecht")
|
|||||||
COL_INSTRUCTIONS = {
|
COL_INSTRUCTIONS = {
|
||||||
("Art",): {
|
("Art",): {
|
||||||
"command": "FIND_AND_REPLACE",
|
"command": "FIND_AND_REPLACE",
|
||||||
"map": SPICIES,
|
"map": SPICIES
|
||||||
"reqire_full_match": True
|
|
||||||
},
|
},
|
||||||
("Ort",): [
|
("Ort",): {
|
||||||
{
|
|
||||||
"command": "FIND_AND_REPLACE",
|
|
||||||
"map": CATCHED_BY
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "REGEX_MOVE_TO_COL",
|
"command": "REGEX_MOVE_TO_COL",
|
||||||
"with_input_iteration": list(CATCHED_BY.keys()), # Moves matching group {{% 1 %}} to "to"
|
"with_input_iteration": CATCHED_BY, # Moves matching group {{% 0 %}} to "to"
|
||||||
"match": r"^.*({{% 0 %}}),?(?:.*+)*$",
|
"match": r"^(?:[^ ]* +)*({{% 0 %}}),?(?: .*+)*$",
|
||||||
"to": "Fangart",
|
"to": "Fangart"
|
||||||
"copy": True
|
},
|
||||||
}
|
("Anzahl",): {
|
||||||
],
|
|
||||||
("Anzahl",): [{
|
|
||||||
"command": "REGEX_EDIT_COL",
|
"command": "REGEX_EDIT_COL",
|
||||||
"match": r"^ *$",
|
"match": r"^ *$",
|
||||||
"replace_with": "1",
|
"replace_with": "1"
|
||||||
"match_none": True
|
|
||||||
},
|
},
|
||||||
{
|
("Anzahl",): {
|
||||||
"command": "REGEX_GROUP_VALUEBASED_CHOOS",
|
"command": "REGEX_GROUP_VALUEBASED_CHOOS",
|
||||||
"match": r"^ *([0-9]+) *oder *([0-9]+) *$",
|
"match": r"^ *([0-9]+) *oder *([0-9]+) *$",
|
||||||
"highest": True
|
"highest": True
|
||||||
}],
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ROW_INSTRUCTION = [
|
ROW_INSTRUCTION = [
|
||||||
{
|
{
|
||||||
"command": "CONDITIONAL_COPY",
|
"command": "CONDITIONAL_COPY",
|
||||||
"when_regex": {
|
"when_regex": {
|
||||||
"Geschlecht": r"^.*(m),?(?:.*+)*$",
|
"Geschlecht": r"^(?:[^ ]* +)*(m),?(?: .*+)*$",
|
||||||
"Anzahl": r"([0-9]+)"
|
"Anzahl": r"([0-9]+)"
|
||||||
},
|
},
|
||||||
"source": "Anzahl",
|
"source": "Anzahl",
|
||||||
@ -61,7 +52,7 @@ ROW_INSTRUCTION = [
|
|||||||
{
|
{
|
||||||
"command": "CONDITIONAL_COPY",
|
"command": "CONDITIONAL_COPY",
|
||||||
"when_regex": {
|
"when_regex": {
|
||||||
"Geschlecht": r"^.*(w),?(?:.*+)*$",
|
"Geschlecht": r"^(?:[^ ]* +)*(w),?(?: .*+)*$",
|
||||||
"Anzahl": r"([0-9]+)"
|
"Anzahl": r"([0-9]+)"
|
||||||
},
|
},
|
||||||
"source": "Anzahl",
|
"source": "Anzahl",
|
||||||
@ -1,17 +1,3 @@
|
|||||||
def flatten_dict(d):
|
|
||||||
flat = {}
|
|
||||||
for key, values in d.items():
|
|
||||||
for value in values:
|
|
||||||
flat[value] = key
|
|
||||||
return flat
|
|
||||||
|
|
||||||
def add_cased_options(inp: str):
|
|
||||||
return set([
|
|
||||||
inp,
|
|
||||||
inp.lower(),
|
|
||||||
inp.upper(),
|
|
||||||
inp.capitalize(),
|
|
||||||
])
|
|
||||||
SPICIES = {
|
SPICIES = {
|
||||||
"Bbar": [
|
"Bbar": [
|
||||||
"B.bar.",
|
"B.bar.",
|
||||||
@ -20,7 +6,6 @@ SPICIES = {
|
|||||||
"bbar",
|
"bbar",
|
||||||
"Barbastella",
|
"Barbastella",
|
||||||
"barbastellus",
|
"barbastellus",
|
||||||
"Bbararar",
|
|
||||||
"B.b.",
|
"B.b.",
|
||||||
"B.b",
|
"B.b",
|
||||||
"BB",
|
"BB",
|
||||||
@ -52,7 +37,6 @@ SPICIES = {
|
|||||||
"P.pip.",
|
"P.pip.",
|
||||||
"P.pip",
|
"P.pip",
|
||||||
"PPIP",
|
"PPIP",
|
||||||
"P.p.",
|
|
||||||
"ppip",
|
"ppip",
|
||||||
"Pipistrellus pipistrellus",
|
"Pipistrellus pipistrellus",
|
||||||
],
|
],
|
||||||
@ -171,7 +155,6 @@ SPICIES = {
|
|||||||
"P.nat.",
|
"P.nat.",
|
||||||
"P.nat",
|
"P.nat",
|
||||||
"PNAT",
|
"PNAT",
|
||||||
"Pnatatat",
|
|
||||||
"pnat",
|
"pnat",
|
||||||
"Pipistrellus nathusii",
|
"Pipistrellus nathusii",
|
||||||
"P.n.",
|
"P.n.",
|
||||||
@ -194,7 +177,6 @@ SPICIES = {
|
|||||||
"unbekannt",
|
"unbekannt",
|
||||||
"unb.",
|
"unb.",
|
||||||
"Unbestimmt",
|
"Unbestimmt",
|
||||||
"Mspecbech",
|
|
||||||
],
|
],
|
||||||
"Malc": [
|
"Malc": [
|
||||||
"M.alc.",
|
"M.alc.",
|
||||||
@ -245,7 +227,6 @@ SPICIES = {
|
|||||||
"MSPEC",
|
"MSPEC",
|
||||||
"mspec",
|
"mspec",
|
||||||
"Myotis",
|
"Myotis",
|
||||||
"Mspec.Mspec",
|
|
||||||
"M.spe.",
|
"M.spe.",
|
||||||
"M.spe",
|
"M.spe",
|
||||||
"MSPE",
|
"MSPE",
|
||||||
@ -270,7 +251,6 @@ SPICIES = {
|
|||||||
"MMYO",
|
"MMYO",
|
||||||
"mmyo",
|
"mmyo",
|
||||||
"Myotis Myotis",
|
"Myotis Myotis",
|
||||||
"Mspec.bech",
|
|
||||||
"M.m.",
|
"M.m.",
|
||||||
"M.m",
|
"M.m",
|
||||||
"MM",
|
"MM",
|
||||||
@ -312,41 +292,30 @@ SPICIES = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
CATCHED_BY = {
|
CATCHED_BY = [
|
||||||
"Nfg": [*add_cased_options("nfg"),
|
"Nfg",
|
||||||
*add_cased_options("nf")],
|
"WQK",
|
||||||
"KK": [*add_cased_options("kk"),
|
"Wst",
|
||||||
*add_cased_options(" k "),
|
"KK",
|
||||||
*add_cased_options("überwinterungskästen"),
|
"AFZ",
|
||||||
*add_cased_options("kastenkontrolle"),
|
"Detektor",
|
||||||
*add_cased_options("kasten")],
|
]
|
||||||
"WQK": [*add_cased_options("wqk"),
|
|
||||||
*add_cased_options("Winterquartierkontrolle"),
|
|
||||||
*add_cased_options("Winterquartierkasten"),
|
|
||||||
*add_cased_options("Winterquartier"),
|
|
||||||
*add_cased_options("kontrolle")],
|
|
||||||
"Wst": [*add_cased_options("wst"),
|
|
||||||
*add_cased_options("Wochenstuben"),
|
|
||||||
*add_cased_options("Wochenstube")],
|
|
||||||
"AFZ": [*add_cased_options("afz")],
|
|
||||||
"Detektor": [*add_cased_options("detektor")],
|
|
||||||
}
|
|
||||||
|
|
||||||
AGE = {
|
AGE = {
|
||||||
"j": [
|
"j": [
|
||||||
"j",
|
"j",
|
||||||
"juv",
|
"juv"
|
||||||
],
|
],
|
||||||
"sub": [
|
"sub": [
|
||||||
"sub",
|
"sub",
|
||||||
"subad",
|
"subad",
|
||||||
"sub ad",
|
"sub ad"
|
||||||
"sub adult",
|
"sub adult",
|
||||||
"sub-adult",
|
"sub-adult"
|
||||||
],
|
],
|
||||||
"a": [
|
"a": [
|
||||||
"a",
|
"a",
|
||||||
"ad",
|
"ad"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,15 +323,21 @@ REPRODUCTION = {
|
|||||||
"p": [
|
"p": [
|
||||||
"plak",
|
"plak",
|
||||||
"pl",
|
"pl",
|
||||||
"p",
|
"p"
|
||||||
],
|
],
|
||||||
"g": [
|
"g": [
|
||||||
"g",
|
"g"
|
||||||
],
|
],
|
||||||
"z": [
|
"z": [
|
||||||
"lak",
|
"lak",
|
||||||
"l",
|
"l",
|
||||||
"z",
|
"z"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def flatten_dict(d):
|
||||||
|
flat = {}
|
||||||
|
for key, values in d.items():
|
||||||
|
for value in values:
|
||||||
|
flat[value] = key
|
||||||
|
return flat
|
||||||
@ -23,24 +23,30 @@ COL_INSTRUCTIONS = {
|
|||||||
("Geschlecht",): [
|
("Geschlecht",): [
|
||||||
{
|
{
|
||||||
"command": "FIND_AND_REPLACE",
|
"command": "FIND_AND_REPLACE",
|
||||||
"map": REPRODUCTION,
|
"map": REPRODUCTION
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "FIND_AND_REPLACE",
|
"command": "FIND_AND_REPLACE",
|
||||||
"map": AGE,
|
"map": AGE
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "REGEX_MOVE_TO_COL",
|
"command": "REGEX_MOVE_TO_COL",
|
||||||
"with_input_iteration": list(REPRODUCTION.keys()), # Moves matching group {{% 1 %}} to "to"
|
"with_input_iteration": list(REPRODUCTION.keys()), # Moves matching group {{% 1 %}} to "to"
|
||||||
"match": r"^.*({{% 0 %}}),?(?:.*+)*$",
|
"match": r"^(?:[^ ]* +)*({{% 0 %}}),?(?: .*+)*$",
|
||||||
"to": "Reproduktionsstatus"
|
"to": "Reproduktionsstatus"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "REGEX_MOVE_TO_COL",
|
"command": "REGEX_MOVE_TO_COL",
|
||||||
"with_input_iteration": list(AGE.keys()), # Moves matching group {{% 1 %}} to "to"
|
"with_input_iteration": list(AGE.keys()), # Moves matching group {{% 1 %}} to "to"
|
||||||
"match": r"^.*({{% 0 %}}),?(?:.*+)*$",
|
"match": r"^(?:[^ ]* +)*({{% 0 %}}),?(?: .*+)*$",
|
||||||
"to": "Alter"
|
"to": "Alter"
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# Special case ma -> Geschlecht a | Alter a
|
||||||
|
"command": "REGEX_MOVE_TO_COL",
|
||||||
|
"match": r"^(?:[^ ]* +)*m(a),?(?: .*+)*$",
|
||||||
|
"to": "Alter"
|
||||||
}],
|
}],
|
||||||
("Ringnummer",): {
|
("Ringnummer",): {
|
||||||
"command": "REGEX_EDIT_COL",
|
"command": "REGEX_EDIT_COL",
|
||||||
@ -50,21 +56,14 @@ COL_INSTRUCTIONS = {
|
|||||||
},
|
},
|
||||||
("Art",): {
|
("Art",): {
|
||||||
"command": "FIND_AND_REPLACE",
|
"command": "FIND_AND_REPLACE",
|
||||||
"map": SPICIES,
|
"map": SPICIES
|
||||||
"reqire_full_match": True
|
|
||||||
},
|
},
|
||||||
("Ort",): [
|
("Ort",): {
|
||||||
{
|
|
||||||
"command": "FIND_AND_REPLACE",
|
|
||||||
"map": CATCHED_BY
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "REGEX_MOVE_TO_COL",
|
"command": "REGEX_MOVE_TO_COL",
|
||||||
"with_input_iteration": list(CATCHED_BY.keys()), # Moves matching group {{% 1 %}} to "to"
|
"with_input_iteration": CATCHED_BY, # Moves matching group {{% 1 %}} to "to"
|
||||||
"match": r"^.*({{% 0 %}}),?(?:.*+)*$",
|
"match": r"^(?:[^ ]* +)*({{% 0 %}}),?(?: .*+)*$",
|
||||||
"to": "Fangart"
|
"to": "Fangart"
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -74,8 +73,11 @@ ROW_INSTRUCTIONS = [
|
|||||||
"command": "ROW_COLOR_TO_BOOL", # Match when found as text color in any col of the row
|
"command": "ROW_COLOR_TO_BOOL", # Match when found as text color in any col of the row
|
||||||
"fg_colors": [(255,0,0), (0,0,255)],
|
"fg_colors": [(255,0,0), (0,0,255)],
|
||||||
"approximate": True,
|
"approximate": True,
|
||||||
"threshold": "ANY", # ALL, ANY, MAJORITY of row must be collored
|
"threshold": "ALL", # ALL, ANY, MAJORITY of row must be collored
|
||||||
"target": "Wiederfunde",
|
"target": "Wiederfunde",
|
||||||
"value": "WAHR",
|
"value": "TRUE",
|
||||||
}
|
}
|
||||||
|
#{
|
||||||
|
# "command": "ROW_COPY_REMAINING"
|
||||||
|
#}
|
||||||
]
|
]
|
||||||
10
logger.py
Normal file
10
logger.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
def lock_logger(q):
|
||||||
|
while True:
|
||||||
|
msg = q.get()
|
||||||
|
if msg == "DONE":
|
||||||
|
break
|
||||||
|
logger.info(msg)
|
||||||
Binary file not shown.
7
run.sh
7
run.sh
@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
SCRIPT_DIR="$HOME/.config/fledermaus-formatter"
|
|
||||||
|
|
||||||
source $SCRIPT_DIR/.venv/bin/activate
|
|
||||||
export PYTHONPATH=$SCRIPT_DIR:$PYTHONPATH
|
|
||||||
$SCRIPT_DIR/.venv/bin/python3 -m fledermaus_formatter $@
|
|
||||||
BIN
table_handler/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
table_handler/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
table_handler/__pycache__/serializer.cpython-312.pyc
Normal file
BIN
table_handler/__pycache__/serializer.cpython-312.pyc
Normal file
Binary file not shown.
BIN
table_handler/commands/__pycache__/copy.cpython-312.pyc
Normal file
BIN
table_handler/commands/__pycache__/copy.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
13
table_handler/commands/copy.py
Normal file
13
table_handler/commands/copy.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
class Copy:
|
||||||
|
def __init__(self, source: str, target: str, replace = False):
|
||||||
|
self.source = source
|
||||||
|
self.target = target
|
||||||
|
self.replace = replace
|
||||||
|
|
||||||
|
def execute_on_row(self, row=dict[str, str], source_col=None):
|
||||||
|
return {
|
||||||
|
**({} if not self.replace else {self.source_col: ""}),
|
||||||
|
self.target: row[self.source].value
|
||||||
|
}
|
||||||
20
table_handler/commands/find_and_replace.py
Normal file
20
table_handler/commands/find_and_replace.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import re
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
class FindAndReplace:
|
||||||
|
def __init__(self, map: dict[str, list[str]], case_sensitive: bool = False):
|
||||||
|
self.map = map
|
||||||
|
self.case_sensitive = case_sensitive
|
||||||
|
|
||||||
|
def execute_on_cell(self, entry) -> str:
|
||||||
|
res = deepcopy(entry.value)
|
||||||
|
|
||||||
|
for species, alternatives in self.map.items():
|
||||||
|
for old_version in alternatives:
|
||||||
|
if self.case_sensitive:
|
||||||
|
res = res.replace(old_version, species)
|
||||||
|
else:
|
||||||
|
pattern = re.compile(re.escape(old_version), re.IGNORECASE)
|
||||||
|
res = pattern.sub(species, res)
|
||||||
|
|
||||||
|
return res
|
||||||
@ -1,11 +1,10 @@
|
|||||||
import regex
|
import regex
|
||||||
|
|
||||||
class RegexEditCol:
|
class RegexEditCol:
|
||||||
def __init__(self, match, replace_with, matching_group_rearange: bool = False, match_none: bool = False):
|
def __init__(self, match, replace_with, matching_group_rearange: bool = False):
|
||||||
self.match = match
|
self.match = match
|
||||||
self.replace_with = replace_with
|
self.replace_with = replace_with
|
||||||
self.matching_group_rearange = matching_group_rearange
|
self.matching_group_rearange = matching_group_rearange
|
||||||
self.match_none = match_none
|
|
||||||
|
|
||||||
def _reorder(self, value: str):
|
def _reorder(self, value: str):
|
||||||
matches = regex.match(self.match, value)
|
matches = regex.match(self.match, value)
|
||||||
@ -19,12 +18,9 @@ class RegexEditCol:
|
|||||||
def execute_on_cell(self, entry) -> str:
|
def execute_on_cell(self, entry) -> str:
|
||||||
value = entry.value
|
value = entry.value
|
||||||
if value is None:
|
if value is None:
|
||||||
if self.match_none:
|
return value
|
||||||
entry.value = self.replace_with
|
|
||||||
return entry
|
|
||||||
|
|
||||||
if self.matching_group_rearange:
|
if self.matching_group_rearange:
|
||||||
entry.value = self._reorder(value)
|
return self._reorder(value)
|
||||||
else:
|
|
||||||
entry.value = regex.sub(self.match, self.replace_with, value)
|
return regex.sub(self.match, self.replace_with, value)
|
||||||
return entry
|
|
||||||
@ -15,8 +15,7 @@ class RegexGroupValuebasedChoose:
|
|||||||
if not len(values):
|
if not len(values):
|
||||||
return None
|
return None
|
||||||
if self.highest:
|
if self.highest:
|
||||||
entry.value = str(values[-1])
|
return str(values[-1])
|
||||||
else:
|
else:
|
||||||
entry.value = str(values[0])
|
return str(values[0])
|
||||||
return entry
|
|
||||||
|
|
||||||
@ -1,12 +1,10 @@
|
|||||||
import regex
|
import regex
|
||||||
import re
|
import re
|
||||||
from fledermaus_formatter.table_handler.Entry import Entry
|
|
||||||
|
|
||||||
class RegexMoveToCol:
|
class RegexMoveToCol:
|
||||||
def __init__(self, match: str, to: str, with_input_iteration: list[list[str]]|None = None, copy: bool = False):
|
def __init__(self, match: str, to: str, with_input_iteration: list[str]|None = None):
|
||||||
self.match = match
|
self.match = match
|
||||||
self.to = to
|
self.to = to
|
||||||
self.copy = copy
|
|
||||||
|
|
||||||
if with_input_iteration is not None:
|
if with_input_iteration is not None:
|
||||||
with_input_iteration = (
|
with_input_iteration = (
|
||||||
@ -54,28 +52,25 @@ class RegexMoveToCol:
|
|||||||
return regex.sub(repl, value)
|
return regex.sub(repl, value)
|
||||||
|
|
||||||
def do_changes(self, source_coll: str, used_regex: str, row: dict[str, str], changes={}):
|
def do_changes(self, source_coll: str, used_regex: str, row: dict[str, str], changes={}):
|
||||||
current_to = changes.get(self.to, row[source_coll]).value
|
current_to = changes.get(self.to, row[source_coll].value)
|
||||||
current_source = changes.get(source_coll, row[source_coll]).value
|
current_source = changes.get(source_coll, row[source_coll].value)
|
||||||
|
|
||||||
pending_res_to = self._get_merged_groups(used_regex, current_to)
|
pending_res_to = self._get_merged_groups(used_regex, current_to)
|
||||||
if pending_res_to is not None:
|
if pending_res_to is not None:
|
||||||
changes[self.to] = Entry(value=pending_res_to, fg_color=row[self.to].fg_color, bg_color=row[self.to].bg_color)
|
changes[self.to] = pending_res_to
|
||||||
changes[source_coll] = Entry(value=self._remove_regex_groups(used_regex, current_source), fg_color=row[source_coll].fg_color, bg_color=row[source_coll].bg_color)
|
changes[source_coll] = self._remove_regex_groups(used_regex, current_source)
|
||||||
return changes
|
return changes
|
||||||
|
|
||||||
def execute_on_row(self, row: dict[str, str], source_coll: str = None):
|
def execute_on_row(self, row: dict[str, str], source_coll: str = None):
|
||||||
if not source_coll:
|
if not source_coll:
|
||||||
raise ValueError("source_col required")
|
raise ValueError("source_col required")
|
||||||
changes = {}
|
|
||||||
|
|
||||||
if self.with_input_iteration:
|
if self.with_input_iteration:
|
||||||
|
changes = {}
|
||||||
for iteration in self.with_input_iteration:
|
for iteration in self.with_input_iteration:
|
||||||
used_regex = self.match
|
used_regex = self.match
|
||||||
for operation_id in range(0, self.__operation_input_depth):
|
for operation_id in range(0, self.__operation_input_depth):
|
||||||
used_regex = used_regex.replace(f"{{{{% {operation_id} %}}}}", iteration[operation_id])
|
used_regex = used_regex.replace(f"{{{{% {operation_id} %}}}}", iteration[operation_id])
|
||||||
changes = self.do_changes(source_coll, used_regex, row, changes)
|
changes = self.do_changes(source_coll, used_regex, row, changes)
|
||||||
else:
|
|
||||||
changes = self.do_changes(source_coll, self.match, row)
|
|
||||||
if self.copy and source_coll in changes:
|
|
||||||
del changes[source_coll]
|
|
||||||
return changes
|
return changes
|
||||||
|
else:
|
||||||
|
return self.do_changes(source_coll, self.match, row)
|
||||||
@ -1,12 +1,3 @@
|
|||||||
from fledermaus_formatter.table_handler.Entry import Entry
|
|
||||||
from math import sqrt
|
|
||||||
|
|
||||||
def div_from_color(given_rgb, reqiuired_rgb):
|
|
||||||
r, g, b = given_rgb
|
|
||||||
cr, cg, cb = reqiuired_rgb
|
|
||||||
color_diff = sqrt((r - cr)**2 + (g - cg)**2 + (b - cb)**2)
|
|
||||||
return color_diff
|
|
||||||
|
|
||||||
class RowColorToCalue:
|
class RowColorToCalue:
|
||||||
def __init__(self, target: str,
|
def __init__(self, target: str,
|
||||||
value: str,
|
value: str,
|
||||||
@ -22,9 +13,9 @@ class RowColorToCalue:
|
|||||||
self.fg_color = fg_colors if fg_colors else []
|
self.fg_color = fg_colors if fg_colors else []
|
||||||
self.bg_color = bg_colors if bg_colors else []
|
self.bg_color = bg_colors if bg_colors else []
|
||||||
|
|
||||||
self.value = value
|
self.value = value,
|
||||||
self.target = target
|
self.target = target,
|
||||||
self.approximate = approximate
|
self.approximate = approximate,
|
||||||
|
|
||||||
if threshold not in ("ALL", "ANY", "MAJORITY"):
|
if threshold not in ("ALL", "ANY", "MAJORITY"):
|
||||||
raise ValueError("threshold must be ALL, ANY or MAJORITY")
|
raise ValueError("threshold must be ALL, ANY or MAJORITY")
|
||||||
@ -32,16 +23,12 @@ class RowColorToCalue:
|
|||||||
|
|
||||||
|
|
||||||
def _check_if_collection_matchers(self, collectin: dict[tuple[int, int, int]], color_place: str) -> bool:
|
def _check_if_collection_matchers(self, collectin: dict[tuple[int, int, int]], color_place: str) -> bool:
|
||||||
threshold_sq = 80
|
|
||||||
|
|
||||||
res_map = []
|
res_map = []
|
||||||
if self.approximate:
|
if self.approximate:
|
||||||
|
margin = 10
|
||||||
for color in collectin:
|
for color in collectin:
|
||||||
is_match = any(
|
for allowed_color in self.__dict__[color_place]:
|
||||||
div_from_color(color, allowed_color) <= threshold_sq
|
res_map.append(all(abs(a - b) <= margin for a, b in zip(color, allowed_color)))
|
||||||
for allowed_color in self.__dict__[color_place]
|
|
||||||
)
|
|
||||||
res_map.append(is_match)
|
|
||||||
else:
|
else:
|
||||||
res_map = list(map(lambda x: x in self.__dict__[color_place]))
|
res_map = list(map(lambda x: x in self.__dict__[color_place]))
|
||||||
|
|
||||||
@ -57,9 +44,9 @@ class RowColorToCalue:
|
|||||||
def execute_on_row(self, row: dict[str, str], source_coll: str|None = None):
|
def execute_on_row(self, row: dict[str, str], source_coll: str|None = None):
|
||||||
color_pace_results = []
|
color_pace_results = []
|
||||||
for color_place in ["fg_color", "bg_color"]:
|
for color_place in ["fg_color", "bg_color"]:
|
||||||
color_collection = (map(lambda c: c.__dict__[color_place], filter(lambda x: x.value, row.values())))
|
color_collection = (map(lambda c: c.__dict__[color_place], row.values()))
|
||||||
color_pace_results.append(self._check_if_collection_matchers(color_collection, color_place))
|
color_pace_results.append(self._check_if_collection_matchers(color_collection, color_place))
|
||||||
|
|
||||||
if any(color_pace_results):
|
if all(color_pace_results):
|
||||||
return {self.target: Entry(value=self.value, fg_color=row[self.target].fg_color, bg_color=row[self.target].bg_color)}
|
return {self.target: self.value}
|
||||||
return {}
|
return {}
|
||||||
BIN
table_handler/conditions/__pycache__/regex_cell.cpython-312.pyc
Normal file
BIN
table_handler/conditions/__pycache__/regex_cell.cpython-312.pyc
Normal file
Binary file not shown.
@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
from openpyxl import Workbook, load_workbook
|
from openpyxl import Workbook, load_workbook
|
||||||
|
from dataclasses import dataclass
|
||||||
from pprint import pprint as print
|
from pprint import pprint as print
|
||||||
|
|
||||||
import fledermaus_formatter.instructions.mappings as mappings
|
from table_handler.utils import hex_to_rgb, delegate_tasks
|
||||||
from fledermaus_formatter.table_handler.Entry import Entry
|
from logger import lock_logger
|
||||||
|
|
||||||
from .commands.find_and_replace import FindAndReplace
|
from .commands.find_and_replace import FindAndReplace
|
||||||
from .commands.regex_edit_col import RegexEditCol
|
from .commands.regex_edit_col import RegexEditCol
|
||||||
@ -11,34 +12,27 @@ from .commands.regex_move_to_col import RegexMoveToCol
|
|||||||
from .commands.copy import Copy
|
from .commands.copy import Copy
|
||||||
from .commands.regex_group_valuebased_choose import RegexGroupValuebasedChoose
|
from .commands.regex_group_valuebased_choose import RegexGroupValuebasedChoose
|
||||||
from .commands.row_color_to_value import RowColorToCalue
|
from .commands.row_color_to_value import RowColorToCalue
|
||||||
from fledermaus_formatter.logger import logger
|
|
||||||
|
|
||||||
from .conditions.regex_cell import RegexCellCondition
|
from .conditions.regex_cell import RegexCellCondition
|
||||||
|
|
||||||
def hex_to_rgb(hex):
|
from multiprocessing import Pool, Queue, Process, Manager, cpu_count
|
||||||
if type(hex) != str and str("Values must be of type <class 'str'>"):
|
|
||||||
# chatch wierd openxl things
|
|
||||||
hex = "#0"
|
|
||||||
hex= "{:06}".format(str(hex).lstrip("#"))
|
|
||||||
return tuple(int(hex[i:i+2],16) for i in (0, 2, 4))
|
|
||||||
|
|
||||||
class TableSerializer:
|
@dataclass
|
||||||
def __init__(self, file: str, expected_input_schema: list[str], expected_output_schema: list[str]):
|
class Entry:
|
||||||
|
value: str|int|float|bool|None
|
||||||
|
bg_color: tuple[str, str, str]
|
||||||
|
fg_color: tuple[str, str, str]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class XslxReader:
|
||||||
|
def __init__(self, file):
|
||||||
self.wb: Workbook = load_workbook(file, read_only=True)
|
self.wb: Workbook = load_workbook(file, read_only=True)
|
||||||
self.ws = self.wb.active
|
self.ws = self.wb.active
|
||||||
if not self.ws:
|
if not self.ws:
|
||||||
raise ValueError("No active worksheet found in the workbook.")
|
raise ValueError("No active worksheet found in the workbook.")
|
||||||
|
|
||||||
self.file = file
|
def serialize_table(self, expected_cols: list[str]) -> dict[int, list[Entry]]:
|
||||||
self.data = self._serialize_table(expected_input_schema)
|
|
||||||
self.serialized_output = {}
|
|
||||||
self.seperator = ", "
|
|
||||||
self.expected_output_schema = expected_output_schema or expected_input_schema
|
|
||||||
self.implicit_copy = True
|
|
||||||
self.output_header = True
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_table(self, expected_cols: list[str]) -> dict[int, list[Entry]]:
|
|
||||||
data = {col: {} for col in expected_cols}
|
data = {col: {} for col in expected_cols}
|
||||||
map_expected_with_ws = {}
|
map_expected_with_ws = {}
|
||||||
for idx, cell in enumerate(self.ws[1]):
|
for idx, cell in enumerate(self.ws[1]):
|
||||||
@ -55,14 +49,25 @@ class TableSerializer:
|
|||||||
for expected_col, ws_idx in map_expected_with_ws.items():
|
for expected_col, ws_idx in map_expected_with_ws.items():
|
||||||
cell = row[ws_idx]
|
cell = row[ws_idx]
|
||||||
data[expected_col][rid] = Entry(
|
data[expected_col][rid] = Entry(
|
||||||
value=str(cell._value) if not cell.value is None else "",
|
value=str(cell.value) if not cell.value is None else "",
|
||||||
bg_color=hex_to_rgb(cell.fill.end_color.rgb[2:] if cell.fill and cell.fill.start_color else "#0"),
|
bg_color=hex_to_rgb(cell.fill.start_color.rgb if cell.fill and cell.fill.start_color else "#0"),
|
||||||
fg_color=hex_to_rgb(cell.font.color.rgb[2:] if cell.font and cell.font.color and type(cell.font.color.rgb) == str else "#0")
|
fg_color=hex_to_rgb(cell.font.color.rgb if cell.font and cell.font.color else "#0")
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
class Transformer:
|
||||||
|
def __init__(self, data, expected_output_schema: list[str], cumulated_instructions = {}):
|
||||||
|
self.data = data
|
||||||
|
self.fully_serialized_output = {}
|
||||||
|
self.seperator = ", "
|
||||||
|
self.expected_output_schema = expected_output_schema
|
||||||
|
self.implicit_copy = True
|
||||||
|
self.output_header = True
|
||||||
|
self.cumulated_instructions = cumulated_instructions
|
||||||
|
|
||||||
|
|
||||||
def write_output(self, destination_file: str):
|
def write_output(self, destination_file: str):
|
||||||
if not self.serialized_output:
|
if not self.fully_serialized_output:
|
||||||
raise ValueError("There is nothing to write, serialize first")
|
raise ValueError("There is nothing to write, serialize first")
|
||||||
|
|
||||||
# write header
|
# write header
|
||||||
@ -74,9 +79,9 @@ class TableSerializer:
|
|||||||
ows.cell(row=1, column=col_i, value=col_name)
|
ows.cell(row=1, column=col_i, value=col_name)
|
||||||
|
|
||||||
# write rows
|
# write rows
|
||||||
for row_i, (row_id, row_data) in enumerate(self.serialized_output.items(), start=2 if self.output_header else 1):
|
for row_i, (row_id, row_data) in enumerate(self.fully_serialized_output.items(), start=2 if self.output_header else 1):
|
||||||
for col_i, col_name in enumerate(self.expected_output_schema, start=1):
|
for col_i, col_name in enumerate(self.expected_output_schema, start=1):
|
||||||
ows.cell(row=row_i, column=col_i, value=row_data.get(col_name).value if row_data.get(col_name) else "")
|
ows.cell(row=row_i, column=col_i, value=row_data.get(col_name))
|
||||||
|
|
||||||
owb.save(destination_file)
|
owb.save(destination_file)
|
||||||
|
|
||||||
@ -96,29 +101,27 @@ class TableSerializer:
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def _extecute_instruct(self, source_cell: str, instruct: dict, row: dict[str, any], working_row: dict[str, any]):
|
def _extecute_instruct(self, source_cell: str, instruct: dict, row: dict[str, any], working_row: dict[str, any]):
|
||||||
in_place_working_row = {**row, **working_row}
|
|
||||||
|
|
||||||
# execute changes
|
# execute changes
|
||||||
res = None
|
res = None
|
||||||
res_row = {}
|
res_row = {}
|
||||||
match instruct.get("command"):
|
match instruct.get("command"):
|
||||||
case "FIND_AND_REPLACE":
|
case "FIND_AND_REPLACE":
|
||||||
res = FindAndReplace(instruct["map"], instruct.get("reqire_full_match", False)).execute_on_cell(in_place_working_row[source_cell])
|
res = FindAndReplace(instruct["map"]).execute_on_cell(row[source_cell])
|
||||||
case "REGEX_MOVE_TO_COL":
|
case "REGEX_MOVE_TO_COL":
|
||||||
res_row = RegexMoveToCol(instruct["match"], instruct["to"], instruct.get("with_input_iteration"), instruct.get("copy", False)) \
|
res_row = RegexMoveToCol(instruct["match"], instruct["to"], instruct.get("with_input_iteration")) \
|
||||||
.execute_on_row(in_place_working_row, source_cell)
|
.execute_on_row(row, source_cell)
|
||||||
case "REGEX_EDIT_COL":
|
case "REGEX_EDIT_COL":
|
||||||
res = RegexEditCol(instruct["match"], instruct["replace_with"], instruct.get("matching_group_rearange"), instruct.get("match_none"))\
|
res = RegexEditCol(instruct["match"], instruct["replace_with"], instruct.get("matching_group_rearange"))\
|
||||||
.execute_on_cell(in_place_working_row[source_cell])
|
.execute_on_cell(row[source_cell])
|
||||||
case "REGEX_GROUP_VALUEBASED_CHOOS":
|
case "REGEX_GROUP_VALUEBASED_CHOOS":
|
||||||
res = RegexGroupValuebasedChoose(instruct["match"], instruct["highest"])\
|
res = RegexGroupValuebasedChoose(instruct["match"], instruct["highest"])\
|
||||||
.execute_on_cell(in_place_working_row[source_cell])
|
.execute_on_cell(row[source_cell])
|
||||||
case "CONDITIONAL_COPY":
|
case "CONDITIONAL_COPY":
|
||||||
res_row = Copy(instruct["source"], instruct["target"], replace=instruct.get("replace"))\
|
res_row = Copy(instruct["source"], instruct["target"], replace=instruct.get("replace"))\
|
||||||
.execute_on_row(in_place_working_row)
|
.execute_on_row(row)
|
||||||
case "ROW_COLOR_TO_BOOL":
|
case "ROW_COLOR_TO_BOOL":
|
||||||
res_row = RowColorToCalue(instruct["target"], instruct["value"], instruct.get("fg_colors"), instruct.get("bg_colors"), instruct.get("approximate"), instruct.get("threshold"))\
|
res_row = RowColorToCalue(instruct["target"], instruct["value"], instruct.get("fg_colors"), instruct.get("bg_colors"), instruct.get("approximate"), instruct.get("threshold"))\
|
||||||
.execute_on_row(in_place_working_row)
|
.execute_on_row(row)
|
||||||
|
|
||||||
# Merge row_changes to working_row
|
# Merge row_changes to working_row
|
||||||
for target_cel, change in res_row.items():
|
for target_cel, change in res_row.items():
|
||||||
@ -129,6 +132,28 @@ class TableSerializer:
|
|||||||
working_row[source_cell] = self._merge_change_to_cel(working_row.get(source_cell), res)
|
working_row[source_cell] = self._merge_change_to_cel(working_row.get(source_cell), res)
|
||||||
return working_row
|
return working_row
|
||||||
|
|
||||||
|
def _run_row_resolution(self, range, worker: str = "X", q = None):
|
||||||
|
self.fully_serialized_output = {}
|
||||||
|
end = list(range)[-1]
|
||||||
|
for rid in range:
|
||||||
|
if rid % 100 == 0 and q:
|
||||||
|
q.put(f"Worker {worker}:Working on row {rid}/{end}")
|
||||||
|
working_row = {}
|
||||||
|
source_row = self.get_row_as_dict(rid)
|
||||||
|
for source_cols, instructions in self.cumulated_instructions:
|
||||||
|
for instruct in instructions if type(instructions) == list else [instructions]:
|
||||||
|
for col in source_cols:
|
||||||
|
if not self._check_conditions(instruct, source_row):
|
||||||
|
continue
|
||||||
|
working_row = self._extecute_instruct(col, instruct, source_row, working_row)
|
||||||
|
if not self.implicit_copy:
|
||||||
|
continue
|
||||||
|
for col in self. expected_output_schema:
|
||||||
|
if col in working_row:
|
||||||
|
continue
|
||||||
|
working_row[col] = source_row.get(col).value
|
||||||
|
self.fully_serialized_output[rid] = working_row
|
||||||
|
return self.fully_serialized_output
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def vertical_len(self):
|
def vertical_len(self):
|
||||||
@ -138,25 +163,27 @@ class TableSerializer:
|
|||||||
return {col: rows.get(rid) for col, rows in self.data.items()}
|
return {col: rows.get(rid) for col, rows in self.data.items()}
|
||||||
|
|
||||||
def run(self, col_instructions: dict[tuple[str], dict], row_instructions: dict[tuple[str], dict]):
|
def run(self, col_instructions: dict[tuple[str], dict], row_instructions: dict[tuple[str], dict]):
|
||||||
self.serialized_output = {}
|
self.cumulated_instructions = list(col_instructions.items()) + [
|
||||||
amount_working_rows = self.vertical_len +2
|
|
||||||
for rid in range(2, amount_working_rows):
|
|
||||||
if rid % 1000 == 0:
|
|
||||||
logger.info(f"{self.file} Working on row {rid}/{amount_working_rows} ")
|
|
||||||
working_row = {}
|
|
||||||
source_row = self.get_row_as_dict(rid)
|
|
||||||
for source_cols, instructions in list(col_instructions.items()) + [
|
|
||||||
((None,), instr) for instr in row_instructions
|
((None,), instr) for instr in row_instructions
|
||||||
]:
|
]
|
||||||
for instruct in instructions if type(instructions) == list else [instructions]:
|
amount_working_rows = self.vertical_len + 2
|
||||||
for col in source_cols:
|
|
||||||
if not self._check_conditions(instruct, source_row):
|
with Manager() as manager:
|
||||||
continue
|
q = manager.Queue()
|
||||||
working_row = self._extecute_instruct(col, instruct, source_row, working_row)
|
|
||||||
if not self.implicit_copy:
|
chnucs = [[elem, worker_name, q] for worker_name, elem in enumerate(
|
||||||
continue
|
delegate_tasks(range(2, amount_working_rows), cpu_count(), 1000)
|
||||||
for col in self.expected_output_schema:
|
)]
|
||||||
if col in working_row:
|
|
||||||
continue
|
log_proc = Process(target=lock_logger, args=(q,))
|
||||||
working_row[col] = source_row.get(col)
|
log_proc.start()
|
||||||
self.serialized_output[rid] = working_row
|
|
||||||
|
with Pool(len(chnucs)) as p:
|
||||||
|
result = p.starmap(
|
||||||
|
Transformer(self.data, self.expected_output_schema, self.cumulated_instructions)._run_row_resolution,
|
||||||
|
chnucs
|
||||||
|
)
|
||||||
|
q.put("DONE")
|
||||||
|
log_proc.join(1)
|
||||||
|
self.fully_serialized_output = {k: v for d in result for k, v in d.items()}
|
||||||
|
return
|
||||||
20
table_handler/utils.py
Normal file
20
table_handler/utils.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
def hex_to_rgb(hex):
|
||||||
|
if type(hex) != str and str("Values must be of type <class 'str'>"):
|
||||||
|
# chatch wierd openxl things
|
||||||
|
hex = "#0"
|
||||||
|
hex= "{:06}".format(str(hex).lstrip("#"))
|
||||||
|
return tuple(int(hex[i:i+2],16) for i in (0, 2, 4))
|
||||||
|
|
||||||
|
def delegate_tasks(tasks, threats, min_tasks_per_threat):
|
||||||
|
n = len(tasks)
|
||||||
|
|
||||||
|
# determine usable cores
|
||||||
|
usable_cores = min(threats, n // min_tasks_per_threat)
|
||||||
|
|
||||||
|
if usable_cores == 0:
|
||||||
|
usable_cores = 1
|
||||||
|
|
||||||
|
# split tasks evenly
|
||||||
|
chunk_size = (n + usable_cores - 1) // usable_cores
|
||||||
|
chunks = [tasks[i:i + chunk_size] for i in range(0, n, chunk_size)]
|
||||||
|
return chunks
|
||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user