workpytools 0.1.4__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 (48) hide show
  1. workpytools/__init__.py +1 -0
  2. workpytools/cli.py +76 -0
  3. workpytools/common/__init__.py +0 -0
  4. workpytools/common/browser_preview.py +40 -0
  5. workpytools/common/clipboard.py +318 -0
  6. workpytools/common/clustering.py +85 -0
  7. workpytools/common/config.py +44 -0
  8. workpytools/common/embedding.py +101 -0
  9. workpytools/common/help_gen.py +542 -0
  10. workpytools/common/logging.py +12 -0
  11. workpytools/common/markdown_html.py +15 -0
  12. workpytools/common/outline_parse.py +81 -0
  13. workpytools/common/output.py +71 -0
  14. workpytools/common/powerpoint.py +43 -0
  15. workpytools/common/shape_cluster.py +75 -0
  16. workpytools/common/table_shapes.py +157 -0
  17. workpytools/common/tabular.py +248 -0
  18. workpytools/common/textfile.py +90 -0
  19. workpytools/common/walk.py +133 -0
  20. workpytools/data/__init__.py +0 -0
  21. workpytools/data/synonym.tsv +3 -0
  22. workpytools/data/user.dic +2 -0
  23. workpytools/processing/__init__.py +0 -0
  24. workpytools/processing/base.py +23 -0
  25. workpytools/processing/clipfmt.py +143 -0
  26. workpytools/processing/clipmd.py +237 -0
  27. workpytools/processing/clipview.py +194 -0
  28. workpytools/processing/cwc.py +427 -0
  29. workpytools/processing/denoise.py +81 -0
  30. workpytools/processing/help.py +47 -0
  31. workpytools/processing/ikko.py +205 -0
  32. workpytools/processing/kukiri.py +94 -0
  33. workpytools/processing/lsdir.py +218 -0
  34. workpytools/processing/mdtsv.py +177 -0
  35. workpytools/processing/mokuji.py +91 -0
  36. workpytools/processing/nagasa.py +94 -0
  37. workpytools/processing/outline.py +85 -0
  38. workpytools/processing/profiler.py +277 -0
  39. workpytools/processing/seiretsu.py +111 -0
  40. workpytools/processing/tbl.py +358 -0
  41. workpytools/processing/touka.py +52 -0
  42. workpytools/processing/vv.py +147 -0
  43. workpytools-0.1.4.dist-info/METADATA +458 -0
  44. workpytools-0.1.4.dist-info/RECORD +48 -0
  45. workpytools-0.1.4.dist-info/WHEEL +5 -0
  46. workpytools-0.1.4.dist-info/entry_points.txt +20 -0
  47. workpytools-0.1.4.dist-info/licenses/LICENSE +21 -0
  48. workpytools-0.1.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,277 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ from pathlib import Path
6
+
7
+ from workpytools.common.clipboard import copy_text_to_clipboard, get_clipboard_text
8
+ from workpytools.common.tabular import (
9
+ DEFAULT_EMPTY_VALUES,
10
+ ColumnProfile,
11
+ Table,
12
+ format_top,
13
+ load_tables,
14
+ profile_columns,
15
+ read_csv_like,
16
+ )
17
+ from workpytools.processing.base import Processor
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ _HEADER = (
22
+ "source",
23
+ "sheet",
24
+ "column",
25
+ "rows",
26
+ "filled",
27
+ "empty",
28
+ "fill_rate",
29
+ "unique",
30
+ "unique_rate",
31
+ "is_filled",
32
+ "is_unique",
33
+ "key_score",
34
+ "top",
35
+ )
36
+
37
+
38
+ def _profile_row(source: str, sheet: str, profile: ColumnProfile) -> list[str]:
39
+ return [
40
+ source,
41
+ sheet,
42
+ profile.column,
43
+ str(profile.rows),
44
+ str(profile.filled),
45
+ str(profile.empty),
46
+ f"{profile.fill_rate:.4f}",
47
+ str(profile.unique),
48
+ f"{profile.unique_rate:.4f}",
49
+ str(profile.is_filled),
50
+ str(profile.is_unique),
51
+ f"{profile.key_score:.4f}",
52
+ format_top(profile.top),
53
+ ]
54
+
55
+
56
+ class ProfilerProcessor(Processor):
57
+ """Profile every column of tabular data (CSV/TSV/Excel/JSON): fill rate,
58
+ uniqueness, top values, and a key-likelihood score. Reads from a file or,
59
+ with no argument, from the clipboard (e.g. a range copied from Excel).
60
+ """
61
+
62
+ name = "profiler"
63
+ help = "表形式データの各列をプロファイルする(欠損・一意性・頻度上位・主キー候補)"
64
+
65
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
66
+ parser.add_argument(
67
+ "path",
68
+ nargs="*",
69
+ default=[],
70
+ help="対象ファイル(複数・グロブ可)。省略時はクリップボードのTSVを読む",
71
+ )
72
+ parser.add_argument("-o", "--output", default=None, help="出力先パス(.tsv / .xlsx)")
73
+ parser.add_argument(
74
+ "--clip", action="store_true", help="結果をクリップボードにコピーする"
75
+ )
76
+ parser.add_argument(
77
+ "--view", action="store_true", help="結果をブラウザでHTML表示する"
78
+ )
79
+ parser.add_argument("--sep", default=None, help="区切り文字(拡張子判定を上書き)")
80
+ parser.add_argument(
81
+ "--header",
82
+ type=int,
83
+ default=0,
84
+ help="ヘッダー行のインデックス(0始まり、既定0)",
85
+ )
86
+ parser.add_argument(
87
+ "--no-header", action="store_true", help="ヘッダーなしとして扱う"
88
+ )
89
+ parser.add_argument(
90
+ "--top", type=int, default=10, help="頻度上位の表示件数(既定10、0で非表示)"
91
+ )
92
+ parser.add_argument(
93
+ "--empty-values",
94
+ default=None,
95
+ help="空とみなす値のカンマ区切りリスト(既定値を置き換える)",
96
+ )
97
+ parser.add_argument(
98
+ "--no-default-empty-values",
99
+ action="store_true",
100
+ help="既定の空値リストを無効化し、Noneと空文字だけを空とみなす",
101
+ )
102
+
103
+ def run(self, args: argparse.Namespace) -> int:
104
+ empty_values = self._resolve_empty_values(args)
105
+
106
+ sources = self._resolve_sources(args)
107
+ has_excel = any(
108
+ Path(s).suffix.lower() == ".xlsx" for s, _ in sources if s != "(clipboard)"
109
+ )
110
+
111
+ rows: list[list[str]] = [list(_HEADER)] if has_excel else [
112
+ [h for h in _HEADER if h != "sheet"]
113
+ ]
114
+
115
+ for source_label, table_by_sheet in sources:
116
+ for sheet_name, table in table_by_sheet.items():
117
+ if not table.rows:
118
+ label = f"{source_label} [{sheet_name}]" if sheet_name else source_label
119
+ logger.warning("データ行が0件のためスキップします: %s", label)
120
+ continue
121
+ for profile in profile_columns(table, top_n=args.top, empty_values=empty_values):
122
+ row = _profile_row(source_label, sheet_name, profile)
123
+ if not has_excel:
124
+ row = [v for v, h in zip(row, _HEADER, strict=True) if h != "sheet"]
125
+ rows.append(row)
126
+
127
+ if len(rows) <= 1:
128
+ raise SystemExit("プロファイル対象の列がありません(入力が空の可能性があります)")
129
+
130
+ self._emit(rows, args)
131
+ return 0
132
+
133
+ def _resolve_empty_values(self, args: argparse.Namespace) -> frozenset[object]:
134
+ if args.no_default_empty_values:
135
+ base: frozenset[object] = frozenset({None, ""})
136
+ else:
137
+ base = DEFAULT_EMPTY_VALUES
138
+
139
+ if args.empty_values:
140
+ return frozenset(v.strip() for v in args.empty_values.split(","))
141
+ return base
142
+
143
+ def _resolve_sources(
144
+ self, args: argparse.Namespace
145
+ ) -> list[tuple[str, dict[str, Table]]]:
146
+ if not args.path:
147
+ try:
148
+ text = get_clipboard_text()
149
+ except Exception as exc:
150
+ raise SystemExit("クリップボードにテキストがありません") from exc
151
+ sep = args.sep if args.sep is not None else "\t"
152
+ header_row = None if args.no_header else args.header
153
+ table = read_csv_like(text, sep, header_row)
154
+ return [("(clipboard)", {"": table})]
155
+
156
+ from glob import glob
157
+
158
+ results = []
159
+ header_row = None if args.no_header else args.header
160
+ for pattern in args.path:
161
+ matches = glob(pattern) or [pattern]
162
+ for filename in matches:
163
+ path = Path(filename)
164
+ try:
165
+ tables = load_tables(path, args.sep, header_row)
166
+ except ValueError as exc:
167
+ raise SystemExit(str(exc)) from exc
168
+ except OSError as exc:
169
+ raise SystemExit(f"ファイルを読み込めません: {path}") from exc
170
+ results.append((str(path), tables))
171
+
172
+ if not results:
173
+ raise SystemExit("対象ファイルが見つかりません")
174
+ return results
175
+
176
+ def _emit(self, rows: list[list[str]], args: argparse.Namespace) -> None:
177
+ tsv_text = "\n".join("\t".join(row) for row in rows)
178
+
179
+ if args.clip:
180
+ copy_text_to_clipboard(tsv_text)
181
+ print("プロファイル結果をコピーしました")
182
+ return
183
+
184
+ if args.view:
185
+ self._view_in_browser(rows)
186
+ return
187
+
188
+ if args.output:
189
+ self._write_output(rows, tsv_text, args.output)
190
+ return
191
+
192
+ print(tsv_text)
193
+
194
+ def _write_output(self, rows: list[list[str]], tsv_text: str, output: str) -> None:
195
+ output_path = Path(output)
196
+ if output_path.suffix.lower() == ".xlsx":
197
+ self._write_excel(rows, output_path)
198
+ else:
199
+ output_path.write_text(tsv_text, encoding="utf-8")
200
+ logger.info("プロファイル結果を書き出しました: %s", output_path)
201
+ print(output_path)
202
+
203
+ def _write_excel(self, rows: list[list[str]], output_path: Path) -> None:
204
+ import xlsxwriter
205
+
206
+ header = rows[0]
207
+ bad_cols = {i for i, h in enumerate(header) if h in ("is_filled", "is_unique")}
208
+
209
+ with xlsxwriter.Workbook(str(output_path)) as workbook:
210
+ worksheet = workbook.add_worksheet("profile")
211
+ header_format = workbook.add_format(
212
+ {"bold": True, "align": "center", "border": 1}
213
+ )
214
+ worksheet.write_row(0, 0, header, header_format)
215
+
216
+ red_format = workbook.add_format({"bg_color": "#FFC7CE", "font_color": "#9C0006"})
217
+ for r, row in enumerate(rows[1:], start=1):
218
+ worksheet.write_row(r, 0, row)
219
+ for c in bad_cols:
220
+ if row[c] == "False":
221
+ worksheet.write(r, c, row[c], red_format)
222
+
223
+ worksheet.autofilter(0, 0, len(rows) - 1, len(header) - 1)
224
+
225
+ def _view_in_browser(self, rows: list[list[str]]) -> None:
226
+ from workpytools.common.browser_preview import write_and_open
227
+
228
+ html = _render_html_table(rows)
229
+ write_and_open(html, "workpytools_profiler_preview.html", no_open=False)
230
+
231
+
232
+ def _render_html_table(rows: list[list[str]]) -> str:
233
+ import html as html_module
234
+
235
+ header, *data_rows = rows
236
+ bad_cols = {i for i, h in enumerate(header) if h in ("is_filled", "is_unique")}
237
+
238
+ head_html = "".join(f"<th>{html_module.escape(h)}</th>" for h in header)
239
+ body_html = []
240
+ for row in data_rows:
241
+ cells = []
242
+ for i, value in enumerate(row):
243
+ cls = ' class="bad"' if i in bad_cols and value == "False" else ""
244
+ cells.append(f"<td{cls}>{html_module.escape(value)}</td>")
245
+ body_html.append(f"<tr>{''.join(cells)}</tr>")
246
+
247
+ style = """\
248
+ :root { color-scheme: light dark; }
249
+ body { font-family: -apple-system, "Segoe UI", sans-serif; margin: 1.5rem; }
250
+ table { border-collapse: collapse; width: 100%; }
251
+ th, td { border: 1px solid #999; padding: 0.3rem 0.6rem; text-align: left; }
252
+ th { position: sticky; top: 0; background: #eee; }
253
+ td.bad { background: #ffdcdc; }
254
+ @media (prefers-color-scheme: dark) {
255
+ body { background: #1e1e1e; color: #ddd; }
256
+ th { background: #333; }
257
+ td.bad { background: #5a2a2a; }
258
+ th, td { border-color: #555; }
259
+ }
260
+ """
261
+
262
+ return f"""<!doctype html>
263
+ <html>
264
+ <head>
265
+ <meta charset="utf-8">
266
+ <meta http-equiv="Cache-Control" content="no-store">
267
+ <title>profiler result</title>
268
+ <style>{style}</style>
269
+ </head>
270
+ <body>
271
+ <table>
272
+ <thead><tr>{head_html}</tr></thead>
273
+ <tbody>{"".join(body_html)}</tbody>
274
+ </table>
275
+ </body>
276
+ </html>
277
+ """
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+
6
+ from workpytools.common.powerpoint import (
7
+ NoActivePresentationError,
8
+ PowerPointNotRunningError,
9
+ get_active_presentation,
10
+ get_running_powerpoint,
11
+ )
12
+ from workpytools.common.table_shapes import (
13
+ GRID_TOLERANCE,
14
+ DuplicateGridPositionError,
15
+ GridShape,
16
+ column_and_row_sizes,
17
+ estimate_grid,
18
+ grid_centers,
19
+ )
20
+ from workpytools.processing.base import Processor
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _SELECTION_SHAPES = 2 # ppSelectionShapes
25
+
26
+
27
+ class SeiretsuProcessor(Processor):
28
+ """Realign selected shapes into a grid by position only. Unlike tbl's
29
+ compose direction, this never creates a table or deletes/creates
30
+ shapes -- each shape keeps its size, text, and formatting; only
31
+ Left/Top are rewritten so shapes' centers land on grid positions.
32
+ """
33
+
34
+ name = "seiretsu"
35
+ help = "選択したシェイプを表に変換せず格子状の位置に整列する"
36
+
37
+ def add_arguments(self, parser: argparse.ArgumentParser) -> None:
38
+ pass
39
+
40
+ def run(self, args: argparse.Namespace) -> int:
41
+ try:
42
+ app = get_running_powerpoint()
43
+ except PowerPointNotRunningError as exc:
44
+ raise SystemExit(
45
+ "PowerPointでプレゼンテーションを開いた状態で実行してください"
46
+ ) from exc
47
+
48
+ try:
49
+ get_active_presentation(app)
50
+ except NoActivePresentationError as exc:
51
+ raise SystemExit(
52
+ "PowerPointでプレゼンテーションを開いた状態で実行してください"
53
+ ) from exc
54
+
55
+ try:
56
+ selection = app.ActiveWindow.Selection
57
+ except Exception as exc:
58
+ raise SystemExit(f"選択状態の取得中にエラーが発生しました: {exc}") from exc
59
+
60
+ if selection.Type != _SELECTION_SHAPES:
61
+ raise SystemExit("対象を選択してから実行してください(2つ以上選択が必要です)")
62
+
63
+ shape_range = selection.ShapeRange
64
+ shapes = [shape_range.Item(i) for i in range(1, shape_range.Count + 1)]
65
+
66
+ if len(shapes) < 2:
67
+ raise SystemExit("対象を選択してから実行してください(2つ以上選択が必要です)")
68
+
69
+ grid_shapes = [
70
+ GridShape(left=s.Left, top=s.Top, width=s.Width, height=s.Height, ref=s)
71
+ for s in shapes
72
+ ]
73
+
74
+ try:
75
+ positions, n_rows, n_cols = estimate_grid(grid_shapes, GRID_TOLERANCE)
76
+ except DuplicateGridPositionError as exc:
77
+ raise SystemExit(str(exc)) from exc
78
+
79
+ missing = n_rows * n_cols - len(positions)
80
+ if missing:
81
+ logger.warning("歯抜けのグリッド位置: %d件", missing)
82
+
83
+ col_width, row_height = column_and_row_sizes(positions, n_rows, n_cols)
84
+
85
+ overall_left = min(s.left for s in grid_shapes)
86
+ overall_top = min(s.top for s in grid_shapes)
87
+
88
+ center_x, center_y = grid_centers(col_width, row_height, overall_left, overall_top)
89
+
90
+ try:
91
+ app.StartNewUndoEntry()
92
+ for pos in positions:
93
+ shape = pos.shape.ref
94
+ new_left = center_x[pos.col] - pos.shape.width / 2
95
+ new_top = center_y[pos.row] - pos.shape.height / 2
96
+ shape.Left = new_left # type: ignore[attr-defined]
97
+ shape.Top = new_top # type: ignore[attr-defined]
98
+ except Exception as exc:
99
+ raise SystemExit(
100
+ f"PowerPointの操作中にエラーが発生しました(Ctrl+Zで開始前の状態に戻せます): {exc}"
101
+ ) from exc
102
+
103
+ logger.info(
104
+ "対象シェイプ数=%d 推定グリッド=%d行%d列 歯抜け=%d",
105
+ len(shapes),
106
+ n_rows,
107
+ n_cols,
108
+ missing,
109
+ )
110
+ print(f"{len(shapes)}個のシェイプを{n_rows}行{n_cols}列に整列しました")
111
+ return 0