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.
Files changed (143) hide show
  1. openstat/__init__.py +3 -0
  2. openstat/__main__.py +4 -0
  3. openstat/backends/__init__.py +16 -0
  4. openstat/backends/duckdb_backend.py +70 -0
  5. openstat/backends/polars_backend.py +52 -0
  6. openstat/cli.py +92 -0
  7. openstat/commands/__init__.py +82 -0
  8. openstat/commands/adv_stat_cmds.py +1255 -0
  9. openstat/commands/advanced_ml_cmds.py +576 -0
  10. openstat/commands/advreg_cmds.py +207 -0
  11. openstat/commands/alias_cmds.py +135 -0
  12. openstat/commands/arch_cmds.py +82 -0
  13. openstat/commands/arules_cmds.py +111 -0
  14. openstat/commands/automodel_cmds.py +212 -0
  15. openstat/commands/backend_cmds.py +82 -0
  16. openstat/commands/base.py +170 -0
  17. openstat/commands/bayes_cmds.py +71 -0
  18. openstat/commands/causal_cmds.py +269 -0
  19. openstat/commands/cluster_cmds.py +152 -0
  20. openstat/commands/data_cmds.py +996 -0
  21. openstat/commands/datamanip_cmds.py +672 -0
  22. openstat/commands/dataquality_cmds.py +174 -0
  23. openstat/commands/datetime_cmds.py +176 -0
  24. openstat/commands/dimreduce_cmds.py +184 -0
  25. openstat/commands/discrete_cmds.py +149 -0
  26. openstat/commands/dsl_cmds.py +143 -0
  27. openstat/commands/epi_cmds.py +93 -0
  28. openstat/commands/equiv_tobit_cmds.py +94 -0
  29. openstat/commands/esttab_cmds.py +196 -0
  30. openstat/commands/export_beamer_cmds.py +142 -0
  31. openstat/commands/export_cmds.py +201 -0
  32. openstat/commands/export_extra_cmds.py +240 -0
  33. openstat/commands/factor_cmds.py +180 -0
  34. openstat/commands/groupby_cmds.py +155 -0
  35. openstat/commands/help_cmds.py +237 -0
  36. openstat/commands/i18n_cmds.py +43 -0
  37. openstat/commands/import_extra_cmds.py +561 -0
  38. openstat/commands/influence_cmds.py +134 -0
  39. openstat/commands/iv_cmds.py +106 -0
  40. openstat/commands/manova_cmds.py +105 -0
  41. openstat/commands/mediate_cmds.py +233 -0
  42. openstat/commands/meta_cmds.py +284 -0
  43. openstat/commands/mi_cmds.py +228 -0
  44. openstat/commands/mixed_cmds.py +79 -0
  45. openstat/commands/mixture_changepoint_cmds.py +166 -0
  46. openstat/commands/ml_adv_cmds.py +147 -0
  47. openstat/commands/ml_cmds.py +178 -0
  48. openstat/commands/model_eval_cmds.py +142 -0
  49. openstat/commands/network_cmds.py +288 -0
  50. openstat/commands/nlquery_cmds.py +161 -0
  51. openstat/commands/nonparam_cmds.py +149 -0
  52. openstat/commands/outreg_cmds.py +247 -0
  53. openstat/commands/panel_cmds.py +141 -0
  54. openstat/commands/pdf_cmds.py +226 -0
  55. openstat/commands/pipeline_cmds.py +319 -0
  56. openstat/commands/plot_cmds.py +189 -0
  57. openstat/commands/plugin_cmds.py +79 -0
  58. openstat/commands/posthoc_cmds.py +153 -0
  59. openstat/commands/power_cmds.py +172 -0
  60. openstat/commands/profile_cmds.py +246 -0
  61. openstat/commands/rbridge_cmds.py +81 -0
  62. openstat/commands/regex_cmds.py +104 -0
  63. openstat/commands/report_cmds.py +48 -0
  64. openstat/commands/repro_cmds.py +129 -0
  65. openstat/commands/resampling_cmds.py +109 -0
  66. openstat/commands/reshape_cmds.py +223 -0
  67. openstat/commands/sem_cmds.py +177 -0
  68. openstat/commands/stat_cmds.py +1040 -0
  69. openstat/commands/stata_import_cmds.py +215 -0
  70. openstat/commands/string_cmds.py +124 -0
  71. openstat/commands/surv_cmds.py +145 -0
  72. openstat/commands/survey_cmds.py +153 -0
  73. openstat/commands/textanalysis_cmds.py +192 -0
  74. openstat/commands/ts_adv_cmds.py +136 -0
  75. openstat/commands/ts_cmds.py +195 -0
  76. openstat/commands/tui_cmds.py +111 -0
  77. openstat/commands/ux_cmds.py +191 -0
  78. openstat/commands/validate_cmds.py +270 -0
  79. openstat/commands/viz_adv_cmds.py +312 -0
  80. openstat/commands/viz_extra_cmds.py +251 -0
  81. openstat/commands/watch_cmds.py +69 -0
  82. openstat/config.py +106 -0
  83. openstat/dsl/__init__.py +0 -0
  84. openstat/dsl/parser.py +332 -0
  85. openstat/dsl/tokenizer.py +105 -0
  86. openstat/i18n.py +120 -0
  87. openstat/io/__init__.py +0 -0
  88. openstat/io/loader.py +187 -0
  89. openstat/jupyter/__init__.py +18 -0
  90. openstat/jupyter/display.py +18 -0
  91. openstat/jupyter/magic.py +60 -0
  92. openstat/logging_config.py +59 -0
  93. openstat/plots/__init__.py +0 -0
  94. openstat/plots/plotter.py +437 -0
  95. openstat/plots/surv_plots.py +32 -0
  96. openstat/plots/ts_plots.py +59 -0
  97. openstat/plugins/__init__.py +5 -0
  98. openstat/plugins/manager.py +69 -0
  99. openstat/repl.py +457 -0
  100. openstat/reporting/__init__.py +0 -0
  101. openstat/reporting/eda.py +208 -0
  102. openstat/reporting/report.py +67 -0
  103. openstat/script_runner.py +319 -0
  104. openstat/session.py +133 -0
  105. openstat/stats/__init__.py +0 -0
  106. openstat/stats/advanced_regression.py +269 -0
  107. openstat/stats/arch_garch.py +84 -0
  108. openstat/stats/bayesian.py +103 -0
  109. openstat/stats/causal.py +258 -0
  110. openstat/stats/clustering.py +206 -0
  111. openstat/stats/discrete.py +311 -0
  112. openstat/stats/epidemiology.py +119 -0
  113. openstat/stats/equiv_tobit.py +163 -0
  114. openstat/stats/factor.py +174 -0
  115. openstat/stats/imputation.py +282 -0
  116. openstat/stats/influence.py +78 -0
  117. openstat/stats/iv.py +131 -0
  118. openstat/stats/manova.py +124 -0
  119. openstat/stats/mixed.py +128 -0
  120. openstat/stats/ml.py +275 -0
  121. openstat/stats/ml_advanced.py +117 -0
  122. openstat/stats/model_eval.py +183 -0
  123. openstat/stats/models.py +1342 -0
  124. openstat/stats/nonparametric.py +130 -0
  125. openstat/stats/panel.py +179 -0
  126. openstat/stats/power.py +295 -0
  127. openstat/stats/resampling.py +203 -0
  128. openstat/stats/survey.py +213 -0
  129. openstat/stats/survival.py +196 -0
  130. openstat/stats/timeseries.py +142 -0
  131. openstat/stats/ts_advanced.py +114 -0
  132. openstat/types.py +11 -0
  133. openstat/web/__init__.py +1 -0
  134. openstat/web/app.py +117 -0
  135. openstat/web/session_manager.py +73 -0
  136. openstat/web/static/app.js +117 -0
  137. openstat/web/static/index.html +38 -0
  138. openstat/web/static/style.css +103 -0
  139. openstat_cli-1.0.0.dist-info/METADATA +748 -0
  140. openstat_cli-1.0.0.dist-info/RECORD +143 -0
  141. openstat_cli-1.0.0.dist-info/WHEEL +4 -0
  142. openstat_cli-1.0.0.dist-info/entry_points.txt +2 -0
  143. 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]"