openstat-cli 1.0.0__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.
- openstat/__init__.py +3 -0
- openstat/__main__.py +4 -0
- openstat/backends/__init__.py +16 -0
- openstat/backends/duckdb_backend.py +70 -0
- openstat/backends/polars_backend.py +52 -0
- openstat/cli.py +92 -0
- openstat/commands/__init__.py +82 -0
- openstat/commands/adv_stat_cmds.py +1255 -0
- openstat/commands/advanced_ml_cmds.py +576 -0
- openstat/commands/advreg_cmds.py +207 -0
- openstat/commands/alias_cmds.py +135 -0
- openstat/commands/arch_cmds.py +82 -0
- openstat/commands/arules_cmds.py +111 -0
- openstat/commands/automodel_cmds.py +212 -0
- openstat/commands/backend_cmds.py +82 -0
- openstat/commands/base.py +170 -0
- openstat/commands/bayes_cmds.py +71 -0
- openstat/commands/causal_cmds.py +269 -0
- openstat/commands/cluster_cmds.py +152 -0
- openstat/commands/data_cmds.py +996 -0
- openstat/commands/datamanip_cmds.py +672 -0
- openstat/commands/dataquality_cmds.py +174 -0
- openstat/commands/datetime_cmds.py +176 -0
- openstat/commands/dimreduce_cmds.py +184 -0
- openstat/commands/discrete_cmds.py +149 -0
- openstat/commands/dsl_cmds.py +143 -0
- openstat/commands/epi_cmds.py +93 -0
- openstat/commands/equiv_tobit_cmds.py +94 -0
- openstat/commands/esttab_cmds.py +196 -0
- openstat/commands/export_beamer_cmds.py +142 -0
- openstat/commands/export_cmds.py +201 -0
- openstat/commands/export_extra_cmds.py +240 -0
- openstat/commands/factor_cmds.py +180 -0
- openstat/commands/groupby_cmds.py +155 -0
- openstat/commands/help_cmds.py +237 -0
- openstat/commands/i18n_cmds.py +43 -0
- openstat/commands/import_extra_cmds.py +561 -0
- openstat/commands/influence_cmds.py +134 -0
- openstat/commands/iv_cmds.py +106 -0
- openstat/commands/manova_cmds.py +105 -0
- openstat/commands/mediate_cmds.py +233 -0
- openstat/commands/meta_cmds.py +284 -0
- openstat/commands/mi_cmds.py +228 -0
- openstat/commands/mixed_cmds.py +79 -0
- openstat/commands/mixture_changepoint_cmds.py +166 -0
- openstat/commands/ml_adv_cmds.py +147 -0
- openstat/commands/ml_cmds.py +178 -0
- openstat/commands/model_eval_cmds.py +142 -0
- openstat/commands/network_cmds.py +288 -0
- openstat/commands/nlquery_cmds.py +161 -0
- openstat/commands/nonparam_cmds.py +149 -0
- openstat/commands/outreg_cmds.py +247 -0
- openstat/commands/panel_cmds.py +141 -0
- openstat/commands/pdf_cmds.py +226 -0
- openstat/commands/pipeline_cmds.py +319 -0
- openstat/commands/plot_cmds.py +189 -0
- openstat/commands/plugin_cmds.py +79 -0
- openstat/commands/posthoc_cmds.py +153 -0
- openstat/commands/power_cmds.py +172 -0
- openstat/commands/profile_cmds.py +246 -0
- openstat/commands/rbridge_cmds.py +81 -0
- openstat/commands/regex_cmds.py +104 -0
- openstat/commands/report_cmds.py +48 -0
- openstat/commands/repro_cmds.py +129 -0
- openstat/commands/resampling_cmds.py +109 -0
- openstat/commands/reshape_cmds.py +223 -0
- openstat/commands/sem_cmds.py +177 -0
- openstat/commands/stat_cmds.py +1040 -0
- openstat/commands/stata_import_cmds.py +215 -0
- openstat/commands/string_cmds.py +124 -0
- openstat/commands/surv_cmds.py +145 -0
- openstat/commands/survey_cmds.py +153 -0
- openstat/commands/textanalysis_cmds.py +192 -0
- openstat/commands/ts_adv_cmds.py +136 -0
- openstat/commands/ts_cmds.py +195 -0
- openstat/commands/tui_cmds.py +111 -0
- openstat/commands/ux_cmds.py +191 -0
- openstat/commands/validate_cmds.py +270 -0
- openstat/commands/viz_adv_cmds.py +312 -0
- openstat/commands/viz_extra_cmds.py +251 -0
- openstat/commands/watch_cmds.py +69 -0
- openstat/config.py +106 -0
- openstat/dsl/__init__.py +0 -0
- openstat/dsl/parser.py +332 -0
- openstat/dsl/tokenizer.py +105 -0
- openstat/i18n.py +120 -0
- openstat/io/__init__.py +0 -0
- openstat/io/loader.py +187 -0
- openstat/jupyter/__init__.py +18 -0
- openstat/jupyter/display.py +18 -0
- openstat/jupyter/magic.py +60 -0
- openstat/logging_config.py +59 -0
- openstat/plots/__init__.py +0 -0
- openstat/plots/plotter.py +437 -0
- openstat/plots/surv_plots.py +32 -0
- openstat/plots/ts_plots.py +59 -0
- openstat/plugins/__init__.py +5 -0
- openstat/plugins/manager.py +69 -0
- openstat/repl.py +457 -0
- openstat/reporting/__init__.py +0 -0
- openstat/reporting/eda.py +208 -0
- openstat/reporting/report.py +67 -0
- openstat/script_runner.py +319 -0
- openstat/session.py +133 -0
- openstat/stats/__init__.py +0 -0
- openstat/stats/advanced_regression.py +269 -0
- openstat/stats/arch_garch.py +84 -0
- openstat/stats/bayesian.py +103 -0
- openstat/stats/causal.py +258 -0
- openstat/stats/clustering.py +206 -0
- openstat/stats/discrete.py +311 -0
- openstat/stats/epidemiology.py +119 -0
- openstat/stats/equiv_tobit.py +163 -0
- openstat/stats/factor.py +174 -0
- openstat/stats/imputation.py +282 -0
- openstat/stats/influence.py +78 -0
- openstat/stats/iv.py +131 -0
- openstat/stats/manova.py +124 -0
- openstat/stats/mixed.py +128 -0
- openstat/stats/ml.py +275 -0
- openstat/stats/ml_advanced.py +117 -0
- openstat/stats/model_eval.py +183 -0
- openstat/stats/models.py +1342 -0
- openstat/stats/nonparametric.py +130 -0
- openstat/stats/panel.py +179 -0
- openstat/stats/power.py +295 -0
- openstat/stats/resampling.py +203 -0
- openstat/stats/survey.py +213 -0
- openstat/stats/survival.py +196 -0
- openstat/stats/timeseries.py +142 -0
- openstat/stats/ts_advanced.py +114 -0
- openstat/types.py +11 -0
- openstat/web/__init__.py +1 -0
- openstat/web/app.py +117 -0
- openstat/web/session_manager.py +73 -0
- openstat/web/static/app.js +117 -0
- openstat/web/static/index.html +38 -0
- openstat/web/static/style.css +103 -0
- openstat_cli-1.0.0.dist-info/METADATA +748 -0
- openstat_cli-1.0.0.dist-info/RECORD +143 -0
- openstat_cli-1.0.0.dist-info/WHEEL +4 -0
- openstat_cli-1.0.0.dist-info/entry_points.txt +2 -0
- openstat_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Group-by aggregation and rolling window commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from openstat.commands.base import command, CommandArgs, friendly_error
|
|
6
|
+
from openstat.session import Session
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@command("groupby", usage="groupby <groupcol> [groupcol2 ...] agg <col>:<func> [...]")
|
|
10
|
+
def cmd_groupby(session: Session, args: str) -> str:
|
|
11
|
+
"""Group-by aggregation: compute statistics by group.
|
|
12
|
+
|
|
13
|
+
Aggregation functions: mean, sum, min, max, std, var, median,
|
|
14
|
+
count, n, nunique, first, last
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
groupby gender agg income:mean age:mean
|
|
18
|
+
groupby country year agg sales:sum profit:mean
|
|
19
|
+
groupby category agg price:min price:max price:mean count:n
|
|
20
|
+
groupby region agg value:median value:std
|
|
21
|
+
|
|
22
|
+
The result replaces the current dataset. Use 'undo' to restore.
|
|
23
|
+
"""
|
|
24
|
+
import polars as pl
|
|
25
|
+
|
|
26
|
+
ca = CommandArgs(args)
|
|
27
|
+
# Split at 'agg' keyword
|
|
28
|
+
agg_raw = ca.rest_after("agg")
|
|
29
|
+
if not agg_raw:
|
|
30
|
+
return "Usage: groupby <col> [col2] agg <col>:<func> [col:func ...]"
|
|
31
|
+
|
|
32
|
+
# Everything before 'agg' is group columns
|
|
33
|
+
before_agg = args.split("agg", 1)[0].strip()
|
|
34
|
+
group_cols = before_agg.split()
|
|
35
|
+
if not group_cols:
|
|
36
|
+
return "Specify at least one group column."
|
|
37
|
+
|
|
38
|
+
# Parse agg specs: col:func or col:func=alias
|
|
39
|
+
agg_specs = agg_raw.strip().split()
|
|
40
|
+
FUNC_MAP = {
|
|
41
|
+
"mean": pl.Expr.mean,
|
|
42
|
+
"sum": pl.Expr.sum,
|
|
43
|
+
"min": pl.Expr.min,
|
|
44
|
+
"max": pl.Expr.max,
|
|
45
|
+
"std": pl.Expr.std,
|
|
46
|
+
"var": pl.Expr.var,
|
|
47
|
+
"median": pl.Expr.median,
|
|
48
|
+
"count": pl.Expr.count,
|
|
49
|
+
"n": pl.Expr.count,
|
|
50
|
+
"nunique": pl.Expr.n_unique,
|
|
51
|
+
"first": pl.Expr.first,
|
|
52
|
+
"last": pl.Expr.last,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
df = session.require_data()
|
|
57
|
+
for gc in group_cols:
|
|
58
|
+
if gc not in df.columns:
|
|
59
|
+
return f"Group column not found: {gc}"
|
|
60
|
+
|
|
61
|
+
exprs = []
|
|
62
|
+
for spec in agg_specs:
|
|
63
|
+
if ":" not in spec:
|
|
64
|
+
return f"Invalid agg spec '{spec}'. Use col:func format."
|
|
65
|
+
col, func = spec.split(":", 1)
|
|
66
|
+
alias = None
|
|
67
|
+
if "=" in func:
|
|
68
|
+
func, alias = func.split("=", 1)
|
|
69
|
+
func = func.lower()
|
|
70
|
+
if col not in df.columns and func not in ("n", "count"):
|
|
71
|
+
return f"Column not found: {col}"
|
|
72
|
+
if func not in FUNC_MAP:
|
|
73
|
+
avail = ", ".join(FUNC_MAP)
|
|
74
|
+
return f"Unknown function '{func}'. Available: {avail}"
|
|
75
|
+
|
|
76
|
+
expr_col = pl.col(col) if col in df.columns else pl.first()
|
|
77
|
+
expr = FUNC_MAP[func](expr_col)
|
|
78
|
+
out_name = alias or f"{col}_{func}"
|
|
79
|
+
exprs.append(expr.alias(out_name))
|
|
80
|
+
|
|
81
|
+
session.snapshot()
|
|
82
|
+
session.df = df.group_by(group_cols).agg(exprs).sort(group_cols)
|
|
83
|
+
return f"Group-by complete. Result: {session.shape_str}"
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return friendly_error(e, "groupby")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@command("rolling", usage="rolling <col> <window> <func> [into(<newcol>)]")
|
|
90
|
+
def cmd_rolling(session: Session, args: str) -> str:
|
|
91
|
+
"""Rolling window statistics on a column.
|
|
92
|
+
|
|
93
|
+
Functions: mean, sum, min, max, std, var, median
|
|
94
|
+
|
|
95
|
+
Options:
|
|
96
|
+
--center — centered window (default: trailing)
|
|
97
|
+
--min_periods=N — minimum observations required
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
rolling price 7 mean into(price_7d_avg)
|
|
101
|
+
rolling sales 30 sum into(sales_30d)
|
|
102
|
+
rolling returns 20 std into(vol_20d) --center
|
|
103
|
+
rolling value 5 median
|
|
104
|
+
"""
|
|
105
|
+
import polars as pl
|
|
106
|
+
|
|
107
|
+
ca = CommandArgs(args)
|
|
108
|
+
if len(ca.positional) < 3:
|
|
109
|
+
return "Usage: rolling <col> <window> <func> [into(<newcol>)]"
|
|
110
|
+
|
|
111
|
+
col = ca.positional[0]
|
|
112
|
+
try:
|
|
113
|
+
window = int(ca.positional[1])
|
|
114
|
+
except ValueError:
|
|
115
|
+
return f"Window must be an integer, got: {ca.positional[1]}"
|
|
116
|
+
func = ca.positional[2].lower()
|
|
117
|
+
center = "--center" in args
|
|
118
|
+
min_periods = int(ca.options.get("min_periods", 1))
|
|
119
|
+
|
|
120
|
+
into_raw = ca.rest_after("into")
|
|
121
|
+
newcol = into_raw.strip().strip("()") if into_raw else f"{col}_roll{window}_{func}"
|
|
122
|
+
|
|
123
|
+
FUNC_MAP = {
|
|
124
|
+
"mean": "mean",
|
|
125
|
+
"sum": "sum",
|
|
126
|
+
"min": "min",
|
|
127
|
+
"max": "max",
|
|
128
|
+
"std": "std",
|
|
129
|
+
"var": "var",
|
|
130
|
+
"median": "median",
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
df = session.require_data()
|
|
135
|
+
if col not in df.columns:
|
|
136
|
+
return f"Column not found: {col}"
|
|
137
|
+
if func not in FUNC_MAP:
|
|
138
|
+
return f"Unknown function '{func}'. Available: {', '.join(FUNC_MAP)}"
|
|
139
|
+
|
|
140
|
+
roll_kwargs = dict(window_size=window, min_periods=min_periods, center=center)
|
|
141
|
+
roll = pl.col(col).rolling_mean(**roll_kwargs) if func == "mean" else \
|
|
142
|
+
pl.col(col).rolling_sum(**roll_kwargs) if func == "sum" else \
|
|
143
|
+
pl.col(col).rolling_min(**roll_kwargs) if func == "min" else \
|
|
144
|
+
pl.col(col).rolling_max(**roll_kwargs) if func == "max" else \
|
|
145
|
+
pl.col(col).rolling_std(**roll_kwargs) if func == "std" else \
|
|
146
|
+
pl.col(col).rolling_var(**roll_kwargs) if func == "var" else \
|
|
147
|
+
pl.col(col).rolling_median(**roll_kwargs)
|
|
148
|
+
|
|
149
|
+
session.snapshot()
|
|
150
|
+
session.df = df.with_columns(roll.alias(newcol))
|
|
151
|
+
n_valid = session.df[newcol].drop_nulls().len()
|
|
152
|
+
return f"Rolling {func}({window}) → '{newcol}': {n_valid}/{df.height} non-null values."
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
return friendly_error(e, "rolling")
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Enhanced help command with examples and category browsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from openstat.commands.base import command, get_registry, get_usage
|
|
6
|
+
from openstat.session import Session
|
|
7
|
+
|
|
8
|
+
# ── Command categories and examples ─────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
_CATEGORIES: dict[str, list[str]] = {
|
|
11
|
+
"Data Management": [
|
|
12
|
+
"load", "save", "describe", "summarize", "drop", "keep", "rename",
|
|
13
|
+
"generate", "replace", "sort", "filter", "sample", "undo", "redo",
|
|
14
|
+
"append", "merge", "reshape", "encode", "decode", "validate",
|
|
15
|
+
"fuzzyjoin", "regex", "sql", "sqlload",
|
|
16
|
+
],
|
|
17
|
+
"Descriptive Statistics": [
|
|
18
|
+
"tabulate", "correlate", "crosstab", "anova",
|
|
19
|
+
],
|
|
20
|
+
"Regression Models": [
|
|
21
|
+
"ols", "logit", "probit", "poisson", "ivregress",
|
|
22
|
+
"quantreg", "truncreg", "intreg", "heckman",
|
|
23
|
+
],
|
|
24
|
+
"Post-Estimation": [
|
|
25
|
+
"margins", "predict", "test", "posthoc", "mediate", "modmediate",
|
|
26
|
+
"estat", "esttab", "outreg2",
|
|
27
|
+
],
|
|
28
|
+
"Panel / Time Series": [
|
|
29
|
+
"xtset", "xtreg", "xttest", "hausman",
|
|
30
|
+
"tsset", "arima", "ardl", "adf", "kpss", "forecast",
|
|
31
|
+
"vecm", "var", "granger", "threshold", "arch", "garch",
|
|
32
|
+
],
|
|
33
|
+
"Survival Analysis": [
|
|
34
|
+
"stset", "stcox", "streg", "stsum",
|
|
35
|
+
],
|
|
36
|
+
"Causal Inference": [
|
|
37
|
+
"pscore", "teffects", "did", "rddesign", "iptw",
|
|
38
|
+
],
|
|
39
|
+
"Machine Learning": [
|
|
40
|
+
"mlfit", "randomforest", "gradientboost", "neuralnet", "svm", "knn",
|
|
41
|
+
"automodel", "cluster", "discriminant",
|
|
42
|
+
],
|
|
43
|
+
"Factor / SEM": [
|
|
44
|
+
"factor", "pca", "sem", "cfa",
|
|
45
|
+
],
|
|
46
|
+
"Epidemiology / Biostatistics": [
|
|
47
|
+
"epi", "irt",
|
|
48
|
+
],
|
|
49
|
+
"Meta-analysis": [
|
|
50
|
+
"meta",
|
|
51
|
+
],
|
|
52
|
+
"Network Analysis": [
|
|
53
|
+
"network",
|
|
54
|
+
],
|
|
55
|
+
"Non-parametric": [
|
|
56
|
+
"kruskal", "mannwhitney", "wilcoxon", "friedman",
|
|
57
|
+
],
|
|
58
|
+
"Resampling & Inference": [
|
|
59
|
+
"bootstrap", "jackknife", "permtest", "power",
|
|
60
|
+
],
|
|
61
|
+
"Model Evaluation": [
|
|
62
|
+
"roc", "calibration", "confusion", "influence",
|
|
63
|
+
],
|
|
64
|
+
"Plots": [
|
|
65
|
+
"plot hist", "plot scatter", "plot line", "plot box", "plot bar",
|
|
66
|
+
"plot heatmap", "plot coef", "plot margins", "plot interaction",
|
|
67
|
+
"plot diagnostics", "plot violin", "plot pairplot",
|
|
68
|
+
],
|
|
69
|
+
"Export & Reporting": [
|
|
70
|
+
"export docx", "export pptx", "export pdf", "export md",
|
|
71
|
+
"log using", "report",
|
|
72
|
+
],
|
|
73
|
+
"Session & Reproducibility": [
|
|
74
|
+
"session info", "session save", "session replay",
|
|
75
|
+
"set seed", "set backend", "version",
|
|
76
|
+
],
|
|
77
|
+
"Scripting": [
|
|
78
|
+
"run", "watch", "import do", "define", "alias",
|
|
79
|
+
],
|
|
80
|
+
"Settings & Integration": [
|
|
81
|
+
"theme", "locale", "dashboard", "ask", "r",
|
|
82
|
+
"config", "plugin",
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_EXAMPLES: dict[str, list[str]] = {
|
|
87
|
+
"load": ["load data.csv", "load survey.dta", "load results.parquet"],
|
|
88
|
+
"save": ["save cleaned.csv", "save output.parquet"],
|
|
89
|
+
"ols": ["ols income educ age", "ols y x1 x2 x3 --robust"],
|
|
90
|
+
"logit": ["logit employed age educ female", "logit y x1 x2 --margins"],
|
|
91
|
+
"margins": ["margins educ", "margins age --at(educ=12)"],
|
|
92
|
+
"mediate": ["mediate income educ age --boot=2000"],
|
|
93
|
+
"modmediate": ["modmediate y mediator x moderator"],
|
|
94
|
+
"plot": [
|
|
95
|
+
"plot hist income", "plot scatter y x", "plot coef",
|
|
96
|
+
"plot interaction y x moderator", "plot margins",
|
|
97
|
+
],
|
|
98
|
+
"export": ["export docx", "export pdf report.pdf", "export md summary.md"],
|
|
99
|
+
"validate": ["validate age min=0 max=120 notnull", "validate email regex=^[^@]+@[^@]+"],
|
|
100
|
+
"fuzzyjoin": ["fuzzyjoin companies.csv on(name) --threshold=85"],
|
|
101
|
+
"regex": [
|
|
102
|
+
'regex extract email "([^@]+)@" into(username)',
|
|
103
|
+
'regex replace phone "[^0-9]" "" into(phone_clean)',
|
|
104
|
+
],
|
|
105
|
+
"alias": ["alias reg ols", "alias list", "alias rm reg"],
|
|
106
|
+
"theme": ["theme dark", "theme solarized", "theme list"],
|
|
107
|
+
"ask": ['ask "What variables have the most missing data?"'],
|
|
108
|
+
"watch": ["watch analysis.ost", "watch pipeline.ost --interval=5"],
|
|
109
|
+
"import do": ["import do stata_script.do", "import do analysis.do --run"],
|
|
110
|
+
"dashboard": ["dashboard"],
|
|
111
|
+
"session": ["session info", "session save analysis.ost", "session replay analysis.ost"],
|
|
112
|
+
"sem": ["sem 'y1 =~ x1 + x2\ny2 =~ x3 + x4'"],
|
|
113
|
+
"meta": ["meta es se --random --forest"],
|
|
114
|
+
"network": ["network build from src to dst", "network centrality", "network plot"],
|
|
115
|
+
"automodel": ["automodel y x1 x2 x3 x4 x5 --criterion=aic"],
|
|
116
|
+
"bootstrap": ["bootstrap ols income educ age --reps=500"],
|
|
117
|
+
"power": ["power ttest --delta=0.5 --alpha=0.05"],
|
|
118
|
+
"r": ['r "summary(data)"', 'r "cor(data)"'],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@command("help", usage="help [<command>] [--list] [--category=<name>]")
|
|
123
|
+
def cmd_help(session: Session, args: str) -> str:
|
|
124
|
+
"""Show help for a command, list all commands, or browse by category.
|
|
125
|
+
|
|
126
|
+
Usage:
|
|
127
|
+
help — show command categories
|
|
128
|
+
help <command> — detailed help with examples
|
|
129
|
+
help --list — alphabetical list of all commands
|
|
130
|
+
help --category=Plots — list commands in a category
|
|
131
|
+
help --search=<keyword> — search command names and descriptions
|
|
132
|
+
|
|
133
|
+
Examples:
|
|
134
|
+
help ols
|
|
135
|
+
help plot
|
|
136
|
+
help --list
|
|
137
|
+
help --category="Machine Learning"
|
|
138
|
+
help --search=regression
|
|
139
|
+
"""
|
|
140
|
+
from openstat.commands.base import _REGISTRY
|
|
141
|
+
|
|
142
|
+
tokens = args.strip().split()
|
|
143
|
+
|
|
144
|
+
# --list flag
|
|
145
|
+
if "--list" in tokens:
|
|
146
|
+
cmds = sorted(_REGISTRY.keys())
|
|
147
|
+
lines = [f"All commands ({len(cmds)}):", "=" * 50]
|
|
148
|
+
for i, name in enumerate(cmds):
|
|
149
|
+
usage = get_usage(name)
|
|
150
|
+
short_desc = _REGISTRY[name].__doc__ or ""
|
|
151
|
+
short_desc = short_desc.strip().split("\n")[0][:60] if short_desc else ""
|
|
152
|
+
lines.append(f" {name:<22} {short_desc}")
|
|
153
|
+
return "\n".join(lines)
|
|
154
|
+
|
|
155
|
+
# --search flag
|
|
156
|
+
search_term = None
|
|
157
|
+
for t in tokens:
|
|
158
|
+
if t.startswith("--search="):
|
|
159
|
+
search_term = t[9:].lower()
|
|
160
|
+
if search_term:
|
|
161
|
+
matches = []
|
|
162
|
+
for name, handler in sorted(_REGISTRY.items()):
|
|
163
|
+
doc = (handler.__doc__ or "").lower()
|
|
164
|
+
if search_term in name.lower() or search_term in doc:
|
|
165
|
+
short = (handler.__doc__ or "").strip().split("\n")[0][:60]
|
|
166
|
+
matches.append(f" {name:<22} {short}")
|
|
167
|
+
if matches:
|
|
168
|
+
return f"Search results for '{search_term}':\n" + "\n".join(matches)
|
|
169
|
+
return f"No commands matching '{search_term}'."
|
|
170
|
+
|
|
171
|
+
# --category flag
|
|
172
|
+
for t in tokens:
|
|
173
|
+
if t.startswith("--category="):
|
|
174
|
+
cat = t[11:].strip('"\'')
|
|
175
|
+
# Case-insensitive match
|
|
176
|
+
matched_cat = next(
|
|
177
|
+
(k for k in _CATEGORIES if k.lower() == cat.lower()), None
|
|
178
|
+
)
|
|
179
|
+
if matched_cat is None:
|
|
180
|
+
cats = "\n".join(f" {k}" for k in _CATEGORIES)
|
|
181
|
+
return f"Category '{cat}' not found. Available:\n{cats}"
|
|
182
|
+
cmds = _CATEGORIES[matched_cat]
|
|
183
|
+
lines = [f"Category: {matched_cat}", "-" * 40]
|
|
184
|
+
for name in cmds:
|
|
185
|
+
handler = _REGISTRY.get(name)
|
|
186
|
+
if handler:
|
|
187
|
+
short = (handler.__doc__ or "").strip().split("\n")[0][:60]
|
|
188
|
+
lines.append(f" {name:<22} {short}")
|
|
189
|
+
else:
|
|
190
|
+
lines.append(f" {name:<22} (not loaded)")
|
|
191
|
+
return "\n".join(lines)
|
|
192
|
+
|
|
193
|
+
# Specific command help
|
|
194
|
+
cmd_name = " ".join(tokens).strip() if tokens else ""
|
|
195
|
+
if cmd_name and cmd_name in _REGISTRY:
|
|
196
|
+
handler = _REGISTRY[cmd_name]
|
|
197
|
+
usage = get_usage(cmd_name)
|
|
198
|
+
doc = (handler.__doc__ or "").strip()
|
|
199
|
+
examples = _EXAMPLES.get(cmd_name, [])
|
|
200
|
+
|
|
201
|
+
lines = [
|
|
202
|
+
f"Command: {cmd_name}",
|
|
203
|
+
"=" * 50,
|
|
204
|
+
f"Usage: {usage}",
|
|
205
|
+
"",
|
|
206
|
+
]
|
|
207
|
+
if doc:
|
|
208
|
+
lines += [doc, ""]
|
|
209
|
+
if examples:
|
|
210
|
+
lines += ["Examples:"]
|
|
211
|
+
for ex in examples:
|
|
212
|
+
lines.append(f" {ex}")
|
|
213
|
+
return "\n".join(lines)
|
|
214
|
+
|
|
215
|
+
# No args — show category overview
|
|
216
|
+
lines = [
|
|
217
|
+
"OpenStat Help — type 'help <command>' for details",
|
|
218
|
+
"=" * 55,
|
|
219
|
+
"Options: help --list | help --search=<kw> | help --category=<name>",
|
|
220
|
+
"",
|
|
221
|
+
"Command Categories:",
|
|
222
|
+
"-" * 55,
|
|
223
|
+
]
|
|
224
|
+
for cat, cmds in _CATEGORIES.items():
|
|
225
|
+
available = [c for c in cmds if c in _REGISTRY]
|
|
226
|
+
if available:
|
|
227
|
+
lines.append(f" {cat:<30} ({len(available)} commands)")
|
|
228
|
+
lines += [
|
|
229
|
+
"",
|
|
230
|
+
"Quick start:",
|
|
231
|
+
" load data.csv",
|
|
232
|
+
" describe",
|
|
233
|
+
" ols outcome predictor1 predictor2",
|
|
234
|
+
" plot coef",
|
|
235
|
+
" export pdf",
|
|
236
|
+
]
|
|
237
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""i18n command: locale get|set <code>."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from openstat.commands.base import command
|
|
6
|
+
from openstat.session import Session
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@command("locale", usage="locale [get | set <code>]")
|
|
10
|
+
def cmd_locale(session: Session, args: str) -> str:
|
|
11
|
+
"""Get or set the display language.
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
locale — show current locale
|
|
15
|
+
locale get — show current locale
|
|
16
|
+
locale set tr — switch to Turkish
|
|
17
|
+
locale set en — switch to English
|
|
18
|
+
"""
|
|
19
|
+
from openstat.i18n import get_locale, set_locale, _STRINGS
|
|
20
|
+
|
|
21
|
+
tokens = args.strip().split()
|
|
22
|
+
subcmd = tokens[0].lower() if tokens else "get"
|
|
23
|
+
|
|
24
|
+
if subcmd in ("get", ""):
|
|
25
|
+
return f"Current locale: {get_locale()}"
|
|
26
|
+
|
|
27
|
+
elif subcmd == "set":
|
|
28
|
+
if len(tokens) < 2:
|
|
29
|
+
available = ", ".join(sorted(_STRINGS))
|
|
30
|
+
return f"Usage: locale set <code> Available: {available}"
|
|
31
|
+
code = tokens[1].lower()
|
|
32
|
+
try:
|
|
33
|
+
set_locale(code)
|
|
34
|
+
return f"Locale set to: {code}"
|
|
35
|
+
except ValueError as exc:
|
|
36
|
+
return str(exc)
|
|
37
|
+
|
|
38
|
+
elif subcmd == "list":
|
|
39
|
+
available = ", ".join(sorted(_STRINGS))
|
|
40
|
+
return f"Available locales: {available}"
|
|
41
|
+
|
|
42
|
+
else:
|
|
43
|
+
return "Usage: locale [get | set <code> | list]"
|