Source code for Franko.franko

import subprocess
import json
import shutil
import os
from typing import Dict, List

from Franko.validate import NameInput


[docs]class Franko: """ A simple interface for Ukrainian name declension using shevchenko.js. Locates Node.js and the bundled JS script on initialization, and provides a generate method to decline names for different inputs. """ def __init__(self) -> None: # Locate the Node.js executable once at instance creation self.node_cmd: str = shutil.which("node") or shutil.which("nodejs") # type: ignore if not self.node_cmd: raise RuntimeError("Node.js not found. Please add 'node' to your PATH.") # Determine the path to decline.bundle.js next to this file script_dir: str = os.path.dirname(os.path.abspath(__file__)) self.bundle_path: str = os.path.join(script_dir, "decline.bundle.js") if not os.path.isfile(self.bundle_path): raise FileNotFoundError(f"decline.bundle.js not found at: {self.bundle_path}")
[docs] def generate(self, txt: str, gender: str = "masculine") -> Dict[str, str]: """ Decline the given Ukrainian full name in all grammatical cases. Args: txt: A string in the format "<surname> <given> <patronymic>". Use "-" to skip a part (e.g. "- Ivan Petrovych"). gender: Either "masculine" or "feminine". Returns: A dictionary with keys: 'nominative', 'genitive', 'dative', 'accusative', 'instrumental', 'locative', 'vocative', mapping to the declined full name. Raises: ValueError: If gender is invalid or txt is empty. RuntimeError: If Node.js execution fails. FileNotFoundError: If the JS bundle is missing. """ # Validate validated = NameInput(parts=txt, gender=gender) # Use validated.parts and validated.gender to build Node.js command cmd: List[str] = [self.node_cmd, self.bundle_path] + validated.parts + [validated.gender] # Execute the JS bundle via Node.js proc = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) if proc.returncode != 0: error_msg: str = proc.stderr.decode("utf-8", errors="replace") raise RuntimeError(f"Node.js error:\n{error_msg}") # Parse and return the JSON output output: str = proc.stdout.decode("utf-8") try: return json.loads(output) except json.JSONDecodeError: raise ValueError(f"Failed to parse JSON output:\n{output}")
if __name__ == "__main__": # Demonstrate multiple calls with one instance fr = Franko() examples: List[tuple] = [ ("123 Даніла Дмитрович", "masculine"), ("2 Тарас Григорович", "masculine"), ("Шевченко - Григорович", "masculine"), ("Косач Лариса Петрівна", "feminine") ] for text, gender in examples: print(f"\nText: '{text}', Gender: '{gender}'") try: forms = fr.generate(text, gender) for case, form in forms.items(): print(f"{case:12}: {form}") except Exception as e: print(f"Error: {e}")