docxdriver 0.3.3__tar.gz → 0.4.0__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.
Files changed (84) hide show
  1. {docxdriver-0.3.3 → docxdriver-0.4.0}/Cargo.lock +13 -5
  2. {docxdriver-0.3.3 → docxdriver-0.4.0}/Cargo.toml +1 -1
  3. {docxdriver-0.3.3 → docxdriver-0.4.0}/PKG-INFO +32 -26
  4. docxdriver-0.4.0/README.md +87 -0
  5. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/Cargo.toml +6 -0
  6. docxdriver-0.4.0/crates/docxdriver-core/LICENSE +21 -0
  7. docxdriver-0.4.0/crates/docxdriver-core/src/facade.rs +200 -0
  8. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/lib.rs +4 -1
  9. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-python/Cargo.toml +2 -0
  10. docxdriver-0.4.0/crates/docxdriver-python/README.md +87 -0
  11. {docxdriver-0.3.3 → docxdriver-0.4.0/crates/docxdriver-python}/python/docxdriver/__init__.py +2 -1
  12. docxdriver-0.4.0/crates/docxdriver-python/python/docxdriver/document.py +126 -0
  13. docxdriver-0.4.0/crates/docxdriver-python/python/docxdriver/plan.py +570 -0
  14. docxdriver-0.4.0/crates/docxdriver-python/src/lib.rs +199 -0
  15. docxdriver-0.4.0/crates/docxdriver-python/tests/test_document.py +75 -0
  16. docxdriver-0.4.0/crates/docxdriver-python/tests/test_plan.py +40 -0
  17. {docxdriver-0.3.3/crates/docxdriver-python → docxdriver-0.4.0}/python/docxdriver/__init__.py +2 -1
  18. docxdriver-0.4.0/python/docxdriver/document.py +126 -0
  19. docxdriver-0.4.0/python/docxdriver/plan.py +570 -0
  20. docxdriver-0.3.3/README.md +0 -81
  21. docxdriver-0.3.3/crates/docxdriver-core/examples/list_scaling.rs +0 -71
  22. docxdriver-0.3.3/crates/docxdriver-core/examples/plan_scaling.rs +0 -43
  23. docxdriver-0.3.3/crates/docxdriver-core/src/bin/generate_typed_schema.rs +0 -172
  24. docxdriver-0.3.3/crates/docxdriver-core/src/html/README.md +0 -14
  25. docxdriver-0.3.3/crates/docxdriver-core/tests/README.md +0 -35
  26. docxdriver-0.3.3/crates/docxdriver-core/tests/fixtures/projection-parity/features.json +0 -33
  27. docxdriver-0.3.3/crates/docxdriver-core/tests/fixtures/projection-parity/parity.docx +0 -0
  28. docxdriver-0.3.3/crates/docxdriver-core/tests/fixtures/projection-parity/prior-projection.json +0 -1393
  29. docxdriver-0.3.3/crates/docxdriver-core/tests/fixtures/ts-reference.docx +0 -0
  30. docxdriver-0.3.3/crates/docxdriver-core/tests/formatting_fidelity.rs +0 -1148
  31. docxdriver-0.3.3/crates/docxdriver-core/tests/html_e2e.rs +0 -983
  32. docxdriver-0.3.3/crates/docxdriver-core/tests/interop.rs +0 -99
  33. docxdriver-0.3.3/crates/docxdriver-core/tests/package_layout.rs +0 -126
  34. docxdriver-0.3.3/crates/docxdriver-core/tests/projection_parity.rs +0 -531
  35. docxdriver-0.3.3/crates/docxdriver-core/tests/spike.rs +0 -44
  36. docxdriver-0.3.3/crates/docxdriver-core/tests/typed_api.rs +0 -2553
  37. docxdriver-0.3.3/crates/docxdriver-python/README.md +0 -81
  38. docxdriver-0.3.3/crates/docxdriver-python/python/docxdriver/repl/__init__.py +0 -7
  39. docxdriver-0.3.3/crates/docxdriver-python/python/docxdriver/repl/host.py +0 -562
  40. docxdriver-0.3.3/crates/docxdriver-python/python/docxdriver/repl/prelude.py +0 -3
  41. docxdriver-0.3.3/crates/docxdriver-python/python/docxdriver/repl/quotes.py +0 -325
  42. docxdriver-0.3.3/crates/docxdriver-python/src/lib.rs +0 -62
  43. docxdriver-0.3.3/crates/docxdriver-python/tests/test_host.py +0 -357
  44. docxdriver-0.3.3/python/docxdriver/repl/__init__.py +0 -7
  45. docxdriver-0.3.3/python/docxdriver/repl/host.py +0 -562
  46. docxdriver-0.3.3/python/docxdriver/repl/prelude.py +0 -3
  47. docxdriver-0.3.3/python/docxdriver/repl/quotes.py +0 -325
  48. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/README.md +0 -0
  49. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/assets/blank.docx +0 -0
  50. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/api.rs +0 -0
  51. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/comments.rs +0 -0
  52. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/equations.rs +0 -0
  53. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/headers.rs +0 -0
  54. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/layout.rs +0 -0
  55. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/lists.rs +0 -0
  56. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/mod.rs +0 -0
  57. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/reading.rs +0 -0
  58. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/readonly.rs +0 -0
  59. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/revisions.rs +0 -0
  60. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/sections.rs +0 -0
  61. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/table_structure.rs +0 -0
  62. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/tables.rs +0 -0
  63. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/commands/typed.rs +0 -0
  64. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/document.rs +0 -0
  65. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/assets.rs +0 -0
  66. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/build.rs +0 -0
  67. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/chrome.rs +0 -0
  68. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/compact.rs +0 -0
  69. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/ctx.rs +0 -0
  70. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/input.rs +0 -0
  71. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/mathml_omml.rs +0 -0
  72. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/mod.rs +0 -0
  73. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/omml_latex.rs +0 -0
  74. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/omml_mathml.rs +0 -0
  75. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/parse.rs +0 -0
  76. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/render.rs +0 -0
  77. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/styles.rs +0 -0
  78. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/html/surface.rs +0 -0
  79. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/media.rs +0 -0
  80. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/outcome.rs +0 -0
  81. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/package.rs +0 -0
  82. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-core/src/segments.rs +0 -0
  83. {docxdriver-0.3.3 → docxdriver-0.4.0}/crates/docxdriver-python/.gitignore +0 -0
  84. {docxdriver-0.3.3 → docxdriver-0.4.0}/pyproject.toml +0 -0
@@ -416,7 +416,7 @@ dependencies = [
416
416
 
417
417
  [[package]]
418
418
  name = "docxdriver-cli"
419
- version = "0.3.3"
419
+ version = "0.4.0"
420
420
  dependencies = [
421
421
  "clap",
422
422
  "docxdriver-core",
@@ -427,7 +427,7 @@ dependencies = [
427
427
 
428
428
  [[package]]
429
429
  name = "docxdriver-core"
430
- version = "0.3.3"
430
+ version = "0.4.0"
431
431
  dependencies = [
432
432
  "hex",
433
433
  "latex2mathml",
@@ -444,7 +444,7 @@ dependencies = [
444
444
 
445
445
  [[package]]
446
446
  name = "docxdriver-mcp"
447
- version = "0.3.3"
447
+ version = "0.4.0"
448
448
  dependencies = [
449
449
  "axum",
450
450
  "cap-std",
@@ -460,18 +460,26 @@ dependencies = [
460
460
  "tokio-util",
461
461
  ]
462
462
 
463
+ [[package]]
464
+ name = "docxdriver-monty-repl"
465
+ version = "0.4.0"
466
+ dependencies = [
467
+ "docxdriver-core",
468
+ ]
469
+
463
470
  [[package]]
464
471
  name = "docxdriver-python"
465
- version = "0.3.3"
472
+ version = "0.4.0"
466
473
  dependencies = [
467
474
  "docxdriver-core",
468
475
  "pyo3",
476
+ "serde",
469
477
  "serde_json",
470
478
  ]
471
479
 
472
480
  [[package]]
473
481
  name = "docxdriver-wasm"
474
- version = "0.3.3"
482
+ version = "0.4.0"
475
483
  dependencies = [
476
484
  "docxdriver-core",
477
485
  "js-sys",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/docxdriver-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.3"
6
+ version = "0.4.0"
7
7
  edition = "2021"
8
8
  license = "MIT"
9
9
  repository = "https://github.com/jhnoel/docxdriver"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: docxdriver
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Python SDK for the native Rust DOCX engine
5
5
  License-Expression: MIT
6
6
  Requires-Python: >=3.10
@@ -18,38 +18,44 @@ Install from PyPI:
18
18
  python -m pip install docxdriver
19
19
  ```
20
20
 
21
- The core API exposes `execute_request`, `canonical_plan_json`, and the convenience
22
- functions `run_command`, `run_plan`, and `run_request`. Results preserve document
23
- contents as Python `bytes`.
24
-
25
- The optional sandbox REPL harness is available as a subpackage in the same
26
- distribution:
21
+ `Document` works on `.docx` bytes; typed plan builders live in
22
+ `docxdriver.plan`:
27
23
 
28
24
  ```python
29
- from docxdriver.repl import NativeBridge, PRELUDE, REFERENCE
25
+ from pathlib import Path
26
+ from docxdriver import Document, create
27
+ from docxdriver.plan import Plan, ReplaceText
28
+
29
+ doc = Document(Path("contract.docx").read_bytes())
30
+ doc.markup() # body with the ids plans address
31
+ doc.comments(); doc.revisions(); doc.styles(); doc.assets()
32
+ doc.find("thirty")
33
+ review = doc.review(Plan(author="Reviewer", ops=[
34
+ ReplaceText(at="0F537164", select="thirty", with_="sixty"),
35
+ ]))
36
+ review.report # preview: each op's outcome
37
+ Path("contract.docx").write_bytes(doc.commit(review.key).data)
38
+
39
+ new = create(paragraphs=["Hello"]) # or create(html="<p>Hello</p>")
30
40
  ```
31
41
 
32
- `NativeBridge` provides async `init`, `call`, and `reset` requests for a host
33
- running a Python sandbox. The sandbox receives authoring classes and async
34
- wrappers from `PRELUDE`; `REFERENCE` provides their API documentation. The
35
- sandbox itself does not import the native extension. Native work runs through
36
- host callbacks outside the server event loop.
37
-
38
- The host confines reads and writes to the provided workspace, verifies source
39
- hashes and candidate output before commit, and performs atomic file replacement
40
- with read-back verification. Commit keys are bounded and single-use. Cancellation
41
- is drained before another host call so a write callback cannot be abandoned.
42
+ Commit keys are single-use. Natively `review.key` carries the key; in a
43
+ sandbox it is `None` and the host shows the key only to the model, so a
44
+ commit proves the review was read. Sandbox hosts written in Python build on
45
+ the stateless `review_plan(data, plan_json)` and
46
+ `commit_plan(data, plan_json, key)`, keeping each plan under a key they issue.
42
47
 
43
- Set `DOCXDRIVER_QUOTE_AUDIT_COMMENTS=1` to enable structured Quote/Term/Inline plan
44
- values and protected quotation audit comments. The provenance policy defaults to
45
- `permissive`; `controlled` and `authoritative` fail closed.
48
+ Engine diagnostics raise `ValueError`. The lower-level `execute_request`,
49
+ `canonical_plan_json`, `run_command`, `run_plan` and `run_request` remain.
46
50
 
47
- The prelude and API reference are generated from shared Pi templates. After
48
- changing those templates, regenerate them from the repository root:
51
+ `plan.py` and `document.py` stay within Monty's Python subset. Rust hosts get
52
+ the same text as `docxdriver_monty_repl::PLAN` and `DOCUMENT`, so sandbox code
53
+ and native code are interchangeable. The op dataclasses are generated from the
54
+ core schema; regenerate from the repository root with
55
+ `python scripts/generate-python-plan.py`.
49
56
 
50
- ```sh
51
- python scripts/generate-docxdriver-repl-prelude.py
52
- ```
57
+ Version 0.4.0 removed the `docxdriver.repl` sandbox host from this package. Its
58
+ source remains in `experimental/python-repl-host` for hosts to vendor.
53
59
 
54
60
  Build and install a wheel from the repository root:
55
61
 
@@ -0,0 +1,87 @@
1
+ # docxdriver
2
+
3
+ Python SDK for the Rust DOCX engine. It supports Python 3.10 and later and has no
4
+ Python runtime dependencies.
5
+
6
+ Install from PyPI:
7
+
8
+ ```sh
9
+ python -m pip install docxdriver
10
+ ```
11
+
12
+ `Document` works on `.docx` bytes; typed plan builders live in
13
+ `docxdriver.plan`:
14
+
15
+ ```python
16
+ from pathlib import Path
17
+ from docxdriver import Document, create
18
+ from docxdriver.plan import Plan, ReplaceText
19
+
20
+ doc = Document(Path("contract.docx").read_bytes())
21
+ doc.markup() # body with the ids plans address
22
+ doc.comments(); doc.revisions(); doc.styles(); doc.assets()
23
+ doc.find("thirty")
24
+ review = doc.review(Plan(author="Reviewer", ops=[
25
+ ReplaceText(at="0F537164", select="thirty", with_="sixty"),
26
+ ]))
27
+ review.report # preview: each op's outcome
28
+ Path("contract.docx").write_bytes(doc.commit(review.key).data)
29
+
30
+ new = create(paragraphs=["Hello"]) # or create(html="<p>Hello</p>")
31
+ ```
32
+
33
+ Commit keys are single-use. Natively `review.key` carries the key; in a
34
+ sandbox it is `None` and the host shows the key only to the model, so a
35
+ commit proves the review was read. Sandbox hosts written in Python build on
36
+ the stateless `review_plan(data, plan_json)` and
37
+ `commit_plan(data, plan_json, key)`, keeping each plan under a key they issue.
38
+
39
+ Engine diagnostics raise `ValueError`. The lower-level `execute_request`,
40
+ `canonical_plan_json`, `run_command`, `run_plan` and `run_request` remain.
41
+
42
+ `plan.py` and `document.py` stay within Monty's Python subset. Rust hosts get
43
+ the same text as `docxdriver_monty_repl::PLAN` and `DOCUMENT`, so sandbox code
44
+ and native code are interchangeable. The op dataclasses are generated from the
45
+ core schema; regenerate from the repository root with
46
+ `python scripts/generate-python-plan.py`.
47
+
48
+ Version 0.4.0 removed the `docxdriver.repl` sandbox host from this package. Its
49
+ source remains in `experimental/python-repl-host` for hosts to vendor.
50
+
51
+ Build and install a wheel from the repository root:
52
+
53
+ ```sh
54
+ python -m pip install "maturin>=1.9,<2"
55
+ maturin build --profile python --locked --manifest-path crates/docxdriver-python/Cargo.toml --out /tmp/docxdriver-wheels
56
+ python -m pip install /tmp/docxdriver-wheels/*.whl
57
+ ```
58
+
59
+ ## Publish to PyPI
60
+
61
+ The `Publish Python SDK` GitHub Actions workflow builds a source distribution
62
+ and wheels for Linux x86_64 and aarch64, macOS universal2, and Windows x86_64
63
+ when a `v*` tag is pushed. The tag must match the version in the root
64
+ `Cargo.toml` workspace package.
65
+
66
+ Before the first release, configure a PyPI trusted publisher for the
67
+ `docxdriver` project with these values:
68
+
69
+ - Owner: `jhnoel`
70
+ - Repository: `docxdriver`
71
+ - Workflow filename: `pypi.yml`
72
+ - GitHub environment: `pypi`
73
+
74
+ Create the `pypi` GitHub environment under the repository settings if you want
75
+ to add release approvals. Then update the workspace version, commit the change,
76
+ and push the matching tag (for example, `v0.2.7`). GitHub Actions publishes the
77
+ built distributions using PyPI's trusted publishing; no long-lived PyPI token is
78
+ needed.
79
+
80
+ The `python` Cargo profile keeps release optimizations and enables panic unwinding
81
+ so PyO3 can turn Rust panics into Python exceptions. ABI3 wheels support Python
82
+ 3.10 and later on the built operating system and architecture. To run the Python
83
+ SDK and host tests after installing the wheel:
84
+
85
+ ```sh
86
+ python -m unittest discover -s crates/docxdriver-python/tests
87
+ ```
@@ -4,7 +4,13 @@ description = "Artifact-first DOCX engine: (bytes, DocxCommand JSON) -> (bytes,
4
4
  version.workspace = true
5
5
  edition.workspace = true
6
6
  license.workspace = true
7
+ repository.workspace = true
7
8
  readme = "README.md"
9
+ keywords = ["docx", "ooxml", "word", "document", "track-changes"]
10
+ categories = ["text-processing", "parser-implementations"]
11
+ # Library sources and the runtime template only; tests, examples and the
12
+ # schema generator stay in the repository.
13
+ include = ["/src/**/*.rs", "!/src/bin/**", "/assets/blank.docx", "/README.md", "/LICENSE"]
8
14
 
9
15
  [dependencies]
10
16
  xmloxide = { version = "0.4.3", default-features = false }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 docxdriver contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,200 @@
1
+ //! Typed document API over the Request engine: one `.docx` in memory,
2
+ //! read, searched, reviewed and committed without JSON envelopes.
3
+ //!
4
+ //! ```text
5
+ //! let doc = Document::open(&bytes);
6
+ //! doc.markup(View::Markup)? body with the ids plans address
7
+ //! doc.comments()? / revisions()? / styles()? / assets()?
8
+ //! doc.find("thirty", Case::Sensitive)?
9
+ //! let plan = doc.plan(json)? bound to these bytes
10
+ //! let review = doc.review(&plan)? key + report, nothing written
11
+ //! let bytes = doc.commit(&plan, &review.key)?
12
+ //! ```
13
+ //!
14
+ //! The preview key binds bytes and plan, so commit refuses anything that
15
+ //! was not reviewed against these exact bytes. No state lives between calls.
16
+
17
+ use serde_json::Value;
18
+
19
+ use crate::api::{
20
+ execute_command, execute_plan, parse_plan_json, Command, CommandRequest, CommandResult,
21
+ CreateCommand, Diagnostic, FindCommand, Plan, PlanReport, PlanRequest, PlanResult,
22
+ PreviewKey, ReadCommand, SourceHash,
23
+ };
24
+
25
+ /// Which text of the body to project.
26
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
27
+ pub enum View {
28
+ /// Tracked changes shown as `<ins>`/`<del>`.
29
+ Markup,
30
+ /// As if every change were accepted.
31
+ Final,
32
+ /// As if every change were rejected.
33
+ Original,
34
+ }
35
+
36
+ /// Case handling for [`Document::find`].
37
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
38
+ pub enum Case {
39
+ Sensitive,
40
+ Insensitive,
41
+ }
42
+
43
+ /// What [`create`] builds a new document from.
44
+ #[derive(Debug, Clone, PartialEq)]
45
+ pub enum Source {
46
+ Paragraphs(Vec<String>),
47
+ Html(String),
48
+ }
49
+
50
+ /// A previewed plan: commit it with `key`; `report` says what each op did.
51
+ #[derive(Debug, Clone, PartialEq)]
52
+ pub struct Review {
53
+ pub key: PreviewKey,
54
+ pub report: PlanReport,
55
+ }
56
+
57
+ /// A `.docx` held as bytes.
58
+ #[derive(Debug, Clone, Copy)]
59
+ pub struct Document<'a> {
60
+ bytes: &'a [u8],
61
+ }
62
+
63
+ /// Builds a new `.docx`.
64
+ pub fn create(source: Source) -> Result<Vec<u8>, Diagnostic> {
65
+ let command = match source {
66
+ Source::Paragraphs(paragraphs) => CreateCommand { paragraphs, html: None },
67
+ Source::Html(html) => CreateCommand { paragraphs: Vec::new(), html: Some(html) },
68
+ };
69
+ match run(None, Command::Create(command)) {
70
+ CommandResult::Completed { bytes: Some(bytes), .. } => Ok(bytes),
71
+ CommandResult::Completed { .. } => Err(engine("create returned no document")),
72
+ CommandResult::Rejected { diagnostic, .. } => Err(diagnostic),
73
+ }
74
+ }
75
+
76
+ impl<'a> Document<'a> {
77
+ pub fn open(bytes: &'a [u8]) -> Self {
78
+ Self { bytes }
79
+ }
80
+
81
+ /// The body as HTML-like markup with stable paragraph ids.
82
+ pub fn markup(&self, view: View) -> Result<String, Diagnostic> {
83
+ let view = match view {
84
+ View::Markup => "markup",
85
+ View::Final => "final",
86
+ View::Original => "original",
87
+ };
88
+ let result = self.read("document", Some(view))?;
89
+ match result.get("markup") {
90
+ Some(Value::String(markup)) => Ok(markup.clone()),
91
+ _ => Err(engine("document read returned no markup")),
92
+ }
93
+ }
94
+
95
+ /// Equations by paragraph, e.g. `{"at": "0F537164", "equation": 1, ...}`,
96
+ /// the addresses `replace_equation`/`delete_equation` take.
97
+ pub fn equations(&self) -> Result<Vec<Value>, Diagnostic> {
98
+ Ok(array(self.read("document", None)?, "equations"))
99
+ }
100
+
101
+ /// Comment threads with anchors and replies.
102
+ pub fn comments(&self) -> Result<Vec<Value>, Diagnostic> {
103
+ self.list("comments")
104
+ }
105
+
106
+ /// Tracked changes; their ids feed `AcceptRevision`/`RejectRevision`.
107
+ pub fn revisions(&self) -> Result<Vec<Value>, Diagnostic> {
108
+ self.list("revisions")
109
+ }
110
+
111
+ /// Style definitions.
112
+ pub fn styles(&self) -> Result<Vec<Value>, Diagnostic> {
113
+ self.list("styles")
114
+ }
115
+
116
+ /// Embedded images and media.
117
+ pub fn assets(&self) -> Result<Vec<Value>, Diagnostic> {
118
+ self.list("assets")
119
+ }
120
+
121
+ /// Paragraphs containing `query`, with their ids.
122
+ pub fn find(&self, query: &str, case: Case) -> Result<Vec<Value>, Diagnostic> {
123
+ let command = FindCommand {
124
+ query: query.into(),
125
+ ignore_case: case == Case::Insensitive,
126
+ view: None,
127
+ };
128
+ let result = completed(run(Some(self.bytes), Command::Find(command)))?;
129
+ Ok(array(result, "matches"))
130
+ }
131
+
132
+ /// Parses a plan's JSON and binds it to these bytes; any `base` given
133
+ /// is replaced. E.g. `{"author":"A","change_mode":"track","ops":[...]}`.
134
+ pub fn plan(&self, json: &str) -> Result<Plan, Diagnostic> {
135
+ let mut value: Value = serde_json::from_str(json).map_err(|error| invalid(error.to_string()))?;
136
+ let Some(object) = value.as_object_mut() else {
137
+ return Err(invalid("a plan is a JSON object".into()));
138
+ };
139
+ object.insert("base".into(), Value::String(SourceHash::from_bytes(self.bytes).as_str()));
140
+ parse_plan_json(&value.to_string())
141
+ }
142
+
143
+ /// Previews `plan`; writes nothing.
144
+ pub fn review(&self, plan: &Plan) -> Result<Review, Diagnostic> {
145
+ match self.execute(plan, None) {
146
+ PlanResult::Previewed { preview_key, report, .. } => Ok(Review { key: preview_key, report }),
147
+ PlanResult::Committed { .. } => Err(engine("preview committed")),
148
+ PlanResult::Rejected { diagnostic, .. } => Err(diagnostic),
149
+ }
150
+ }
151
+
152
+ /// Applies a reviewed plan, returning the new document's bytes.
153
+ pub fn commit(&self, plan: &Plan, key: &PreviewKey) -> Result<Vec<u8>, Diagnostic> {
154
+ match self.execute(plan, Some(key.clone())) {
155
+ PlanResult::Committed { bytes, .. } => Ok(bytes),
156
+ PlanResult::Previewed { .. } => Err(engine("commit only previewed")),
157
+ PlanResult::Rejected { diagnostic, .. } => Err(diagnostic),
158
+ }
159
+ }
160
+
161
+ fn execute(&self, plan: &Plan, preview_key: Option<PreviewKey>) -> PlanResult {
162
+ execute_plan(self.bytes, &PlanRequest { plan: plan.clone(), preview_key })
163
+ }
164
+
165
+ // One listing read, e.g. "comments" → result["comments"].
166
+ fn list(&self, part: &str) -> Result<Vec<Value>, Diagnostic> {
167
+ Ok(array(self.read(part, None)?, part))
168
+ }
169
+
170
+ fn read(&self, part: &str, view: Option<&str>) -> Result<Value, Diagnostic> {
171
+ let command = ReadCommand { read_kind: Some(part.into()), view: view.map(Into::into) };
172
+ completed(run(Some(self.bytes), Command::Read(command)))
173
+ }
174
+ }
175
+
176
+ fn run(input: Option<&[u8]>, command: Command) -> CommandResult {
177
+ execute_command(input, &CommandRequest { command, expected_source: None })
178
+ }
179
+
180
+ fn completed(result: CommandResult) -> Result<Value, Diagnostic> {
181
+ match result {
182
+ CommandResult::Completed { result, .. } => Ok(result.unwrap_or(Value::Null)),
183
+ CommandResult::Rejected { diagnostic, .. } => Err(diagnostic),
184
+ }
185
+ }
186
+
187
+ fn array(mut result: Value, field: &str) -> Vec<Value> {
188
+ match result.get_mut(field).map(Value::take) {
189
+ Some(Value::Array(items)) => items,
190
+ _ => Vec::new(),
191
+ }
192
+ }
193
+
194
+ fn invalid(message: String) -> Diagnostic {
195
+ Diagnostic { code: "invalid_plan".into(), message, path: None, span: None }
196
+ }
197
+
198
+ fn engine(message: &str) -> Diagnostic {
199
+ Diagnostic { code: "engine".into(), message: message.into(), path: None, span: None }
200
+ }
@@ -3,11 +3,13 @@
3
3
  //! The stable contract is the source-bound typed [`Request`] and [`RequestResult`]
4
4
  //! API. Requests are either a typed one-command envelope or a validated Plan;
5
5
  //! plans support canonical JSON/TOML, deterministic preview keys, atomic folds,
6
- //! typed diagnostics, and complete output metadata.
6
+ //! typed diagnostics, and complete output metadata. [`Document`] is the same
7
+ //! engine as plain methods on one document's bytes.
7
8
 
8
9
  mod api;
9
10
  mod commands;
10
11
  mod document;
12
+ mod facade;
11
13
  mod html;
12
14
  mod media;
13
15
  mod outcome;
@@ -15,3 +17,4 @@ mod package;
15
17
  mod segments;
16
18
 
17
19
  pub use api::*;
20
+ pub use facade::{create, Case, Document, Review, Source, View};
@@ -4,6 +4,7 @@ description = "Native Python bindings for docxdriver-core"
4
4
  version.workspace = true
5
5
  edition.workspace = true
6
6
  license.workspace = true
7
+ publish = false
7
8
  readme = "README.md"
8
9
 
9
10
  [lib]
@@ -13,4 +14,5 @@ crate-type = ["cdylib", "rlib"]
13
14
  [dependencies]
14
15
  docxdriver-core = { path = "../docxdriver-core" }
15
16
  pyo3 = { version = "0.27.2", features = ["abi3-py310"] }
17
+ serde = "1"
16
18
  serde_json = "1"
@@ -0,0 +1,87 @@
1
+ # docxdriver
2
+
3
+ Python SDK for the Rust DOCX engine. It supports Python 3.10 and later and has no
4
+ Python runtime dependencies.
5
+
6
+ Install from PyPI:
7
+
8
+ ```sh
9
+ python -m pip install docxdriver
10
+ ```
11
+
12
+ `Document` works on `.docx` bytes; typed plan builders live in
13
+ `docxdriver.plan`:
14
+
15
+ ```python
16
+ from pathlib import Path
17
+ from docxdriver import Document, create
18
+ from docxdriver.plan import Plan, ReplaceText
19
+
20
+ doc = Document(Path("contract.docx").read_bytes())
21
+ doc.markup() # body with the ids plans address
22
+ doc.comments(); doc.revisions(); doc.styles(); doc.assets()
23
+ doc.find("thirty")
24
+ review = doc.review(Plan(author="Reviewer", ops=[
25
+ ReplaceText(at="0F537164", select="thirty", with_="sixty"),
26
+ ]))
27
+ review.report # preview: each op's outcome
28
+ Path("contract.docx").write_bytes(doc.commit(review.key).data)
29
+
30
+ new = create(paragraphs=["Hello"]) # or create(html="<p>Hello</p>")
31
+ ```
32
+
33
+ Commit keys are single-use. Natively `review.key` carries the key; in a
34
+ sandbox it is `None` and the host shows the key only to the model, so a
35
+ commit proves the review was read. Sandbox hosts written in Python build on
36
+ the stateless `review_plan(data, plan_json)` and
37
+ `commit_plan(data, plan_json, key)`, keeping each plan under a key they issue.
38
+
39
+ Engine diagnostics raise `ValueError`. The lower-level `execute_request`,
40
+ `canonical_plan_json`, `run_command`, `run_plan` and `run_request` remain.
41
+
42
+ `plan.py` and `document.py` stay within Monty's Python subset. Rust hosts get
43
+ the same text as `docxdriver_monty_repl::PLAN` and `DOCUMENT`, so sandbox code
44
+ and native code are interchangeable. The op dataclasses are generated from the
45
+ core schema; regenerate from the repository root with
46
+ `python scripts/generate-python-plan.py`.
47
+
48
+ Version 0.4.0 removed the `docxdriver.repl` sandbox host from this package. Its
49
+ source remains in `experimental/python-repl-host` for hosts to vendor.
50
+
51
+ Build and install a wheel from the repository root:
52
+
53
+ ```sh
54
+ python -m pip install "maturin>=1.9,<2"
55
+ maturin build --profile python --locked --manifest-path crates/docxdriver-python/Cargo.toml --out /tmp/docxdriver-wheels
56
+ python -m pip install /tmp/docxdriver-wheels/*.whl
57
+ ```
58
+
59
+ ## Publish to PyPI
60
+
61
+ The `Publish Python SDK` GitHub Actions workflow builds a source distribution
62
+ and wheels for Linux x86_64 and aarch64, macOS universal2, and Windows x86_64
63
+ when a `v*` tag is pushed. The tag must match the version in the root
64
+ `Cargo.toml` workspace package.
65
+
66
+ Before the first release, configure a PyPI trusted publisher for the
67
+ `docxdriver` project with these values:
68
+
69
+ - Owner: `jhnoel`
70
+ - Repository: `docxdriver`
71
+ - Workflow filename: `pypi.yml`
72
+ - GitHub environment: `pypi`
73
+
74
+ Create the `pypi` GitHub environment under the repository settings if you want
75
+ to add release approvals. Then update the workspace version, commit the change,
76
+ and push the matching tag (for example, `v0.2.7`). GitHub Actions publishes the
77
+ built distributions using PyPI's trusted publishing; no long-lived PyPI token is
78
+ needed.
79
+
80
+ The `python` Cargo profile keeps release optimizations and enables panic unwinding
81
+ so PyO3 can turn Rust panics into Python exceptions. ABI3 wheels support Python
82
+ 3.10 and later on the built operating system and architecture. To run the Python
83
+ SDK and host tests after installing the wheel:
84
+
85
+ ```sh
86
+ python -m unittest discover -s crates/docxdriver-python/tests
87
+ ```
@@ -1,6 +1,7 @@
1
1
  """Python SDK for the docxdriver document engine."""
2
2
  import json
3
- from ._core import __version__, canonical_plan_json, execute_request
3
+ from ._core import __version__, canonical_plan_json, commit_plan, execute_request, review_plan
4
+ from .document import Document, Review, create
4
5
 
5
6
 
6
7
  def run_request(data, request):