Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ src/.settings/org.eclipse.core.resources.prefs
src/.settings/org.eclipse.core.resources.prefs
src/jhora/data/geonames_places_5k.csv
src/jhora/data/geonames_places_5k_IN.csv

# pyjhora_batch: local build output, and batch input/output holding real
# birth details — never commit personal data.
.DS_Store
__pycache__/
/build
/reports
/partners.csv
/partners.csv.orig
28 changes: 28 additions & 0 deletions generate_one.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Single-record smoke test for the batch wrapper.

Demonstrates pyjhora_batch.generate_pdf: one record in, one PDF out, headless.
Unlike driving ChartTabbed's constructor directly, the birth details below are
actually honored (the constructor kwargs are not — see wrapper.py).
"""

from pyjhora_batch import generate_pdf

RECORD = {
"name": "Test Person",
"date_of_birth": "1985,6,15", # yyyy,m,d
"time_of_birth": "10:30:00", # hh:mm:ss (24h)
"place_name": "Ujjain",
"latitude": 23.5,
"longitude": 75.75,
"timezone": 5.5, # hours from UTC
"gender": "male",
}


def main():
out = generate_pdf(RECORD, "reports/test.pdf")
print(f"Done. Wrote {out}")


if __name__ == "__main__":
main()
372 changes: 372 additions & 0 deletions pyjhora_batch/README.md

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions pyjhora_batch/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""pyjhora_batch — a thin batch layer on top of PyJHora.

Reuses PyJHora's astrology engine; never reimplements calculations. Each record
can produce three artifacts:

* ``report_data.build_report`` extracts the whole horoscope as structured data
(no Qt), which ``text_writer`` and ``pdf_writer`` render as a ``.txt`` and a
vector ``.pdf``.
* ``jhd_writer.write_jhd`` writes the Jagannatha Hora ``.jhd`` seed file.
* ``wrapper.generate_pdf`` is the legacy screenshot PDF (drives the Qt GUI).

``engine.run_csv`` / ``engine.run_batch`` drive all of it over many records.
"""

from .wrapper import BirthRecord, RecordError, ensure_app, generate_pdf
from .jhd_writer import build_jhd, write_jhd
from .report_data import Report, Section, build_report
from .text_writer import render_text, write_text
from .engine import (PDF_MODES, BatchSummary, RecordResult, run_batch, run_csv,
run_excel, default_worker_count)

__all__ = [
"BirthRecord", "RecordError", "ensure_app", "generate_pdf",
"build_jhd", "write_jhd",
"Report", "Section", "build_report",
"render_text", "write_text",
"PDF_MODES", "BatchSummary", "RecordResult", "run_batch", "run_csv",
"run_excel", "default_worker_count",
]


def __getattr__(name):
# pdf_writer imports reportlab; keep that optional for .txt/.jhd-only users.
if name in ("render_pdf", "write_pdf_report"):
from . import pdf_writer
return getattr(pdf_writer, name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
8 changes: 8 additions & 0 deletions pyjhora_batch/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Enable `python -m pyjhora_batch ...`."""

import sys

from .cli import main

if __name__ == "__main__":
sys.exit(main())
127 changes: 127 additions & 0 deletions pyjhora_batch/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Command-line interface for the batch layer.

python -m pyjhora_batch input.csv -o reports/ --workers auto

Auto-detects CSV vs Excel by file extension and dispatches to the engine.
Running as a module puts the repo root on sys.path, which the 'spawn' worker
processes need to re-import the package.
"""

from __future__ import annotations

import argparse
import sys
from pathlib import Path

from .engine import PDF_MODES, default_worker_count, run_csv, run_excel

_EXCEL_SUFFIXES = {".xlsx", ".xlsm", ".xltx", ".xltm"}
_CSV_SUFFIXES = {".csv", ".tsv", ".txt"}


def _parse_workers(value: str) -> int:
if value == "auto":
return default_worker_count()
try:
n = int(value)
except ValueError:
raise argparse.ArgumentTypeError(f"--workers must be an integer or 'auto' (got {value!r})")
if n < 1:
raise argparse.ArgumentTypeError("--workers must be >= 1")
return n


def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
prog="pyjhora_batch",
description="Batch-generate horoscope PDFs (and importable .jhd files) "
"from a CSV or Excel file of birth details.",
)
p.add_argument("input", type=Path,
help="Input file (.csv/.tsv or .xlsx). Columns: name, "
"date_of_birth (YYYY,M,D), time_of_birth (HH:MM:SS), place, "
"latitude, longitude, timezone, gender (aliases accepted).")
p.add_argument("-o", "--out-dir", type=Path, default=Path("reports"),
help="Output directory (default: reports).")
p.add_argument("-w", "--workers", type=_parse_workers, default=1, metavar="N",
help="Worker processes: an integer, or 'auto' (cpus-1). Default: 1.")
p.add_argument("--sheet", default=None,
help="Excel worksheet name or 0-based index (default: active sheet).")
p.add_argument("--no-pdf", action="store_true", help="Skip PDF generation.")
p.add_argument("--no-jhd", action="store_true", help="Skip .jhd generation.")
p.add_argument("--no-txt", action="store_true",
help="Skip the plain-text report.")
p.add_argument("--pdf-mode", choices=PDF_MODES, default="vector",
help="vector: typeset the data as real text — sharp, searchable, "
"~150 KB (default). screenshot: PyJHora's Qt widget capture — "
"includes the drawn chart diagrams but is ~130 DPI JPEG, ~10 MB.")
p.add_argument("--dhasa", action="append", metavar="NAME", dest="dhasas",
help="Dhasa system to include (repeatable). Default: vimsottari. "
"Use '--dhasa all' for all 60 systems PyJHora exposes "
"(adds ~8500 rows). "
"e.g. --dhasa vimsottari --dhasa ashtottari")
p.add_argument("--expand-all-tabs", action="store_true",
help="Expand all chart tabs in the PDF (screenshot mode only).")
p.add_argument("-q", "--quiet", action="store_true",
help="Only print warnings/errors and the final summary.")
p.add_argument("--allow-failures", action="store_true",
help="Exit 0 even if some records fail (default: exit 1 on any failure).")
return p


def _resolve_sheet(sheet):
if sheet is None:
return None
try:
return int(sheet)
except (TypeError, ValueError):
return sheet


def main(argv=None) -> int:
args = build_parser().parse_args(argv)

if not args.input.is_file():
print(f"error: input file not found: {args.input}", file=sys.stderr)
return 2
if args.no_pdf and args.no_jhd and args.no_txt:
print("error: --no-pdf, --no-jhd and --no-txt together produce no output.",
file=sys.stderr)
return 2

suffix = args.input.suffix.lower()
common = dict(
verbose=not args.quiet,
workers=args.workers,
write_pdf=not args.no_pdf,
write_jhd_file=not args.no_jhd,
write_txt=not args.no_txt,
pdf_mode=args.pdf_mode,
dhasas=tuple(args.dhasas) if args.dhasas else None,
expand_all_tabs=True if args.expand_all_tabs else None,
)

if suffix in _EXCEL_SUFFIXES:
summary = run_excel(args.input, args.out_dir, sheet=_resolve_sheet(args.sheet), **common)
elif suffix in _CSV_SUFFIXES:
summary = run_csv(args.input, args.out_dir, **common)
else:
print(f"error: unsupported input type {suffix!r}; use a .csv or .xlsx file.",
file=sys.stderr)
return 2

print(f"\n{summary}")
print(f"Output: {args.out_dir}")
if summary.failures:
print(f"Failures: {len(summary.failures)} (see {args.out_dir / 'failures.csv'} "
f"and {args.out_dir / 'batch.log'})")
for r in summary.failures:
print(f" row {r.row_number} [{r.status}] {r.name or ''}: {r.error}")

if summary.failed and not args.allow_failures:
return 1
return 0


if __name__ == "__main__":
sys.exit(main())
Loading