compile-pdf-cjd 0.1.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.
- compile_pdf_cjd-0.1.0/.coderabbit.yaml +29 -0
- compile_pdf_cjd-0.1.0/.cursor/rules/baseline.mdc +21 -0
- compile_pdf_cjd-0.1.0/.cursorrules +16 -0
- compile_pdf_cjd-0.1.0/.github/dependabot.yml +11 -0
- compile_pdf_cjd-0.1.0/.github/workflows/publish-pypi.yml +39 -0
- compile_pdf_cjd-0.1.0/.gitignore +5 -0
- compile_pdf_cjd-0.1.0/CLAUDE.md +9 -0
- compile_pdf_cjd-0.1.0/PKG-INFO +63 -0
- compile_pdf_cjd-0.1.0/README.md +28 -0
- compile_pdf_cjd-0.1.0/pyproject.toml +72 -0
- compile_pdf_cjd-0.1.0/src/compile_pdf_cjd/__init__.py +0 -0
- compile_pdf_cjd-0.1.0/src/compile_pdf_cjd/api.py +209 -0
- compile_pdf_cjd-0.1.0/src/compile_pdf_cjd/cli.py +170 -0
- compile_pdf_cjd-0.1.0/src/compile_pdf_cjd/orchestrator.py +279 -0
- compile_pdf_cjd-0.1.0/src/compile_pdf_cjd/schema.py +111 -0
- compile_pdf_cjd-0.1.0/src/compile_pdf_cjd/xml.py +133 -0
- compile_pdf_cjd-0.1.0/tests/__init__.py +0 -0
- compile_pdf_cjd-0.1.0/tests/conftest.py +191 -0
- compile_pdf_cjd-0.1.0/tests/test_cjd_api.py +152 -0
- compile_pdf_cjd-0.1.0/tests/test_cjd_api_xml.py +124 -0
- compile_pdf_cjd-0.1.0/tests/test_cjd_cli.py +221 -0
- compile_pdf_cjd-0.1.0/tests/test_cjd_cli_xml.py +108 -0
- compile_pdf_cjd-0.1.0/tests/test_cjd_orchestrator.py +188 -0
- compile_pdf_cjd-0.1.0/tests/test_cjd_schema.py +78 -0
- compile_pdf_cjd-0.1.0/tests/test_cjd_xml.py +148 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
language: en-US
|
|
2
|
+
reviews:
|
|
3
|
+
profile: chill
|
|
4
|
+
request_changes_workflow: false
|
|
5
|
+
high_level_summary: true
|
|
6
|
+
poem: false
|
|
7
|
+
review_status: true
|
|
8
|
+
collapse_walkthrough: true
|
|
9
|
+
auto_review:
|
|
10
|
+
enabled: true
|
|
11
|
+
drafts: false
|
|
12
|
+
path_filters:
|
|
13
|
+
- "!dist/**"
|
|
14
|
+
- "!build/**"
|
|
15
|
+
- "!.next/**"
|
|
16
|
+
- "!node_modules/**"
|
|
17
|
+
- "!**/__pycache__/**"
|
|
18
|
+
- "!**/*.min.js"
|
|
19
|
+
- "!**/*.lock"
|
|
20
|
+
- "!**/package-lock.json"
|
|
21
|
+
- "!**/uv.lock"
|
|
22
|
+
- "!**/poetry.lock"
|
|
23
|
+
path_instructions:
|
|
24
|
+
- path: "**/*.{ts,tsx,js,jsx}"
|
|
25
|
+
instructions: "Flag unhandled promises, missing null checks, hardcoded secrets/env vars, and unsafe `any` usage. Skip stylistic preferences — Prettier handles those."
|
|
26
|
+
- path: "**/*.py"
|
|
27
|
+
instructions: "Flag unhandled exceptions, missing type hints on public functions, hardcoded secrets, and SQL injection risks. Skip style — Ruff handles those."
|
|
28
|
+
chat:
|
|
29
|
+
auto_reply: true
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: PrintWithSynergy baseline rules
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are working in this repository, part of the printwithsynergy organization.
|
|
7
|
+
|
|
8
|
+
# Conventions
|
|
9
|
+
- Conventional commits (feat:, fix:, chore:, refactor:, docs:, test:)
|
|
10
|
+
- Never commit secrets. Use .env (gitignored) for local config.
|
|
11
|
+
- Prefer pure functions; isolate side effects.
|
|
12
|
+
- Write tests alongside code changes when behavior changes.
|
|
13
|
+
|
|
14
|
+
# Before modifying code
|
|
15
|
+
- Use the code-review-graph MCP tools (get_impact_radius_tool, get_blast_radius) to check downstream impact.
|
|
16
|
+
- For unfamiliar symbols, use sverklo_lookup or sverklo_refs first.
|
|
17
|
+
- Never assume context. If the codebase has CLAUDE.md or AGENTS.md, read it first.
|
|
18
|
+
|
|
19
|
+
# Style
|
|
20
|
+
- Match existing code style. Do not reformat unrelated lines.
|
|
21
|
+
- No "helper" refactors mixed with feature changes.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
You are working in the `compile-pdf-cjd` repository, part of the printwithsynergy organization.
|
|
2
|
+
|
|
3
|
+
# Conventions
|
|
4
|
+
- Conventional commits (feat:, fix:, chore:, refactor:, docs:, test:)
|
|
5
|
+
- Never commit secrets. Use .env (gitignored) for local config.
|
|
6
|
+
- Prefer pure functions; isolate side effects.
|
|
7
|
+
- Write tests alongside code changes when behavior changes.
|
|
8
|
+
|
|
9
|
+
# Before modifying code
|
|
10
|
+
- Use the code-review-graph MCP tools (get_impact_radius_tool, get_blast_radius) to check downstream impact.
|
|
11
|
+
- For unfamiliar symbols, use sverklo_lookup or sverklo_refs first.
|
|
12
|
+
- Never assume context. If the codebase has CLAUDE.md or AGENTS.md, read it first.
|
|
13
|
+
|
|
14
|
+
# Style
|
|
15
|
+
- Match existing code style. Do not reformat unrelated lines.
|
|
16
|
+
- No "helper" refactors mixed with feature changes.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: publish-pypi
|
|
2
|
+
|
|
3
|
+
# Publishes compile-pdf-cjd to PyPI when a v*.*.* tag is pushed.
|
|
4
|
+
#
|
|
5
|
+
# Auth: PyPI API token stored in PYPI_TOKEN repository secret.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags:
|
|
10
|
+
- "v*.*.*"
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
name: build sdist + wheel
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
18
|
+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
- run: pip install --upgrade pip build
|
|
22
|
+
- run: python -m build --sdist --wheel --outdir dist/
|
|
23
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
24
|
+
with:
|
|
25
|
+
name: dist
|
|
26
|
+
path: dist/*
|
|
27
|
+
|
|
28
|
+
publish:
|
|
29
|
+
name: publish to PyPI
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
34
|
+
with:
|
|
35
|
+
name: dist
|
|
36
|
+
path: dist/
|
|
37
|
+
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
|
38
|
+
with:
|
|
39
|
+
password: ${{ secrets.PYPI_TOKEN }}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# compile-pdf-cjd
|
|
2
|
+
|
|
3
|
+
compile-pdf-cjd
|
|
4
|
+
|
|
5
|
+
## Code Review & Blast-Radius Protocol
|
|
6
|
+
- Before edits: run code-review-graph impact tools on changed symbols
|
|
7
|
+
- After edits: ensure tests pass before commit
|
|
8
|
+
- CodeRabbit reviews PRs automatically; Cursor BugBot provides second opinion
|
|
9
|
+
- Never disable the code-review-graph Launch Agent
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: compile-pdf-cjd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CompilePDF CJD orchestrator — sequences trap, impose, marks, and rewrite producers in a single Compile Job Definition.
|
|
5
|
+
Project-URL: Homepage, https://compilepdf.com
|
|
6
|
+
Project-URL: Repository, https://github.com/printwithsynergy/compile-pdf-cjd
|
|
7
|
+
Project-URL: Issues, https://github.com/printwithsynergy/compile-pdf-cjd/issues
|
|
8
|
+
Author-email: Print With Synergy <iam@quincy.codes>
|
|
9
|
+
License: AGPL-3.0-or-later
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: click>=8.1
|
|
19
|
+
Requires-Dist: compile-pdf-core<1.0,>=0.1.0
|
|
20
|
+
Requires-Dist: compile-pdf-impose<1.0,>=0.1.0
|
|
21
|
+
Requires-Dist: compile-pdf-marks<1.0,>=0.1.0
|
|
22
|
+
Requires-Dist: compile-pdf-rewrite<1.0,>=0.1.0
|
|
23
|
+
Requires-Dist: compile-pdf-trap<1.0,>=0.1.0
|
|
24
|
+
Requires-Dist: defusedxml>=0.7.1
|
|
25
|
+
Requires-Dist: fastapi>=0.110
|
|
26
|
+
Requires-Dist: pydantic>=2.6
|
|
27
|
+
Requires-Dist: structlog>=24.1
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.9; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# compile-pdf-cjd
|
|
37
|
+
|
|
38
|
+
CJD orchestrator — sequences all four CompilePDF producers (trap, rewrite, marks, impose) in one job.
|
|
39
|
+
|
|
40
|
+
Compile Job Definition: a JSON/XML envelope that bundles a multi-producer run into a single submission. Sequences `trap → rewrite → marks → impose` with lineage threading across every stage. XML + JSON formats; each stage's diff artifact is stored in the lineage record.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv pip install compile-pdf-cjd
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
This package depends on the four producer packages plus `compile-pdf-core`; installing it pulls the whole orchestrator chain.
|
|
49
|
+
|
|
50
|
+
## Position in the stack
|
|
51
|
+
|
|
52
|
+
The only CompilePDF package that imports all four producers. Stand-alone producers live in their own repos:
|
|
53
|
+
|
|
54
|
+
- https://github.com/printwithsynergy/compile-pdf-trap
|
|
55
|
+
- https://github.com/printwithsynergy/compile-pdf-rewrite
|
|
56
|
+
- https://github.com/printwithsynergy/compile-pdf-marks
|
|
57
|
+
- https://github.com/printwithsynergy/compile-pdf-impose
|
|
58
|
+
- https://github.com/printwithsynergy/compile-pdf-core
|
|
59
|
+
|
|
60
|
+
- Repo: https://github.com/printwithsynergy/compile-pdf-cjd
|
|
61
|
+
- Deployment host: https://github.com/printwithsynergy/compile-pdf
|
|
62
|
+
- Marketing: https://compilepdf.com
|
|
63
|
+
- License: AGPL-3.0-or-later
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# compile-pdf-cjd
|
|
2
|
+
|
|
3
|
+
CJD orchestrator — sequences all four CompilePDF producers (trap, rewrite, marks, impose) in one job.
|
|
4
|
+
|
|
5
|
+
Compile Job Definition: a JSON/XML envelope that bundles a multi-producer run into a single submission. Sequences `trap → rewrite → marks → impose` with lineage threading across every stage. XML + JSON formats; each stage's diff artifact is stored in the lineage record.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv pip install compile-pdf-cjd
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This package depends on the four producer packages plus `compile-pdf-core`; installing it pulls the whole orchestrator chain.
|
|
14
|
+
|
|
15
|
+
## Position in the stack
|
|
16
|
+
|
|
17
|
+
The only CompilePDF package that imports all four producers. Stand-alone producers live in their own repos:
|
|
18
|
+
|
|
19
|
+
- https://github.com/printwithsynergy/compile-pdf-trap
|
|
20
|
+
- https://github.com/printwithsynergy/compile-pdf-rewrite
|
|
21
|
+
- https://github.com/printwithsynergy/compile-pdf-marks
|
|
22
|
+
- https://github.com/printwithsynergy/compile-pdf-impose
|
|
23
|
+
- https://github.com/printwithsynergy/compile-pdf-core
|
|
24
|
+
|
|
25
|
+
- Repo: https://github.com/printwithsynergy/compile-pdf-cjd
|
|
26
|
+
- Deployment host: https://github.com/printwithsynergy/compile-pdf
|
|
27
|
+
- Marketing: https://compilepdf.com
|
|
28
|
+
- License: AGPL-3.0-or-later
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "compile-pdf-cjd"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "CompilePDF CJD orchestrator — sequences trap, impose, marks, and rewrite producers in a single Compile Job Definition."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "AGPL-3.0-or-later" }
|
|
7
|
+
authors = [{ name = "Print With Synergy", email = "iam@quincy.codes" }]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Topic :: Multimedia :: Graphics",
|
|
16
|
+
"Topic :: Software Development :: Libraries",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
dependencies = [
|
|
20
|
+
"compile-pdf-core>=0.1.0,<1.0",
|
|
21
|
+
"compile-pdf-trap>=0.1.0,<1.0",
|
|
22
|
+
"compile-pdf-impose>=0.1.0,<1.0",
|
|
23
|
+
"compile-pdf-marks>=0.1.0,<1.0",
|
|
24
|
+
"compile-pdf-rewrite>=0.1.0,<1.0",
|
|
25
|
+
"fastapi>=0.110",
|
|
26
|
+
"pydantic>=2.6",
|
|
27
|
+
"click>=8.1",
|
|
28
|
+
"structlog>=24.1",
|
|
29
|
+
"defusedxml>=0.7.1",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0",
|
|
35
|
+
"pytest-asyncio>=0.23",
|
|
36
|
+
"pytest-cov>=4.1",
|
|
37
|
+
"ruff>=0.4",
|
|
38
|
+
"mypy>=1.9",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
compile-pdf-cjd = "compile_pdf_cjd.cli:main"
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Homepage = "https://compilepdf.com"
|
|
46
|
+
Repository = "https://github.com/printwithsynergy/compile-pdf-cjd"
|
|
47
|
+
Issues = "https://github.com/printwithsynergy/compile-pdf-cjd/issues"
|
|
48
|
+
|
|
49
|
+
[build-system]
|
|
50
|
+
requires = ["hatchling"]
|
|
51
|
+
build-backend = "hatchling.build"
|
|
52
|
+
|
|
53
|
+
[tool.hatch.build.targets.wheel]
|
|
54
|
+
packages = ["src/compile_pdf_cjd"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 100
|
|
58
|
+
target-version = "py312"
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "C4", "RET"]
|
|
62
|
+
ignore = ["E501"]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
python_files = ["test_*.py"]
|
|
67
|
+
addopts = ["-ra", "--strict-markers", "--strict-config"]
|
|
68
|
+
asyncio_mode = "auto"
|
|
69
|
+
|
|
70
|
+
[tool.coverage.run]
|
|
71
|
+
source = ["src/compile_pdf_cjd"]
|
|
72
|
+
branch = true
|
|
File without changes
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""FastAPI router for the CJD pipeline + lineage lookup.
|
|
2
|
+
|
|
3
|
+
Mounts:
|
|
4
|
+
|
|
5
|
+
* ``POST /v1/cjd/apply`` — execute a CJD job
|
|
6
|
+
* ``GET /v1/lineage/{lineage_id}`` — fetch a chain by id
|
|
7
|
+
* ``GET /v1/lineage`` — list known lineage ids (paginated)
|
|
8
|
+
|
|
9
|
+
Pass ``?async=true`` on ``POST /v1/cjd/apply`` to submit the job to
|
|
10
|
+
Celery and receive a ``202`` with a ``job_id`` you can poll at
|
|
11
|
+
``GET /v1/jobs/{job_id}``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import base64
|
|
17
|
+
|
|
18
|
+
import structlog
|
|
19
|
+
from fastapi import APIRouter, HTTPException, Query, Request, status
|
|
20
|
+
from pydantic import BaseModel
|
|
21
|
+
|
|
22
|
+
from compile_pdf_cjd.orchestrator import CjdOrderError, execute
|
|
23
|
+
from compile_pdf_cjd.schema import CjdJob
|
|
24
|
+
from compile_pdf_cjd.xml import CjdXmlError, parse_cjd_xml
|
|
25
|
+
from compile_pdf_core.async_jobs import AsyncJobAccepted, JobStatus, create_job
|
|
26
|
+
from compile_pdf_core.lineage.store import (
|
|
27
|
+
LineageNotFoundError,
|
|
28
|
+
default_store,
|
|
29
|
+
serialize_chain,
|
|
30
|
+
)
|
|
31
|
+
from compile_pdf_core.retention import parse_consent, resolve_tenant
|
|
32
|
+
from compile_pdf_core.tasks import task_payload_hash
|
|
33
|
+
from compile_pdf_core.version import (
|
|
34
|
+
CJD_SCHEMA_VERSION,
|
|
35
|
+
VERSION,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = structlog.get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
cjd_router = APIRouter()
|
|
41
|
+
lineage_router = APIRouter()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CjdApplyResponse(BaseModel):
|
|
45
|
+
model_config = {"extra": "forbid"}
|
|
46
|
+
|
|
47
|
+
output_pdf_b64: str
|
|
48
|
+
output_pdf_sha256: str
|
|
49
|
+
lineage_id: str
|
|
50
|
+
steps: list[dict[str, object]]
|
|
51
|
+
trap_diff: dict[str, object] | None = None
|
|
52
|
+
schema_version: str = CJD_SCHEMA_VERSION
|
|
53
|
+
compile_version: str = VERSION
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@cjd_router.post(
|
|
57
|
+
"/apply",
|
|
58
|
+
response_model=CjdApplyResponse,
|
|
59
|
+
status_code=status.HTTP_200_OK,
|
|
60
|
+
responses={202: {"model": AsyncJobAccepted}},
|
|
61
|
+
)
|
|
62
|
+
async def cjd_apply(
|
|
63
|
+
job: CjdJob,
|
|
64
|
+
request: Request,
|
|
65
|
+
async_: bool = Query(False, alias="async"),
|
|
66
|
+
) -> CjdApplyResponse | AsyncJobAccepted:
|
|
67
|
+
"""Execute a CJD job: orchestrate the four producers in dependency
|
|
68
|
+
order, persist lineage records, return the final PDF + chain.
|
|
69
|
+
|
|
70
|
+
Pass ``?async=true`` to enqueue the job on Celery and receive a ``202``
|
|
71
|
+
``AsyncJobAccepted`` response. Poll ``GET /v1/jobs/{job_id}`` until the
|
|
72
|
+
status is ``complete`` or ``failed``.
|
|
73
|
+
"""
|
|
74
|
+
if async_:
|
|
75
|
+
job_payload = job.model_dump(mode="json")
|
|
76
|
+
ph = task_payload_hash(job_payload)
|
|
77
|
+
job_id = create_job(kind="cjd", payload_hash=ph)
|
|
78
|
+
from compile_pdf_core.async_tasks import async_wrap_cjd
|
|
79
|
+
|
|
80
|
+
async_wrap_cjd.apply_async(args=[job_id, job_payload])
|
|
81
|
+
logger.info("cjd.apply.async_accepted", job_id=job_id, payload_hash=ph[:16])
|
|
82
|
+
from fastapi.responses import JSONResponse
|
|
83
|
+
|
|
84
|
+
return JSONResponse( # type: ignore[return-value]
|
|
85
|
+
status_code=status.HTTP_202_ACCEPTED,
|
|
86
|
+
content=AsyncJobAccepted(
|
|
87
|
+
job_id=job_id,
|
|
88
|
+
status=JobStatus.pending,
|
|
89
|
+
poll_url=f"/v1/jobs/{job_id}",
|
|
90
|
+
).model_dump(),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
consent = parse_consent(request)
|
|
94
|
+
tenant = resolve_tenant(request)
|
|
95
|
+
try:
|
|
96
|
+
result = execute(job, consent=consent, tenant=tenant)
|
|
97
|
+
except CjdOrderError as exc:
|
|
98
|
+
raise HTTPException(status_code=422, detail=f"CJD ordering rejected: {exc}") from exc
|
|
99
|
+
except (ValueError, TypeError) as exc:
|
|
100
|
+
# Includes base64 errors from inside execute().
|
|
101
|
+
raise HTTPException(status_code=400, detail=f"CJD job rejected: {exc}") from exc
|
|
102
|
+
|
|
103
|
+
logger.info(
|
|
104
|
+
"cjd.apply.ok",
|
|
105
|
+
lineage_id=result.lineage_id,
|
|
106
|
+
steps=len(result.steps),
|
|
107
|
+
output_sha=result.output_pdf_sha256[:16],
|
|
108
|
+
consent=consent,
|
|
109
|
+
retained_steps=sum(1 for s in result.steps if s.retained_for_training),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return CjdApplyResponse(
|
|
113
|
+
output_pdf_b64=base64.b64encode(result.output_pdf_bytes).decode("ascii"),
|
|
114
|
+
output_pdf_sha256=result.output_pdf_sha256,
|
|
115
|
+
lineage_id=result.lineage_id,
|
|
116
|
+
steps=[_step_to_dict(s) for s in result.steps],
|
|
117
|
+
trap_diff=result.trap_diff,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@cjd_router.post(
|
|
122
|
+
"/apply-xml",
|
|
123
|
+
response_model=CjdApplyResponse,
|
|
124
|
+
status_code=status.HTTP_200_OK,
|
|
125
|
+
)
|
|
126
|
+
async def cjd_apply_xml(request: Request) -> CjdApplyResponse:
|
|
127
|
+
"""XML-encoded variant of POST /v1/cjd/apply.
|
|
128
|
+
|
|
129
|
+
Body is a CJD XML envelope (per :mod:`compile_pdf.cjd.xml`); the
|
|
130
|
+
response shape matches the JSON endpoint exactly. Useful for
|
|
131
|
+
operators integrating Compile into JDF / PJTF pipelines that
|
|
132
|
+
already speak XML on the wire.
|
|
133
|
+
"""
|
|
134
|
+
body = await request.body()
|
|
135
|
+
if not body:
|
|
136
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="empty XML body")
|
|
137
|
+
try:
|
|
138
|
+
job = parse_cjd_xml(body)
|
|
139
|
+
except CjdXmlError as exc:
|
|
140
|
+
raise HTTPException(status_code=422, detail=f"CJD XML rejected: {exc}") from exc
|
|
141
|
+
|
|
142
|
+
consent = parse_consent(request)
|
|
143
|
+
tenant = resolve_tenant(request)
|
|
144
|
+
try:
|
|
145
|
+
result = execute(job, consent=consent, tenant=tenant)
|
|
146
|
+
except CjdOrderError as exc:
|
|
147
|
+
raise HTTPException(status_code=422, detail=f"CJD ordering rejected: {exc}") from exc
|
|
148
|
+
except (ValueError, TypeError) as exc:
|
|
149
|
+
raise HTTPException(status_code=400, detail=f"CJD job rejected: {exc}") from exc
|
|
150
|
+
|
|
151
|
+
logger.info(
|
|
152
|
+
"cjd.apply_xml.ok",
|
|
153
|
+
lineage_id=result.lineage_id,
|
|
154
|
+
steps=len(result.steps),
|
|
155
|
+
output_sha=result.output_pdf_sha256[:16],
|
|
156
|
+
consent=consent,
|
|
157
|
+
retained_steps=sum(1 for s in result.steps if s.retained_for_training),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return CjdApplyResponse(
|
|
161
|
+
output_pdf_b64=base64.b64encode(result.output_pdf_bytes).decode("ascii"),
|
|
162
|
+
output_pdf_sha256=result.output_pdf_sha256,
|
|
163
|
+
lineage_id=result.lineage_id,
|
|
164
|
+
steps=[_step_to_dict(s) for s in result.steps],
|
|
165
|
+
trap_diff=result.trap_diff,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class LineageListResponse(BaseModel):
|
|
170
|
+
model_config = {"extra": "forbid"}
|
|
171
|
+
|
|
172
|
+
lineage_ids: list[str]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@lineage_router.get("/{lineage_id}", status_code=status.HTTP_200_OK)
|
|
176
|
+
async def lineage_get(lineage_id: str) -> dict[str, object]:
|
|
177
|
+
"""Fetch a lineage chain by id."""
|
|
178
|
+
try:
|
|
179
|
+
chain = default_store().get(lineage_id)
|
|
180
|
+
except LineageNotFoundError as exc:
|
|
181
|
+
raise HTTPException(status_code=404, detail=f"lineage_id not found: {lineage_id}") from exc
|
|
182
|
+
return serialize_chain(chain)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@lineage_router.get(
|
|
186
|
+
"",
|
|
187
|
+
response_model=LineageListResponse,
|
|
188
|
+
status_code=status.HTTP_200_OK,
|
|
189
|
+
)
|
|
190
|
+
async def lineage_list(limit: int = Query(default=50, ge=1, le=500)) -> LineageListResponse:
|
|
191
|
+
"""List known lineage ids (best-effort; backend may paginate)."""
|
|
192
|
+
return LineageListResponse(lineage_ids=default_store().list_ids(limit=limit))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _step_to_dict(step) -> dict[str, object]: # type: ignore[no-untyped-def]
|
|
196
|
+
payload: dict[str, object] = {
|
|
197
|
+
"step_index": step.step_index,
|
|
198
|
+
"producer": step.producer,
|
|
199
|
+
"input_sha256": step.input_sha256,
|
|
200
|
+
"output_sha256": step.output_sha256,
|
|
201
|
+
"cache_key": step.cache_key,
|
|
202
|
+
"plan_sha256": step.plan_sha256,
|
|
203
|
+
"retained_for_training": step.retained_for_training,
|
|
204
|
+
}
|
|
205
|
+
if step.extras:
|
|
206
|
+
payload["extras"] = dict(step.extras)
|
|
207
|
+
if step.trap_diff is not None:
|
|
208
|
+
payload["trap_diff"] = step.trap_diff
|
|
209
|
+
return payload
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Click subcommand registration for ``compile-pdf cjd`` + ``lineage``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from compile_pdf_cjd.orchestrator import CjdOrderError, execute
|
|
13
|
+
from compile_pdf_cjd.schema import CjdJob, cjd_job_json_schema
|
|
14
|
+
from compile_pdf_cjd.xml import CjdXmlError, parse_cjd_xml, render_cjd_xml
|
|
15
|
+
from compile_pdf_core.lineage.store import (
|
|
16
|
+
LineageNotFoundError,
|
|
17
|
+
default_store,
|
|
18
|
+
serialize_chain,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register(group: click.Group) -> None:
|
|
23
|
+
"""Attach ``cjd``, ``cjd-schema``, and ``lineage`` subcommands."""
|
|
24
|
+
|
|
25
|
+
@group.command("cjd", help="Execute a CJD job (multi-producer pipeline).")
|
|
26
|
+
@click.option(
|
|
27
|
+
"--job",
|
|
28
|
+
"job_path",
|
|
29
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
30
|
+
required=True,
|
|
31
|
+
help="JSON CJD-job document.",
|
|
32
|
+
)
|
|
33
|
+
@click.option(
|
|
34
|
+
"--input",
|
|
35
|
+
"input_path",
|
|
36
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
37
|
+
default=None,
|
|
38
|
+
help=(
|
|
39
|
+
"Optional. When set, the input PDF bytes are read from this "
|
|
40
|
+
"path and replace job.input_pdf_b64 (handy for large inputs)."
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
@click.option(
|
|
44
|
+
"--trap-diff",
|
|
45
|
+
"trap_diff_path",
|
|
46
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
47
|
+
default=None,
|
|
48
|
+
help="Write the trap-diff artifact to this path (no-op if no trap step).",
|
|
49
|
+
)
|
|
50
|
+
@click.option(
|
|
51
|
+
"--xml/--json",
|
|
52
|
+
"use_xml",
|
|
53
|
+
default=False,
|
|
54
|
+
help="Read the job document as XML (default: JSON).",
|
|
55
|
+
)
|
|
56
|
+
@click.argument(
|
|
57
|
+
"output_path",
|
|
58
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
59
|
+
)
|
|
60
|
+
def cjd_cmd(
|
|
61
|
+
job_path: Path,
|
|
62
|
+
input_path: Path | None,
|
|
63
|
+
trap_diff_path: Path | None,
|
|
64
|
+
use_xml: bool,
|
|
65
|
+
output_path: Path,
|
|
66
|
+
) -> None:
|
|
67
|
+
if use_xml:
|
|
68
|
+
try:
|
|
69
|
+
job = parse_cjd_xml(job_path.read_bytes())
|
|
70
|
+
except CjdXmlError as exc:
|
|
71
|
+
click.echo(f"XML job rejected: {exc}", err=True)
|
|
72
|
+
sys.exit(3)
|
|
73
|
+
if input_path is not None:
|
|
74
|
+
# Re-serialize with the override and re-parse via JSON path.
|
|
75
|
+
payload = job.model_dump(mode="json")
|
|
76
|
+
payload["input_pdf_b64"] = base64.b64encode(input_path.read_bytes()).decode("ascii")
|
|
77
|
+
job = CjdJob.model_validate(payload)
|
|
78
|
+
else:
|
|
79
|
+
job_dict = json.loads(job_path.read_text(encoding="utf-8"))
|
|
80
|
+
if input_path is not None:
|
|
81
|
+
job_dict["input_pdf_b64"] = base64.b64encode(input_path.read_bytes()).decode(
|
|
82
|
+
"ascii"
|
|
83
|
+
)
|
|
84
|
+
try:
|
|
85
|
+
job = CjdJob.model_validate(job_dict)
|
|
86
|
+
except Exception as exc:
|
|
87
|
+
click.echo(f"job validation failed: {exc}", err=True)
|
|
88
|
+
sys.exit(3)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
result = execute(job)
|
|
92
|
+
except CjdOrderError as exc:
|
|
93
|
+
click.echo(f"job rejected: {exc}", err=True)
|
|
94
|
+
sys.exit(4)
|
|
95
|
+
|
|
96
|
+
output_path.write_bytes(result.output_pdf_bytes)
|
|
97
|
+
if trap_diff_path is not None and result.trap_diff is not None:
|
|
98
|
+
trap_diff_path.write_text(json.dumps(result.trap_diff, indent=2), encoding="utf-8")
|
|
99
|
+
|
|
100
|
+
click.echo(
|
|
101
|
+
json.dumps(
|
|
102
|
+
{
|
|
103
|
+
"lineage_id": result.lineage_id,
|
|
104
|
+
"output_pdf_sha256": result.output_pdf_sha256,
|
|
105
|
+
"steps": [
|
|
106
|
+
{
|
|
107
|
+
"step_index": s.step_index,
|
|
108
|
+
"producer": s.producer,
|
|
109
|
+
"output_sha256": s.output_sha256[:16],
|
|
110
|
+
"cache_key": s.cache_key[:16],
|
|
111
|
+
}
|
|
112
|
+
for s in result.steps
|
|
113
|
+
],
|
|
114
|
+
"output": str(output_path),
|
|
115
|
+
"trap_diff": str(trap_diff_path)
|
|
116
|
+
if trap_diff_path and result.trap_diff is not None
|
|
117
|
+
else None,
|
|
118
|
+
},
|
|
119
|
+
indent=2,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@group.command("cjd-schema", hidden=True, help="Dump the CJD-job JSON Schema.")
|
|
124
|
+
def cjd_schema_cmd() -> None:
|
|
125
|
+
click.echo(json.dumps(cjd_job_json_schema(), indent=2))
|
|
126
|
+
|
|
127
|
+
@group.command(
|
|
128
|
+
"cjd-xml-render",
|
|
129
|
+
hidden=True,
|
|
130
|
+
help="Convert a JSON CJD job to XML and print the result.",
|
|
131
|
+
)
|
|
132
|
+
@click.argument(
|
|
133
|
+
"job_path",
|
|
134
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
135
|
+
)
|
|
136
|
+
def cjd_xml_render_cmd(job_path: Path) -> None:
|
|
137
|
+
job_dict = json.loads(job_path.read_text(encoding="utf-8"))
|
|
138
|
+
try:
|
|
139
|
+
job = CjdJob.model_validate(job_dict)
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
click.echo(f"job validation failed: {exc}", err=True)
|
|
142
|
+
sys.exit(3)
|
|
143
|
+
click.echo(render_cjd_xml(job).decode("utf-8"))
|
|
144
|
+
|
|
145
|
+
@group.command("lineage", help="Print the lineage chain for a previously-run CJD job.")
|
|
146
|
+
@click.argument("lineage_id", type=str)
|
|
147
|
+
@click.option(
|
|
148
|
+
"--chain/--summary",
|
|
149
|
+
default=False,
|
|
150
|
+
help="Print the full chain (default) vs. just the lineage_id + step count.",
|
|
151
|
+
)
|
|
152
|
+
def lineage_cmd(lineage_id: str, chain: bool) -> None:
|
|
153
|
+
try:
|
|
154
|
+
ch = default_store().get(lineage_id)
|
|
155
|
+
except LineageNotFoundError:
|
|
156
|
+
click.echo(f"lineage_id not found: {lineage_id}", err=True)
|
|
157
|
+
sys.exit(5)
|
|
158
|
+
if chain:
|
|
159
|
+
click.echo(json.dumps(serialize_chain(ch), indent=2))
|
|
160
|
+
else:
|
|
161
|
+
click.echo(
|
|
162
|
+
json.dumps(
|
|
163
|
+
{
|
|
164
|
+
"lineage_id": ch.lineage_id,
|
|
165
|
+
"step_count": len(ch.steps),
|
|
166
|
+
"producers": [s.producer for s in ch.steps],
|
|
167
|
+
},
|
|
168
|
+
indent=2,
|
|
169
|
+
)
|
|
170
|
+
)
|