abax 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- abax/__init__.py +4 -0
- abax/__main__.py +6 -0
- abax/_runtime.py +119 -0
- abax/app.py +239 -0
- abax/autodeps.py +221 -0
- abax/console_worker.py +117 -0
- abax/core/__init__.py +33 -0
- abax/core/archive.py +133 -0
- abax/core/arrayfuncs.py +197 -0
- abax/core/ast_nodes.py +69 -0
- abax/core/budget.py +171 -0
- abax/core/calc/__init__.py +1 -0
- abax/core/calc/algebraic.py +530 -0
- abax/core/calc/rpn.py +350 -0
- abax/core/calc/rpn12.py +476 -0
- abax/core/calc/rpn16.py +516 -0
- abax/core/calc/ti_engine.py +265 -0
- abax/core/calc/voyager.py +140 -0
- abax/core/calc/voyager_layout.py +123 -0
- abax/core/cells.py +51 -0
- abax/core/clipboard.py +221 -0
- abax/core/completion.py +301 -0
- abax/core/console_ns.py +211 -0
- abax/core/criteria.py +92 -0
- abax/core/engineering_fns.py +648 -0
- abax/core/errors.py +52 -0
- abax/core/evaluator.py +197 -0
- abax/core/faceplate_assets.py +175 -0
- abax/core/fileops.py +221 -0
- abax/core/filesearch.py +101 -0
- abax/core/fill.py +196 -0
- abax/core/finance_fns.py +784 -0
- abax/core/fmbuttons.py +133 -0
- abax/core/fonts.py +77 -0
- abax/core/format/__init__.py +1 -0
- abax/core/format/ansipalette.py +60 -0
- abax/core/format/cellformat.py +64 -0
- abax/core/format/cellstyle.py +100 -0
- abax/core/format/colormap.py +44 -0
- abax/core/format/condformat.py +233 -0
- abax/core/functions/__init__.py +216 -0
- abax/core/functions/builtins.py +1520 -0
- abax/core/functions/helpers.py +167 -0
- abax/core/functions/rf.py +184 -0
- abax/core/goalseek.py +177 -0
- abax/core/graphing.py +271 -0
- abax/core/io/__init__.py +1 -0
- abax/core/io/adif_io.py +151 -0
- abax/core/io/csv_io.py +76 -0
- abax/core/io/csv_stream.py +301 -0
- abax/core/io/exchange_io.py +169 -0
- abax/core/io/flatfile_io.py +223 -0
- abax/core/io/html_report.py +104 -0
- abax/core/io/markdown_io.py +142 -0
- abax/core/io/notebook_io.py +165 -0
- abax/core/io/r_io.py +184 -0
- abax/core/io/sqlite_io.py +180 -0
- abax/core/io/urlfetch.py +177 -0
- abax/core/io/xml_io.py +142 -0
- abax/core/latexmath.py +282 -0
- abax/core/math_fns.py +693 -0
- abax/core/names.py +166 -0
- abax/core/navigation.py +120 -0
- abax/core/pandoc.py +68 -0
- abax/core/parser.py +162 -0
- abax/core/pivot.py +288 -0
- abax/core/precedents.py +127 -0
- abax/core/profile.py +175 -0
- abax/core/ptyterm.py +325 -0
- abax/core/r1c1.py +116 -0
- abax/core/recode.py +398 -0
- abax/core/reference.py +87 -0
- abax/core/reffuncs.py +165 -0
- abax/core/richdisplay.py +79 -0
- abax/core/science/__init__.py +1 -0
- abax/core/science/antenna.py +172 -0
- abax/core/science/antenna_impedance.py +139 -0
- abax/core/science/bayes.py +317 -0
- abax/core/science/chartsvg.py +298 -0
- abax/core/science/cluster.py +307 -0
- abax/core/science/complexnum.py +234 -0
- abax/core/science/dxcc.py +486 -0
- abax/core/science/eigen.py +404 -0
- abax/core/science/fft.py +154 -0
- abax/core/science/filters.py +314 -0
- abax/core/science/financial.py +597 -0
- abax/core/science/gmm.py +326 -0
- abax/core/science/interp.py +196 -0
- abax/core/science/iq.py +109 -0
- abax/core/science/matrix.py +229 -0
- abax/core/science/metrics.py +305 -0
- abax/core/science/ml.py +396 -0
- abax/core/science/mom.py +193 -0
- abax/core/science/nec.py +209 -0
- abax/core/science/numeric.py +192 -0
- abax/core/science/ode.py +267 -0
- abax/core/science/ode_implicit.py +361 -0
- abax/core/science/regression.py +189 -0
- abax/core/science/resynth.py +243 -0
- abax/core/science/rf.py +359 -0
- abax/core/science/rf_bands.py +81 -0
- abax/core/science/rf_math.py +175 -0
- abax/core/science/signal.py +243 -0
- abax/core/science/spectral.py +217 -0
- abax/core/science/stats.py +632 -0
- abax/core/science/trees.py +378 -0
- abax/core/science/units.py +252 -0
- abax/core/science/wire_mom.py +277 -0
- abax/core/search.py +161 -0
- abax/core/series.py +133 -0
- abax/core/sheet.py +404 -0
- abax/core/shell.py +118 -0
- abax/core/sortfilter.py +181 -0
- abax/core/sqlsheets.py +202 -0
- abax/core/stats_dist.py +1081 -0
- abax/core/structure.py +233 -0
- abax/core/text_datetime_fns.py +536 -0
- abax/core/tokenizer.py +101 -0
- abax/core/translate.py +87 -0
- abax/core/typeinfer.py +156 -0
- abax/core/undo.py +89 -0
- abax/core/validation.py +188 -0
- abax/core/values.py +53 -0
- abax/core/wbdiff.py +107 -0
- abax/core/workbook.py +180 -0
- abax/diagnostics.py +139 -0
- abax/engine/__init__.py +16 -0
- abax/engine/analysis.py +699 -0
- abax/engine/document.py +168 -0
- abax/engine/excel_io.py +97 -0
- abax/engine/nbvalidate.py +81 -0
- abax/engine/necpy.py +148 -0
- abax/engine/npkernel.py +63 -0
- abax/engine/ods_io.py +222 -0
- abax/engine/parquet_io.py +134 -0
- abax/gui/__init__.py +5 -0
- abax/gui/_qtcompat.py +232 -0
- abax/gui/calc/__init__.py +1 -0
- abax/gui/calc/algebraic_faceplate.py +114 -0
- abax/gui/calc/calculator_panel.py +220 -0
- abax/gui/calc/faceplate.py +488 -0
- abax/gui/calc/image_faceplate.py +245 -0
- abax/gui/calc/ti_faceplate.py +731 -0
- abax/gui/command_palette.py +154 -0
- abax/gui/completion.py +53 -0
- abax/gui/console/__init__.py +1 -0
- abax/gui/console/console_bridge.py +102 -0
- abax/gui/console/ptyterminal.py +185 -0
- abax/gui/console/pyconsole.py +244 -0
- abax/gui/console/terminal.py +83 -0
- abax/gui/dialogs/__init__.py +1 -0
- abax/gui/dialogs/antenna_dialog.py +220 -0
- abax/gui/dialogs/budget_dialog.py +170 -0
- abax/gui/dialogs/clipboard_dialog.py +102 -0
- abax/gui/dialogs/condformat_dialog.py +93 -0
- abax/gui/dialogs/dataframe_dialog.py +117 -0
- abax/gui/dialogs/deps_dialog.py +151 -0
- abax/gui/dialogs/equation_dialog.py +106 -0
- abax/gui/dialogs/filemanager_dialog.py +541 -0
- abax/gui/dialogs/filter_dialog.py +90 -0
- abax/gui/dialogs/find_dialog.py +146 -0
- abax/gui/dialogs/formula_browser.py +67 -0
- abax/gui/dialogs/goalseek_dialog.py +87 -0
- abax/gui/dialogs/graph_dialog.py +522 -0
- abax/gui/dialogs/matrix_dialog.py +108 -0
- abax/gui/dialogs/ml_dialog.py +166 -0
- abax/gui/dialogs/name_manager_dialog.py +63 -0
- abax/gui/dialogs/ode_dialog.py +104 -0
- abax/gui/dialogs/pivot_dialog.py +126 -0
- abax/gui/dialogs/recode_dialog.py +130 -0
- abax/gui/dialogs/rf_dialog.py +160 -0
- abax/gui/dialogs/rf_reference_dialog.py +113 -0
- abax/gui/dialogs/signal_dialog.py +209 -0
- abax/gui/dialogs/smith_dialog.py +169 -0
- abax/gui/dialogs/solver_dialog.py +88 -0
- abax/gui/dialogs/sort_dialog.py +77 -0
- abax/gui/dialogs/sql_dialog.py +105 -0
- abax/gui/dialogs/stats_dialog.py +175 -0
- abax/gui/dialogs/theme_dialog.py +74 -0
- abax/gui/dialogs/undo_history_dialog.py +55 -0
- abax/gui/dialogs/validation_dialog.py +90 -0
- abax/gui/grid/__init__.py +1 -0
- abax/gui/grid/frozen_panes.py +127 -0
- abax/gui/grid/grid_model.py +211 -0
- abax/gui/grid/grid_view.py +312 -0
- abax/gui/icons.py +359 -0
- abax/gui/main_window.py +823 -0
- abax/gui/mixin_calc.py +120 -0
- abax/gui/mixin_console.py +92 -0
- abax/gui/mixin_document.py +698 -0
- abax/gui/mixin_io.py +265 -0
- abax/gui/mixin_macros.py +124 -0
- abax/gui/mixin_navigation.py +60 -0
- abax/gui/mixin_palette.py +156 -0
- abax/gui/mixin_settings.py +20 -0
- abax/gui/mixin_tools.py +433 -0
- abax/gui/mixin_view.py +163 -0
- abax/gui/runner.py +73 -0
- abax/gui/themes/high_contrast.qss +57 -0
- abax/gui/themes/light.qss +53 -0
- abax/gui/themes/obsidian.qss +53 -0
- abax/gui/theming.py +145 -0
- abax/kernel.py +154 -0
- abax/macros.py +183 -0
- abax/recorder.py +237 -0
- abax/settings.py +103 -0
- abax/state.py +58 -0
- abax/tui/__init__.py +34 -0
- abax/tui/app.py +53 -0
- abax/tui/capabilities.py +27 -0
- abax/tui/commands.py +21 -0
- abax/tui/editor.py +617 -0
- abax/tui/keys.py +86 -0
- abax/tui/render.py +205 -0
- abax/tui/themes.py +112 -0
- abax/widget.py +131 -0
- abax/workers.py +82 -0
- abax-0.1.2.dist-info/METADATA +366 -0
- abax-0.1.2.dist-info/RECORD +223 -0
- abax-0.1.2.dist-info/WHEEL +5 -0
- abax-0.1.2.dist-info/entry_points.txt +3 -0
- abax-0.1.2.dist-info/licenses/LICENSE +674 -0
- abax-0.1.2.dist-info/top_level.txt +1 -0
abax/__init__.py
ADDED
abax/__main__.py
ADDED
abax/_runtime.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Runtime detection: platform paths, optional-dependency booleans, version.
|
|
2
|
+
|
|
3
|
+
Imported widely, so it stays cheap: no Qt, no curses, no heavy work at import.
|
|
4
|
+
Mirrors the spec's _runtime.py exactly, parameterized for project "abax".
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
APP_NAME = "abax"
|
|
14
|
+
|
|
15
|
+
# --- optional dependency flags --------------------------------------------
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import msgspec as _ms # noqa: F401
|
|
19
|
+
|
|
20
|
+
_HAS_MSGSPEC = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
_ms = None
|
|
23
|
+
_HAS_MSGSPEC = False
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import platformdirs as _pd_mod # noqa: F401
|
|
27
|
+
|
|
28
|
+
_HAS_PLATFORMDIRS = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
_pd_mod = None
|
|
31
|
+
_HAS_PLATFORMDIRS = False
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
import openpyxl as _openpyxl # noqa: F401
|
|
35
|
+
|
|
36
|
+
_HAS_OPENPYXL = True
|
|
37
|
+
except ImportError:
|
|
38
|
+
_openpyxl = None
|
|
39
|
+
_HAS_OPENPYXL = False
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
import importlib.util as _ilu
|
|
43
|
+
|
|
44
|
+
# GUI works on either Qt binding; PySide6 (LGPL) is preferred. Detect via
|
|
45
|
+
# find_spec so the fast paths never import a heavy Qt stack just to check.
|
|
46
|
+
_HAS_QT = (_ilu.find_spec("PySide6") is not None
|
|
47
|
+
or _ilu.find_spec("PyQt6") is not None)
|
|
48
|
+
except Exception:
|
|
49
|
+
_HAS_QT = False
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
import textual # noqa: F401
|
|
53
|
+
|
|
54
|
+
_HAS_TEXTUAL = True
|
|
55
|
+
except ImportError:
|
|
56
|
+
_HAS_TEXTUAL = False
|
|
57
|
+
|
|
58
|
+
# --- version flags ---------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
PY_VERSION = sys.version_info
|
|
61
|
+
HAS_LAZY_IMPORTS = PY_VERSION >= (3, 15) # PEP 810
|
|
62
|
+
|
|
63
|
+
# --- platform paths --------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
if _HAS_PLATFORMDIRS:
|
|
66
|
+
from platformdirs import PlatformDirs as _PD
|
|
67
|
+
|
|
68
|
+
_dirs = _PD(APP_NAME, appauthor=False)
|
|
69
|
+
CONFIG_DIR = Path(_dirs.user_config_dir)
|
|
70
|
+
DATA_DIR = Path(_dirs.user_data_dir)
|
|
71
|
+
CACHE_DIR = Path(_dirs.user_cache_dir)
|
|
72
|
+
LOG_DIR = Path(_dirs.user_log_dir)
|
|
73
|
+
else:
|
|
74
|
+
# stdlib fallback — mirrors platformdirs logic.
|
|
75
|
+
_home = Path.home()
|
|
76
|
+
if sys.platform == "win32":
|
|
77
|
+
_base = Path(os.environ.get("APPDATA", _home / "AppData/Roaming"))
|
|
78
|
+
_local = Path(os.environ.get("LOCALAPPDATA", _home / "AppData/Local"))
|
|
79
|
+
CONFIG_DIR = _base / APP_NAME
|
|
80
|
+
DATA_DIR = _local / APP_NAME
|
|
81
|
+
CACHE_DIR = _local / APP_NAME / "Cache"
|
|
82
|
+
LOG_DIR = _local / APP_NAME / "Logs"
|
|
83
|
+
elif sys.platform == "darwin":
|
|
84
|
+
_sup = _home / "Library" / "Application Support" / APP_NAME
|
|
85
|
+
CONFIG_DIR = _sup
|
|
86
|
+
DATA_DIR = _sup
|
|
87
|
+
CACHE_DIR = _home / "Library" / "Caches" / APP_NAME
|
|
88
|
+
LOG_DIR = _home / "Library" / "Logs" / APP_NAME
|
|
89
|
+
else:
|
|
90
|
+
_cfg = Path(os.environ.get("XDG_CONFIG_HOME", _home / ".config"))
|
|
91
|
+
_data = Path(os.environ.get("XDG_DATA_HOME", _home / ".local/share"))
|
|
92
|
+
_cache = Path(os.environ.get("XDG_CACHE_HOME", _home / ".cache"))
|
|
93
|
+
CONFIG_DIR = _cfg / APP_NAME
|
|
94
|
+
DATA_DIR = _data / APP_NAME
|
|
95
|
+
CACHE_DIR = _cache / APP_NAME
|
|
96
|
+
LOG_DIR = _data / APP_NAME / "logs"
|
|
97
|
+
|
|
98
|
+
EXCHANGE_DIR = DATA_DIR / "exchange"
|
|
99
|
+
|
|
100
|
+
for _d in (CONFIG_DIR, DATA_DIR, CACHE_DIR, LOG_DIR, EXCHANGE_DIR):
|
|
101
|
+
_d.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
|
|
103
|
+
# --- optional aggregate accelerator ---------------------------------------
|
|
104
|
+
# The stdlib core reduces ranges itself; the engine layer may inject a faster
|
|
105
|
+
# numpy-backed reducer here for large all-numeric ranges. core reads the slot
|
|
106
|
+
# through this seam (abax._runtime is the one sanctioned cross-layer import),
|
|
107
|
+
# so it never imports numpy directly.
|
|
108
|
+
_aggregate_accelerator = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def set_aggregate_accelerator(fn) -> None:
|
|
112
|
+
"""Register (or clear, with ``None``) the optional range-aggregate accelerator."""
|
|
113
|
+
global _aggregate_accelerator
|
|
114
|
+
_aggregate_accelerator = fn
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def aggregate_accelerator():
|
|
118
|
+
"""The currently registered aggregate accelerator, or ``None``."""
|
|
119
|
+
return _aggregate_accelerator
|
abax/app.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""CLI entry point.
|
|
2
|
+
|
|
3
|
+
Lazy imports only — never import Qt, Textual, or curses at module top level.
|
|
4
|
+
The ``--help``/``--version``/``--deps`` fast paths respond instantly without
|
|
5
|
+
touching the GUI/TUI stacks (and never create a venv).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
17
|
+
p = argparse.ArgumentParser(
|
|
18
|
+
prog="abax",
|
|
19
|
+
description="abax — a keyboard-first statistics and data-science workstation.",
|
|
20
|
+
)
|
|
21
|
+
p.add_argument("--version", action="store_true", help="print version and exit")
|
|
22
|
+
p.add_argument("--deps", action="store_true", help="print optional-dependency status and exit")
|
|
23
|
+
p.add_argument(
|
|
24
|
+
"--macros",
|
|
25
|
+
action="append",
|
|
26
|
+
default=[],
|
|
27
|
+
metavar="PATH",
|
|
28
|
+
help="macro file or directory to load (repeatable); adds UDFs and macros",
|
|
29
|
+
)
|
|
30
|
+
sub = p.add_subparsers(dest="command")
|
|
31
|
+
|
|
32
|
+
pg = sub.add_parser("gui", help="launch the Qt GUI")
|
|
33
|
+
pg.add_argument("file", nargs="?", help="spreadsheet to open")
|
|
34
|
+
|
|
35
|
+
pt = sub.add_parser("tui", help="launch the curses TUI")
|
|
36
|
+
pt.add_argument("file", nargs="?", help="spreadsheet to open")
|
|
37
|
+
|
|
38
|
+
pv = sub.add_parser("view", help="print a spreadsheet as a table")
|
|
39
|
+
pv.add_argument("file", help="spreadsheet to open (.csv/.xlsx/.json)")
|
|
40
|
+
pv.add_argument("--sheet", help="sheet name (default: active)")
|
|
41
|
+
|
|
42
|
+
pc = sub.add_parser("convert", help="convert between formats by extension")
|
|
43
|
+
pc.add_argument("src")
|
|
44
|
+
pc.add_argument("dst")
|
|
45
|
+
pc.add_argument("--values", action="store_true", help="write computed values, not formulas")
|
|
46
|
+
|
|
47
|
+
pe = sub.add_parser("get", help="print one cell's computed value")
|
|
48
|
+
pe.add_argument("file")
|
|
49
|
+
pe.add_argument("ref", help="A1 reference, e.g. B7")
|
|
50
|
+
|
|
51
|
+
sub.add_parser("deps", help="install the optional dependencies (full-fat)")
|
|
52
|
+
|
|
53
|
+
pm = sub.add_parser("macro", help="list or run macros")
|
|
54
|
+
msub = pm.add_subparsers(dest="macro_cmd")
|
|
55
|
+
msub.add_parser("list", help="list discovered macros and user functions")
|
|
56
|
+
mr = msub.add_parser("run", help="run a macro against a file")
|
|
57
|
+
mr.add_argument("name")
|
|
58
|
+
mr.add_argument("file")
|
|
59
|
+
mr.add_argument("-o", "--output", help="save path (default: overwrite the input file)")
|
|
60
|
+
mr.add_argument("--at", metavar="A1", help="anchor cell for relative macros (e.g. C5)")
|
|
61
|
+
|
|
62
|
+
return p
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_SUBCOMMANDS = frozenset({"gui", "tui", "view", "convert", "get", "macro", "deps"})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _normalize_argv(argv: list[str]) -> list[str]:
|
|
69
|
+
"""A bare file path (no subcommand) opens it in the GUI, so `abax data.csv`
|
|
70
|
+
behaves like `abax gui data.csv`. Flags and real subcommands pass through."""
|
|
71
|
+
if argv and not argv[0].startswith("-") and argv[0] not in _SUBCOMMANDS:
|
|
72
|
+
return ["gui", *argv]
|
|
73
|
+
return argv
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv: list[str] | None = None) -> int:
|
|
77
|
+
argv = _normalize_argv(sys.argv[1:] if argv is None else argv)
|
|
78
|
+
parser = _build_parser()
|
|
79
|
+
args = parser.parse_args(argv)
|
|
80
|
+
|
|
81
|
+
# --- fast paths (no heavy imports) ---
|
|
82
|
+
if args.version:
|
|
83
|
+
print(f"abax {__version__}")
|
|
84
|
+
return 0
|
|
85
|
+
if args.deps:
|
|
86
|
+
from .diagnostics import format_deps
|
|
87
|
+
|
|
88
|
+
print(format_deps())
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
# Load macros (and their UDFs) so every command — view/get/gui/tui — can use
|
|
92
|
+
# them. Cheap when no macro files exist; runs after the fast paths above.
|
|
93
|
+
from .macros import default_macro_dirs, discover_macros, install_functions
|
|
94
|
+
|
|
95
|
+
registry = discover_macros([*default_macro_dirs(), *args.macros])
|
|
96
|
+
udfs = install_functions(registry)
|
|
97
|
+
|
|
98
|
+
cmd = args.command
|
|
99
|
+
if cmd == "gui":
|
|
100
|
+
from .gui.runner import run_gui
|
|
101
|
+
|
|
102
|
+
return run_gui(args.file, registry)
|
|
103
|
+
if cmd == "tui":
|
|
104
|
+
from .tui import run_tui
|
|
105
|
+
|
|
106
|
+
return run_tui(args.file, registry)
|
|
107
|
+
if cmd == "view":
|
|
108
|
+
return _cmd_view(args.file, args.sheet)
|
|
109
|
+
if cmd == "convert":
|
|
110
|
+
return _cmd_convert(args.src, args.dst, args.values)
|
|
111
|
+
if cmd == "get":
|
|
112
|
+
return _cmd_get(args.file, args.ref)
|
|
113
|
+
if cmd == "deps":
|
|
114
|
+
return _cmd_deps()
|
|
115
|
+
if cmd == "macro":
|
|
116
|
+
return _cmd_macro(args, registry, udfs)
|
|
117
|
+
|
|
118
|
+
# No subcommand: prefer GUI, fall back to TUI, then help.
|
|
119
|
+
from . import _runtime as rt
|
|
120
|
+
|
|
121
|
+
if rt._HAS_QT:
|
|
122
|
+
from .gui.runner import run_gui
|
|
123
|
+
|
|
124
|
+
return run_gui(None, registry)
|
|
125
|
+
if sys.stdout.isatty():
|
|
126
|
+
from .tui import run_tui
|
|
127
|
+
|
|
128
|
+
return run_tui(None, registry)
|
|
129
|
+
parser.print_help()
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _cmd_deps() -> int:
|
|
134
|
+
"""Install every optional dependency (full-fat), blocking with progress."""
|
|
135
|
+
from . import autodeps
|
|
136
|
+
|
|
137
|
+
autodeps.set_enabled(True)
|
|
138
|
+
todo = autodeps.prefetch_all(background=False, force=True)
|
|
139
|
+
if todo:
|
|
140
|
+
print(f"Attempted {len(todo)} package(s): {', '.join(todo)}")
|
|
141
|
+
have = sum(1 for _pip, mod in autodeps.ALL if autodeps.installed(mod))
|
|
142
|
+
print(f"Optional dependencies present: {have}/{len(autodeps.ALL)}")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _cmd_view(path: str, sheet_name: str | None) -> int:
|
|
147
|
+
from .engine.document import Document
|
|
148
|
+
|
|
149
|
+
doc = Document.open(path)
|
|
150
|
+
sheet = doc.workbook.get_sheet(sheet_name) if sheet_name else doc.workbook.sheet
|
|
151
|
+
if sheet is None:
|
|
152
|
+
print(f"no such sheet: {sheet_name}", file=sys.stderr)
|
|
153
|
+
return 2
|
|
154
|
+
print(_render_table(sheet))
|
|
155
|
+
return 0
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _cmd_convert(src: str, dst: str, values: bool) -> int:
|
|
159
|
+
from .engine.document import Document
|
|
160
|
+
|
|
161
|
+
doc = Document.open(src)
|
|
162
|
+
try:
|
|
163
|
+
doc.save(dst)
|
|
164
|
+
except RuntimeError as exc: # e.g. openpyxl missing
|
|
165
|
+
print(str(exc), file=sys.stderr)
|
|
166
|
+
return 3
|
|
167
|
+
print(f"converted {src} -> {dst}")
|
|
168
|
+
return 0
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _cmd_get(path: str, ref: str) -> int:
|
|
172
|
+
from .engine.document import Document
|
|
173
|
+
|
|
174
|
+
doc = Document.open(path)
|
|
175
|
+
print(doc.workbook.sheet.format_value(doc.workbook.sheet.get(ref)))
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _cmd_macro(args, registry, udfs) -> int:
|
|
180
|
+
if args.macro_cmd == "run":
|
|
181
|
+
from .engine.document import Document
|
|
182
|
+
from .macros import MacroError, run_macro
|
|
183
|
+
|
|
184
|
+
cursor = None
|
|
185
|
+
if args.at:
|
|
186
|
+
from .core.reference import parse_a1
|
|
187
|
+
|
|
188
|
+
cursor = parse_a1(args.at)
|
|
189
|
+
doc = Document.open(args.file)
|
|
190
|
+
try:
|
|
191
|
+
ctx = run_macro(registry, args.name, doc.workbook, cursor=cursor)
|
|
192
|
+
except MacroError as exc:
|
|
193
|
+
print(str(exc), file=sys.stderr)
|
|
194
|
+
return 4
|
|
195
|
+
for msg in ctx.messages:
|
|
196
|
+
print(msg)
|
|
197
|
+
out = args.output or args.file
|
|
198
|
+
doc.save(out)
|
|
199
|
+
print(f"ran macro {args.name!r}; saved {out}")
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
# default / "list"
|
|
203
|
+
if registry.macros:
|
|
204
|
+
print("macros:")
|
|
205
|
+
for name in sorted(registry.macros):
|
|
206
|
+
print(f" {name}")
|
|
207
|
+
else:
|
|
208
|
+
print("no macros found (drop .py files in CONFIG_DIR/macros or pass --macros PATH)")
|
|
209
|
+
if udfs:
|
|
210
|
+
print("user functions:")
|
|
211
|
+
for name in udfs:
|
|
212
|
+
print(f" {name}()")
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _render_table(sheet) -> str:
|
|
217
|
+
from .core.reference import index_to_col
|
|
218
|
+
|
|
219
|
+
n_rows, n_cols = sheet.used_bounds()
|
|
220
|
+
if n_rows == 0:
|
|
221
|
+
return "(empty)"
|
|
222
|
+
cells = [[sheet.display(r, c) for c in range(n_cols)] for r in range(n_rows)]
|
|
223
|
+
headers = [index_to_col(c) for c in range(n_cols)]
|
|
224
|
+
row_label_w = len(str(n_rows))
|
|
225
|
+
widths = [
|
|
226
|
+
max(len(headers[c]), max((len(cells[r][c]) for r in range(n_rows)), default=0))
|
|
227
|
+
for c in range(n_cols)
|
|
228
|
+
]
|
|
229
|
+
out = [" " * row_label_w + " | " + " | ".join(h.ljust(widths[c]) for c, h in enumerate(headers))]
|
|
230
|
+
out.append("-" * len(out[0]))
|
|
231
|
+
for r in range(n_rows):
|
|
232
|
+
line = str(r + 1).rjust(row_label_w) + " | "
|
|
233
|
+
line += " | ".join(cells[r][c].ljust(widths[c]) for c in range(n_cols))
|
|
234
|
+
out.append(line)
|
|
235
|
+
return "\n".join(out)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
raise SystemExit(main())
|
abax/autodeps.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""On-demand auto-installation of abax's optional dependencies.
|
|
2
|
+
|
|
3
|
+
abax's core is stdlib-only and every heavier capability is an *optional* package
|
|
4
|
+
with a graceful fallback. By default abax fetches those packages **automatically**
|
|
5
|
+
in the background, so a plain install grows into a "full-fat" one on its own — the
|
|
6
|
+
data-science stack, Excel/Parquet I/O, the PTY terminal, and Jupyter integration
|
|
7
|
+
all appear without the user running a single `pip install … [extra]`.
|
|
8
|
+
|
|
9
|
+
Design points:
|
|
10
|
+
- **Best-effort & non-blocking.** Installs run in a daemon thread; startup and the
|
|
11
|
+
UI never wait on pip. If pip is missing, the machine is offline, or a build fails,
|
|
12
|
+
abax silently keeps using its pure-Python fallbacks.
|
|
13
|
+
- **Attempted once per machine.** A marker file per package (under the cache dir)
|
|
14
|
+
means a slow or failing install isn't retried on every launch. A *forced* install
|
|
15
|
+
(the explicit "install optional features now" action) ignores the markers.
|
|
16
|
+
- **Opt-out.** ``settings.auto_install = False`` or the ``ABAX_NO_AUTOINSTALL``
|
|
17
|
+
environment variable disables it entirely.
|
|
18
|
+
|
|
19
|
+
The GUI binding itself (PySide6/PyQt6) is **not** auto-installed — you need a Qt
|
|
20
|
+
binding to launch the GUI in the first place, and it's the one heavyweight a user
|
|
21
|
+
deliberately chooses (`pip install abax[gui]`).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import importlib.util
|
|
27
|
+
import os
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import threading
|
|
31
|
+
|
|
32
|
+
# (pip distribution name, import module name) for every optional dependency.
|
|
33
|
+
# Ordered light -> heavy so the quick wins land first and the slowest (pymc) last.
|
|
34
|
+
_SCIENCE = [
|
|
35
|
+
("numpy", "numpy"), ("pandas", "pandas"), ("scipy", "scipy"),
|
|
36
|
+
("scikit-learn", "sklearn"), ("statsmodels", "statsmodels"),
|
|
37
|
+
("pingouin", "pingouin"), ("lifelines", "lifelines"),
|
|
38
|
+
("scikit-survival", "sksurv"),
|
|
39
|
+
]
|
|
40
|
+
# Bayesian stack, split out (pymc pulls pytensor + arviz + numba/llvmlite ~150 MB).
|
|
41
|
+
# Still part of the default full-fat `ALL` set below.
|
|
42
|
+
_BAYES = [("pymc", "pymc")]
|
|
43
|
+
_TERMINAL = [("pyte", "pyte")]
|
|
44
|
+
if sys.platform == "win32":
|
|
45
|
+
_TERMINAL.append(("pywinpty", "winpty")) # ConPTY backend on Windows
|
|
46
|
+
|
|
47
|
+
FEATURES: dict[str, list[tuple[str, str]]] = {
|
|
48
|
+
"fast-io": [("platformdirs", "platformdirs"), ("msgspec", "msgspec")],
|
|
49
|
+
"excel": [("openpyxl", "openpyxl")],
|
|
50
|
+
"parquet": [("pyarrow", "pyarrow")],
|
|
51
|
+
"terminal": _TERMINAL,
|
|
52
|
+
"tui": [("rich", "rich"), ("textual", "textual")],
|
|
53
|
+
"jupyter": [("nbformat", "nbformat"), ("ipykernel", "ipykernel"),
|
|
54
|
+
("anywidget", "anywidget")],
|
|
55
|
+
"science": _SCIENCE,
|
|
56
|
+
"bayes": _BAYES,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# The full-fat set (the `all` extra), ordered light -> heavy so the quick wins
|
|
60
|
+
# land first and the heaviest (pymc) last; markers dedupe across features.
|
|
61
|
+
ALL: list[tuple[str, str]] = [
|
|
62
|
+
("platformdirs", "platformdirs"), ("msgspec", "msgspec"),
|
|
63
|
+
("openpyxl", "openpyxl"),
|
|
64
|
+
*_TERMINAL,
|
|
65
|
+
("rich", "rich"), ("textual", "textual"),
|
|
66
|
+
("nbformat", "nbformat"), ("anywidget", "anywidget"),
|
|
67
|
+
("pyarrow", "pyarrow"),
|
|
68
|
+
*_SCIENCE,
|
|
69
|
+
("ipykernel", "ipykernel"),
|
|
70
|
+
*_BAYES,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Human-facing descriptions for the first-run chooser: feature -> (label,
|
|
74
|
+
# what it installs, approximate MB). Feature closures share dependencies, so the
|
|
75
|
+
# totals for a preset are smaller than the sum of the parts.
|
|
76
|
+
FEATURE_INFO: dict[str, tuple[str, str, int]] = {
|
|
77
|
+
"fast-io": ("Faster settings & correct folders",
|
|
78
|
+
"msgspec + platformdirs", 8),
|
|
79
|
+
"excel": ("Excel spreadsheets (.xlsx)", "openpyxl", 4),
|
|
80
|
+
"terminal": ("A true terminal panel",
|
|
81
|
+
"pyte" + (" + pywinpty" if sys.platform == "win32" else ""), 3),
|
|
82
|
+
"tui": ("A richer terminal UI", "textual + rich", 12),
|
|
83
|
+
"jupyter": ("Jupyter integration",
|
|
84
|
+
"nbformat + ipykernel + anywidget — notebook validation, the abax "
|
|
85
|
+
"kernel, and the editable-sheet widget", 80),
|
|
86
|
+
"parquet": ("Parquet / Feather data files", "pyarrow", 90),
|
|
87
|
+
"science": ("Data science: statistics, ML, DataFrames, graphing",
|
|
88
|
+
"numpy, pandas, scipy, scikit-learn, statsmodels, lifelines, "
|
|
89
|
+
"pingouin, scikit-survival", 450),
|
|
90
|
+
"bayes": ("Bayesian / probabilistic modeling (large)",
|
|
91
|
+
"pymc + pytensor + arviz + numba/llvmlite", 150),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# The two common presets offered by the chooser. "thin" = the lean conveniences
|
|
95
|
+
# (matching the pip `thin` extra minus the Qt binding); "all" = everything.
|
|
96
|
+
PRESETS: dict[str, list[str]] = {
|
|
97
|
+
"thin": ["fast-io", "excel", "terminal", "tui"],
|
|
98
|
+
"all": list(FEATURES),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def preset(name: str) -> list[str]:
|
|
103
|
+
"""Feature keys for a named preset (``"thin"`` / ``"all"``)."""
|
|
104
|
+
return list(PRESETS.get(name, []))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- configuration / hooks (the install fn + marker dir are injectable for tests)
|
|
108
|
+
_INSTALL_FN = None # set below; tests may replace it
|
|
109
|
+
_MARKER_DIR = None # None -> CACHE_DIR/autodeps
|
|
110
|
+
_enabled_override: bool | None = None
|
|
111
|
+
_lock = threading.Lock()
|
|
112
|
+
_attempted_session: set[str] = set()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def set_enabled(flag: bool | None) -> None:
|
|
116
|
+
"""Force auto-install on/off (``None`` restores the default)."""
|
|
117
|
+
global _enabled_override
|
|
118
|
+
_enabled_override = flag
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def enabled() -> bool:
|
|
122
|
+
if os.environ.get("ABAX_NO_AUTOINSTALL"):
|
|
123
|
+
return False
|
|
124
|
+
if _enabled_override is not None:
|
|
125
|
+
return _enabled_override
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def installed(import_name: str) -> bool:
|
|
130
|
+
try:
|
|
131
|
+
return importlib.util.find_spec(import_name) is not None
|
|
132
|
+
except Exception:
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _marker_dir():
|
|
137
|
+
from pathlib import Path
|
|
138
|
+
if _MARKER_DIR is not None:
|
|
139
|
+
return Path(_MARKER_DIR)
|
|
140
|
+
from ._runtime import CACHE_DIR
|
|
141
|
+
return CACHE_DIR / "autodeps"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _attempted(pip_name: str) -> bool:
|
|
145
|
+
if pip_name in _attempted_session:
|
|
146
|
+
return True
|
|
147
|
+
try:
|
|
148
|
+
return (_marker_dir() / f"{pip_name}.attempted").exists()
|
|
149
|
+
except Exception:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _mark(pip_name: str) -> None:
|
|
154
|
+
_attempted_session.add(pip_name)
|
|
155
|
+
try:
|
|
156
|
+
d = _marker_dir()
|
|
157
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
(d / f"{pip_name}.attempted").write_text("1", encoding="utf-8")
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _pip_install(pip_name: str, timeout: float = 1800) -> bool:
|
|
164
|
+
try:
|
|
165
|
+
proc = subprocess.run(
|
|
166
|
+
[sys.executable, "-m", "pip", "install", "--quiet", pip_name],
|
|
167
|
+
capture_output=True, timeout=timeout)
|
|
168
|
+
return proc.returncode == 0
|
|
169
|
+
except Exception:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_INSTALL_FN = _pip_install
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def missing(packages) -> list[tuple[str, str]]:
|
|
177
|
+
"""The subset of ``(pip, module)`` pairs whose module isn't importable."""
|
|
178
|
+
return [(pip, mod) for pip, mod in packages if not installed(mod)]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def ensure(packages, *, background: bool = True, force: bool = False) -> list[str]:
|
|
182
|
+
"""Install any missing packages, best-effort and once.
|
|
183
|
+
|
|
184
|
+
``packages`` is a list of ``(pip_name, import_name)`` pairs. Returns the pip
|
|
185
|
+
names it will attempt (empty if disabled, all present, or already attempted).
|
|
186
|
+
With ``force`` it ignores the once-per-machine markers (still skips packages
|
|
187
|
+
already importable).
|
|
188
|
+
"""
|
|
189
|
+
if not enabled():
|
|
190
|
+
return []
|
|
191
|
+
todo: list[str] = []
|
|
192
|
+
with _lock:
|
|
193
|
+
for pip, mod in packages:
|
|
194
|
+
if installed(mod):
|
|
195
|
+
continue
|
|
196
|
+
if not force and _attempted(pip):
|
|
197
|
+
continue
|
|
198
|
+
_mark(pip) # claim now so concurrent calls don't race
|
|
199
|
+
todo.append(pip)
|
|
200
|
+
if not todo:
|
|
201
|
+
return []
|
|
202
|
+
|
|
203
|
+
def work() -> None:
|
|
204
|
+
for pip in todo:
|
|
205
|
+
_INSTALL_FN(pip)
|
|
206
|
+
|
|
207
|
+
if background:
|
|
208
|
+
threading.Thread(target=work, name="abax-autodeps", daemon=True).start()
|
|
209
|
+
else:
|
|
210
|
+
work()
|
|
211
|
+
return todo
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def ensure_feature(key: str, *, background: bool = True, force: bool = False) -> list[str]:
|
|
215
|
+
"""Ensure the packages backing one feature (e.g. ``"science"``/``"excel"``)."""
|
|
216
|
+
return ensure(FEATURES.get(key, []), background=background, force=force)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def prefetch_all(*, background: bool = True, force: bool = False) -> list[str]:
|
|
220
|
+
"""Fetch the entire full-fat optional stack (called once on GUI startup)."""
|
|
221
|
+
return ensure(ALL, background=background, force=force)
|