licos-dev-cli 0.4.2__tar.gz → 0.4.4__tar.gz

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.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: licos-dev-cli
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: LICOS Dev CLI - generate files and call model capabilities
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: click>=8.1
7
- Requires-Dist: licos-dev-sdk>=0.4.2
7
+ Requires-Dist: licos-dev-sdk>=0.4.4
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "licos-dev-cli"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "LICOS Dev CLI - generate files and call model capabilities"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
11
- "licos-dev-sdk>=0.4.2",
11
+ "licos-dev-sdk>=0.4.4",
12
12
  "click>=8.1",
13
13
  ]
14
14
 
@@ -55,7 +55,7 @@ def text_input(value, path, name):
55
55
 
56
56
  @click.command("docx-structure")
57
57
  @click.option("--file", "source", required=True, type=click.Path(exists=True, dir_okay=False))
58
- @click.option("--scope", type=click.Choice(["blocks", "paragraphs"]), default="blocks", show_default=True)
58
+ @click.option("--scope", type=click.Choice(["blocks", "paragraphs", "tables"]), default="blocks", show_default=True)
59
59
  @click.option("--offset", type=click.IntRange(min=1), default=1, show_default=True)
60
60
  @click.option("--limit", type=click.IntRange(1, 200), default=20, show_default=True)
61
61
  @click.option("--max-chars", type=click.IntRange(1, 8000), default=200, show_default=True)
@@ -70,6 +70,21 @@ def docx_structure(**arguments):
70
70
  result(read_docx_structure(**arguments))
71
71
 
72
72
 
73
+ @click.command("docx-inspect")
74
+ @click.option("--file", "source", required=True, type=click.Path(exists=True, dir_okay=False))
75
+ @click.option("--scope", type=click.Choice(["all", "fields", "images", "tables"]), default="all", show_default=True)
76
+ @click.option("--offset", type=click.IntRange(min=1), default=1, show_default=True)
77
+ @click.option("--limit", type=click.IntRange(1, 200), default=20, show_default=True)
78
+ @sdk_errors
79
+ def docx_inspect(**arguments):
80
+ """Inspect native TOC fields, embedded/linked images and tables, including headers/footers.
81
+
82
+ Reports field update settings, not rendered pages. Continue each scope with next_offset.
83
+ """
84
+ from licos_dev_sdk import inspect_docx
85
+ result(inspect_docx(**arguments))
86
+
87
+
73
88
  @click.command("docx-target")
74
89
  @click.option("--file", "source", required=True, type=click.Path(exists=True, dir_okay=False))
75
90
  @target_options
@@ -141,7 +156,8 @@ def docx_table_set(text, text_file, **arguments):
141
156
  @click.command("docx-table-insert-rows")
142
157
  @edit_options
143
158
  @click.option("--table", required=True, type=click.IntRange(min=1))
144
- @click.option("--before", required=True, type=click.IntRange(min=1), help="One-based insertion position; row_count+1 appends.")
159
+ @click.option("--before", type=click.IntRange(min=1), help="Insert before this one-based row. Choose --before or --append.")
160
+ @click.option("--append", is_flag=True, help="Append after the last row; no row count calculation needed.")
145
161
  @click.option("--template-row", required=True, type=click.IntRange(min=1), help="Existing styled row; must not intersect a vertical merge.")
146
162
  @click.option("--input", "input_path", required=True, type=click.Path(exists=True, dir_okay=False), help="Two-dimensional JSON array of scalar values. Fill a merged cell once; count is inferred.")
147
163
  @sdk_errors
@@ -153,17 +169,35 @@ def docx_table_insert_rows(input_path, **arguments):
153
169
 
154
170
  @click.command("docx-verify")
155
171
  @click.option("--file", "source", required=True, type=click.Path(exists=True, dir_okay=False))
156
- @click.option("--input", "input_path", type=click.Path(exists=True, dir_okay=False), help="JSON array of target/text assertions and optional mode (equals, contains, absent).")
172
+ @click.option("--original", type=click.Path(exists=True, dir_okay=False), help="Compare non-body package parts (headers, footers, styles, images) and section settings with the original.")
173
+ @click.option("--input", "input_path", type=click.Path(exists=True, dir_okay=False), help="JSON array: target, exactly one of text/text_file, optional mode. Relative text_file paths use the JSON file's directory.")
174
+ @target_options
175
+ @click.option("--text", default=None, help="Expected text at one target; use this or --text-file instead of --input.")
176
+ @click.option("--text-file", type=click.Path(exists=True, dir_okay=False), help="UTF-8 expected text; preserves whitespace and line breaks.")
177
+ @click.option("--mode", type=click.Choice(["equals", "contains", "absent"]), help="Single-target comparison; defaults to equals.")
157
178
  @sdk_errors
158
- def docx_verify(source, input_path):
159
- """Verify exact positions. Without assertions only structure is checked, not content.
179
+ def docx_verify(source, original, input_path, paragraph, block, table, row, column, text, text_file, mode):
180
+ """Verify exact positions and optional original parts. No assertions means no content verification.
160
181
 
161
- Example: [{"target":{"table":1,"row":1,"column":1},"text":"Supplier: A"}]
182
+ Example: [{"target":{"block":3},"text_file":"expected.txt","mode":"equals"}]
162
183
 
163
- Exits 2 if an assertion fails. Does not certify rendered page layout.
184
+ Exits 2 if an assertion or original comparison fails. Does not certify rendered page layout.
164
185
  """
165
186
  from licos_dev_sdk import verify_docx
166
- report = verify_docx(source, expectations=json.loads(Path(input_path).read_text(encoding="utf-8")) if input_path else None)
187
+ position = target(paragraph, block, table, row, column)
188
+ single = bool(position) or text is not None or text_file is not None or mode is not None
189
+ if input_path is not None and single:
190
+ raise click.UsageError("use --input or a single target with --text/--text-file, not both")
191
+ expectations = json.loads(Path(input_path).read_text(encoding="utf-8")) if input_path else None
192
+ if single:
193
+ expectation = {"target": position, "mode": mode or "equals"}
194
+ if text is not None:
195
+ expectation["text"] = text
196
+ if text_file is not None:
197
+ expectation["text_file"] = text_file
198
+ expectations = [expectation]
199
+ report = verify_docx(source, original=original, expectations=expectations,
200
+ base_dir=Path(input_path).resolve().parent if input_path else Path.cwd())
167
201
  result(report)
168
202
  if not report["verified"]:
169
203
  raise click.exceptions.Exit(2)
@@ -171,4 +205,4 @@ def docx_verify(source, input_path):
171
205
 
172
206
  DOCX_COMMANDS = [docx, docx_template, docx_edit, docx_read, docx_structure, docx_target,
173
207
  docx_replace, docx_delete_blocks, docx_edit_structure, docx_table_set,
174
- docx_table_insert_rows, docx_verify]
208
+ docx_table_insert_rows, docx_verify, docx_inspect]
@@ -14,6 +14,7 @@ from .common import _read_input, _read_json, _json_option, _echo_json, _path_to_
14
14
  # ── CLI Group ────────────────────────────────────────────────────────────────
15
15
 
16
16
  @click.group()
17
+ @click.version_option(package_name="licos-dev-cli")
17
18
  def cli():
18
19
  """LICOS Dev - file generation and model CLI for AI agents."""
19
20
  pass
@@ -24,6 +24,22 @@ def invoke(*arguments):
24
24
  return json.loads(result.output)
25
25
 
26
26
 
27
+ def test_inspect_and_file_assertions_use_cli_contract(source, tmp_path, monkeypatch):
28
+ report = invoke("docx-inspect", "--file", source)
29
+ assert report["table_count"] == 1 and report["image_count"] == 0
30
+ folder = tmp_path / "checks"
31
+ folder.mkdir()
32
+ expected = folder / "expected.txt"
33
+ expected.write_text("账号:123", encoding="utf-8")
34
+ checks = folder / "assertions.json"
35
+ checks.write_text(json.dumps([{"target": {"block": 1}, "text_file": "expected.txt"}]), encoding="utf-8")
36
+ monkeypatch.chdir(tmp_path)
37
+ assert invoke("docx-verify", "--file", source, "--input", checks)["verified"]
38
+ assert invoke("docx-verify", "--file", source, "--block", 1, "--text-file", "checks/expected.txt")["verified"]
39
+ invalid = CliRunner().invoke(cli, ["docx-verify", "--file", str(source), "--block", "1", "--text", "x", "--text-file", str(expected)])
40
+ assert invalid.exit_code != 0 and "exactly one" in invalid.output
41
+
42
+
27
43
  def test_focused_commands_complete_read_replace_delete_and_verify(source, tmp_path):
28
44
  read = invoke("docx-structure", "--file", source)
29
45
  output = tmp_path / "filled.docx"
@@ -75,9 +91,52 @@ def test_bad_revision_reports_original_error_and_leaves_output_absent(source, tm
75
91
  def test_command_help_is_focused_and_preserves_constraints():
76
92
  runner = CliRunner()
77
93
  for name in ("docx-structure", "docx-target", "docx-replace", "docx-delete-blocks", "docx-edit-structure",
78
- "docx-table-set", "docx-table-insert-rows", "docx-verify"):
94
+ "docx-table-set", "docx-table-insert-rows", "docx-verify", "docx-inspect"):
79
95
  help_text = runner.invoke(cli, [name, "--help"])
80
96
  assert help_text.exit_code == 0
81
97
  assert len(help_text.output.encode("utf-8")) < 5000
82
98
  help_text = runner.invoke(cli, ["docx-table-insert-rows", "--help"]).output
83
99
  assert "--revision" in help_text and "--template-row" in help_text and "--count" not in help_text
100
+
101
+
102
+ def test_append_and_verify_with_original_and_text_file(source, tmp_path):
103
+ rows = tmp_path / "rows.json"
104
+ rows.write_text('[["pump",2]]', encoding="utf-8")
105
+ output = tmp_path / "rows.docx"
106
+ read = invoke("docx-structure", "--file", source, "--scope", "tables")
107
+ assert read["items"][0]["row_count"] == 1
108
+ invoke("docx-table-insert-rows", "--source", source, "--output", output, "--revision", read["revision"],
109
+ "--table", 1, "--append", "--template-row", 1, "--input", rows)
110
+ assert invoke("docx-verify", "--file", output, "--original", source, "--table", 1, "--row", 2, "--column", 1, "--text", "pump")["verified"]
111
+ replacement = tmp_path / "text.txt"
112
+ text = " 中文🙂\t空格\n" * 100 + " "
113
+ replacement.write_text(text, encoding="utf-8")
114
+ invoke("docx-replace", "--source", output, "--output", output, "--overwrite",
115
+ "--revision", invoke("docx-structure", "--file", output)["revision"],
116
+ "--block", 1, "--search", "账号:123", "--replace-file", replacement)
117
+ verified = invoke("docx-verify", "--file", output, "--original", source, "--block", 1, "--text-file", replacement)
118
+ assert verified["content_verified"] and verified["preservation_verified"]
119
+
120
+
121
+ @pytest.mark.parametrize("arguments", [
122
+ ["--block", "1"], ["--text", "x"], ["--mode", "contains"],
123
+ ["--block", "1", "--text", "x", "--input", "checks.json"],
124
+ ["--block", "1", "--text", "x", "--text-file", "text.txt"],
125
+ ])
126
+ def test_invalid_verification_options_are_reported(source, tmp_path, monkeypatch, arguments):
127
+ monkeypatch.chdir(tmp_path)
128
+ (tmp_path / "checks.json").write_text("[]", encoding="utf-8")
129
+ (tmp_path / "text.txt").write_text("x", encoding="utf-8")
130
+ result = CliRunner().invoke(cli, ["docx-verify", "--file", str(source), *arguments])
131
+ assert result.exit_code != 0
132
+ assert "Error:" in result.output
133
+
134
+
135
+ def test_original_comparison_failure_exits_two(source, tmp_path):
136
+ changed = tmp_path / "changed.docx"
137
+ doc = Document(source)
138
+ doc.sections[0].footer.paragraphs[0].text = "changed footer"
139
+ doc.save(changed)
140
+ failure = CliRunner().invoke(cli, ["docx-verify", "--file", str(changed), "--original", str(source)])
141
+ assert failure.exit_code == 2
142
+ assert not json.loads(failure.output)["preservation_verified"]
File without changes