python-cad-tools 0.1.0rc1__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.
- python_cad_tools/__init__.py +10 -0
- python_cad_tools/_locking.py +63 -0
- python_cad_tools/_project_loader.py +273 -0
- python_cad_tools/_project_state.py +65 -0
- python_cad_tools/_scaffold.py +66 -0
- python_cad_tools/_transaction.py +208 -0
- python_cad_tools/_viewer_static/app.webmanifest +18 -0
- python_cad_tools/_viewer_static/asset-manifest.json +1 -0
- python_cad_tools/_viewer_static/assets/animationGroup-Sd34ntWg.js +1 -0
- python_cad_tools/_viewer_static/assets/bakedVertexAnimation-Cd_pRdrn.js +316 -0
- python_cad_tools/_viewer_static/assets/basisTextureLoader-DWgvS_1G.js +1 -0
- python_cad_tools/_viewer_static/assets/brdfTextureTools-CWGiG1W2.js +2 -0
- python_cad_tools/_viewer_static/assets/bumpFragment-CWULheF1.js +127 -0
- python_cad_tools/_viewer_static/assets/bumpVertex-DqzFZayE.js +6 -0
- python_cad_tools/_viewer_static/assets/bumpVertexDeclaration-C5EFyTio.js +6 -0
- python_cad_tools/_viewer_static/assets/clipPlaneFragment-BXRtt4jj.js +46 -0
- python_cad_tools/_viewer_static/assets/clipPlaneVertex-B_VhD1oQ.js +37 -0
- python_cad_tools/_viewer_static/assets/cubemapToSphericalPolynomial-DRpaukza.js +1 -0
- python_cad_tools/_viewer_static/assets/dds-Dkb2Cx2N.js +1 -0
- python_cad_tools/_viewer_static/assets/dds.pure-DtvJsKvY.js +1 -0
- python_cad_tools/_viewer_static/assets/ddsTextureLoader-H78xux1g.js +1 -0
- python_cad_tools/_viewer_static/assets/default.fragment-oSQ-tWhe.js +469 -0
- python_cad_tools/_viewer_static/assets/default.vertex-DhFjw_Da.js +209 -0
- python_cad_tools/_viewer_static/assets/defaultUboDeclaration-BMDHNppx.js +5 -0
- python_cad_tools/_viewer_static/assets/dumpTools-BaNZPjUX.js +2 -0
- python_cad_tools/_viewer_static/assets/envTextureLoader-V0YnLcIM.js +1 -0
- python_cad_tools/_viewer_static/assets/exrTextureLoader-CjGyIK3x.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphApplyForceBlock-CW5nuobR.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphApplyImpulseBlock-LFqYIkhj.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphArrayIndexBlock-BMOHUbBm.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphBezierCurveEasingBlock-knmTiuEk.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphBinaryOperationBlock-DFLuCAKJ.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphBranchBlock-BJRHMXMo.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphCachedOperationBlock-Dng-6jE0.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphCancelDelayBlock-CgbK3CQZ.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphCodeExecutionBlock-8zMtq9tg.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphConditionalDataBlock-BiLr2rkI.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphConsoleLogBlock-J1vcCtNP.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphConstantBlock-CFb5nuaV.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphContextBlock-DAeGVzhC.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphCounterBlock-lZlAQiJd.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphDataSwitchBlock-BIzvmbVy.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphDebounceBlock-DvNuMVwy.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphDebugBlock-Dymd2VEE.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphDoNBlock-v68aoNLH.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphEasingBlock-CTnGv92q.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphFlipFlopBlock-DXsO0Chw.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphForLoopBlock-CPpe9T75.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphFunctionReferenceBlock-DjDev4Pq.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGLTFDataProvider-GIYCz4AQ.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetAngularVelocityBlock-BStkOVeL.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetAssetBlock-Eiqoivbz.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetLinearVelocityBlock-B0cpV5ws.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetPhysicsMassPropertiesBlock-B-H64yaI.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetPropertyBlock-CUyOsPw0.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetSoundVolumeBlock-C0FjrsUz.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphGetVariableBlock-O4597zBl.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphIndexOfBlock-E00Z3KXM.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphInterpolationBlock-DnkA9MNH.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphIsKeyPressedBlock-DDfeuAyf.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphIsSoundPlayingBlock-DMXg8jdM.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphJsonPointerParserBlock-OmCorK0y.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphKeyDownEventBlock-RuVtj6g2.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphKeyUpEventBlock-DO4pAfHi.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphKeyboardEventBlock-Bx3pAzzh.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMathBlocks-7W1XYOC8.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMathCombineExtractBlocks-a4f0Kv6e.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMatrixMathBlocks-C5Z-y1Au.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMeshPickEventBlock-D4m-I4CS.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphMultiGateBlock-BISVSMw6.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPauseAnimationBlock-BYP2Z-HG.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPauseSoundBlock-Drh1EeW6.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPhysicsCollisionEventBlock-BoziAQ75.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPlayAnimationBlock-Clry-68M.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPlaySoundBlock-LbBVxGBE.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerDownEventBlock-DIVXCSHh.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerMoveEventBlock-BNNOzY2W.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerOutEventBlock-B8f2dOFT.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerOverEventBlock-CQr8LCee.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphPointerUpEventBlock-DMg_iEHl.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphReceiveCustomEventBlock-B8GJm9Ai.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSceneReadyEventBlock-Dcdb29K2.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSceneTickEventBlock-BLE5p4mi.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSendCustomEventBlock-BMzDItQ5.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSequenceBlock-DRURovjn.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetAngularVelocityBlock-Ba8dhkLM.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetDelayBlock-CnQR--Gn.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetLinearVelocityBlock-Dm1r3vkh.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetPhysicsMotionTypeBlock-BNtMLimi.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetPropertyBlock-C-uv2jhP.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetSoundVolumeBlock-D9LDx3lg.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSetVariableBlock-DszzRB5X.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSoundEndedEventBlock-DeTDSudb.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphStopAnimationBlock-DWuwoFWr.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphStopSoundBlock-DEnj2cQw.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphSwitchBlock-zs8Fp9ky.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphThrottleBlock-BYAKUQXd.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphTransformCoordinatesSystemBlock-DYP54m5P.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphTypeToTypeBlocks-vFwn79bS.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphUnaryOperationBlock-CaRh4Ef0.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphVectorMathBlocks-BEIhMs-A.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphWaitAllBlock-DFbELP_j.js +1 -0
- python_cad_tools/_viewer_static/assets/flowGraphWhileLoopBlock-C1OKQ-_F.js +1 -0
- python_cad_tools/_viewer_static/assets/fogFragment-DtDlQBnz.js +25 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting-Cfigjb15.js +242 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting-DIn8R8oF.js +296 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting.fragment-BxeRJ2k-.js +38 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting.fragment-DNDmvRs-.js +37 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting.vertex-D92z5jYn.js +86 -0
- python_cad_tools/_viewer_static/assets/gaussianSplatting.vertex-DSWfFnKQ.js +86 -0
- python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.fragment-DBQE2gDk.js +12 -0
- python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.fragment-qwx5ntqJ.js +17 -0
- python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.vertex-CP2kd3jU.js +30 -0
- python_cad_tools/_viewer_static/assets/gaussianSplattingVoxel.vertex-Dr-fEg_W.js +19 -0
- python_cad_tools/_viewer_static/assets/geometry.fragment-CsJIm2H0.js +242 -0
- python_cad_tools/_viewer_static/assets/geometry.fragment-Dph3muZt.js +262 -0
- python_cad_tools/_viewer_static/assets/geometry.vertex-BXGMTA_J.js +212 -0
- python_cad_tools/_viewer_static/assets/geometry.vertex-Bm9Wg3Gi.js +214 -0
- python_cad_tools/_viewer_static/assets/glowBlurPostProcess.fragment-BJSwKIJD.js +7 -0
- python_cad_tools/_viewer_static/assets/glowMapGeneration.fragment-DcyS1cTF.js +63 -0
- python_cad_tools/_viewer_static/assets/glowMapGeneration.vertex-BGq1Fakj.js +75 -0
- python_cad_tools/_viewer_static/assets/glowMapMerge.fragment-DxcneU_F.js +25 -0
- python_cad_tools/_viewer_static/assets/glowMapMerge.vertex-DaJJKqMV.js +8 -0
- python_cad_tools/_viewer_static/assets/harmonicsFunctions-DzJcvNWG.js +16 -0
- python_cad_tools/_viewer_static/assets/hdrTextureLoader-BGQvgh0h.js +2 -0
- python_cad_tools/_viewer_static/assets/helperFunctions-Cugfp1Fz.js +87 -0
- python_cad_tools/_viewer_static/assets/iesTextureLoader-CO4G0p2M.js +2 -0
- python_cad_tools/_viewer_static/assets/index-B2zBEWsq.js +7007 -0
- python_cad_tools/_viewer_static/assets/index-DcTTrc-8.css +1 -0
- python_cad_tools/_viewer_static/assets/kernelBlur.fragment-B6CTXTxY.js +57 -0
- python_cad_tools/_viewer_static/assets/kernelBlur.fragment-BkKbudbW.js +57 -0
- python_cad_tools/_viewer_static/assets/kernelBlur.vertex-BYwFU5IS.js +11 -0
- python_cad_tools/_viewer_static/assets/kernelBlur.vertex-D371zUfT.js +11 -0
- python_cad_tools/_viewer_static/assets/kernelBlurVaryingDeclaration-CgBGaCHg.js +1 -0
- python_cad_tools/_viewer_static/assets/kernelBlurVaryingDeclaration-YS8JpqdO.js +1 -0
- python_cad_tools/_viewer_static/assets/ktxTextureLoader-ncMSJTjl.js +1 -0
- python_cad_tools/_viewer_static/assets/lightFragment-skHiFbKB.js +398 -0
- python_cad_tools/_viewer_static/assets/logDepthDeclaration-BR8XRz9F.js +4 -0
- python_cad_tools/_viewer_static/assets/logDepthVertex-UKrhEIvg.js +14 -0
- python_cad_tools/_viewer_static/assets/mainUVVaryingDeclaration-BxQWGFcG.js +4 -0
- python_cad_tools/_viewer_static/assets/mesh.vertexData.functions-ayNlGC5K.js +1 -0
- python_cad_tools/_viewer_static/assets/meshUboDeclaration-BnX046aQ.js +4 -0
- python_cad_tools/_viewer_static/assets/oitFragment-CTMCOfNP.js +605 -0
- python_cad_tools/_viewer_static/assets/openpbr.fragment-DPJPqi6p.js +1530 -0
- python_cad_tools/_viewer_static/assets/openpbr.fragment-Dv6qTVVi.js +1384 -0
- python_cad_tools/_viewer_static/assets/openpbr.vertex-Bn2wrdGL.js +364 -0
- python_cad_tools/_viewer_static/assets/openpbr.vertex-CHESW3gB.js +251 -0
- python_cad_tools/_viewer_static/assets/openpbrMaterial.pure-BdK7VgO_.js +3 -0
- python_cad_tools/_viewer_static/assets/openpbrMaterialLoadingAdapter-Bm_AHDUN.js +4 -0
- python_cad_tools/_viewer_static/assets/openpbrTransmissionLayerData-BDa9qEoN.js +339 -0
- python_cad_tools/_viewer_static/assets/openpbrTransmissionLayerData-Ba_ly4LE.js +343 -0
- python_cad_tools/_viewer_static/assets/openpbrUboDeclaration-BY2lSH2C.js +5 -0
- python_cad_tools/_viewer_static/assets/openpbrUboDeclaration-DajhikDS.js +6 -0
- python_cad_tools/_viewer_static/assets/packingFunctions-BHPwcGG2.js +4 -0
- python_cad_tools/_viewer_static/assets/packingFunctions-DTwzBRBx.js +4 -0
- python_cad_tools/_viewer_static/assets/pass.fragment-9oKU4pI3.js +4 -0
- python_cad_tools/_viewer_static/assets/passCube.fragment-CpAeiYx1.js +23 -0
- python_cad_tools/_viewer_static/assets/passCube.fragment-DHtSWVRF.js +23 -0
- python_cad_tools/_viewer_static/assets/pbr.fragment-cIhMKwLs.js +2560 -0
- python_cad_tools/_viewer_static/assets/pbr.vertex-BfdONsf2.js +255 -0
- python_cad_tools/_viewer_static/assets/pbrBRDFFunctions-B_ppOzPO.js +173 -0
- python_cad_tools/_viewer_static/assets/pbrDebug-DDdwL5_t.js +693 -0
- python_cad_tools/_viewer_static/assets/pbrIBLFunctions-b1zdpzOP.js +43 -0
- python_cad_tools/_viewer_static/assets/pbrMaterial.pure-BZA7yVhu.js +2 -0
- python_cad_tools/_viewer_static/assets/pbrMaterialLoadingAdapter-BRvKRnjl.js +1 -0
- python_cad_tools/_viewer_static/assets/pbrUboDeclaration-CMZp25k3.js +5 -0
- python_cad_tools/_viewer_static/assets/postprocess.vertex-DzdUpNbW.js +9 -0
- python_cad_tools/_viewer_static/assets/procedural.vertex-BnbuQ-1f.js +7 -0
- python_cad_tools/_viewer_static/assets/procedural.vertex-Dp-Ng7Pc.js +8 -0
- python_cad_tools/_viewer_static/assets/reflectionFunction-DBlr3oDs.js +83 -0
- python_cad_tools/_viewer_static/assets/rgbdDecode.fragment-CyvnZFOJ.js +5 -0
- python_cad_tools/_viewer_static/assets/rgbdEncode.fragment-Dz4zLnoD.js +5 -0
- python_cad_tools/_viewer_static/assets/rgbdEncode.fragment-iscgpA1_.js +5 -0
- python_cad_tools/_viewer_static/assets/sceneUboDeclaration-BzFIc4mb.js +11 -0
- python_cad_tools/_viewer_static/assets/textureProcessor.fragment-C9D0rd-M.js +151 -0
- python_cad_tools/_viewer_static/assets/textureProcessor.fragment-JMKyWZng.js +146 -0
- python_cad_tools/_viewer_static/assets/tgaTextureLoader-oxTElIQJ.js +1 -0
- python_cad_tools/_viewer_static/assets/vertexColorMixing-mdC9xZe1.js +418 -0
- python_cad_tools/_viewer_static/favicon.svg +10 -0
- python_cad_tools/_viewer_static/index.html +18 -0
- python_cad_tools/_viewer_static/sw.js +61 -0
- python_cad_tools/artifacts.py +546 -0
- python_cad_tools/build-info.json +1 -0
- python_cad_tools/build.py +270 -0
- python_cad_tools/cli.py +212 -0
- python_cad_tools/context.py +67 -0
- python_cad_tools/determinism.py +66 -0
- python_cad_tools/drawings/__init__.py +124 -0
- python_cad_tools/drawings/_catalog.py +79 -0
- python_cad_tools/drawings/_validation.py +238 -0
- python_cad_tools/elements.py +121 -0
- python_cad_tools/exceptions.py +45 -0
- python_cad_tools/exporters/__init__.py +8 -0
- python_cad_tools/exporters/base.py +24 -0
- python_cad_tools/exporters/drawings/__init__.py +5 -0
- python_cad_tools/exporters/drawings/annotations.py +444 -0
- python_cad_tools/exporters/drawings/dxf.py +110 -0
- python_cad_tools/exporters/drawings/model.py +70 -0
- python_cad_tools/exporters/drawings/pdf.py +119 -0
- python_cad_tools/exporters/drawings/primitives.py +93 -0
- python_cad_tools/exporters/drawings/projection.py +64 -0
- python_cad_tools/exporters/drawings/svg.py +135 -0
- python_cad_tools/exporters/glb.py +100 -0
- python_cad_tools/exporters/ifc.py +229 -0
- python_cad_tools/exporters/quantities.py +136 -0
- python_cad_tools/exporters/step.py +117 -0
- python_cad_tools/geometry.py +187 -0
- python_cad_tools/manifests.py +338 -0
- python_cad_tools/project_config.py +174 -0
- python_cad_tools/py.typed +1 -0
- python_cad_tools/runtime_sync.py +126 -0
- python_cad_tools/schemas/build-manifest-1.json +1 -0
- python_cad_tools/schemas/design-manifest-1.json +1 -0
- python_cad_tools/schemas/download-manifest-2.json +46 -0
- python_cad_tools/schemas/drawing-annotations-1.json +66 -0
- python_cad_tools/schemas/glb-manifest-1.json +1 -0
- python_cad_tools/schemas/ifc-validation-1.json +1 -0
- python_cad_tools/schemas/quantities-1.json +1 -0
- python_cad_tools/schemas/run-metadata-1.json +1 -0
- python_cad_tools/schemas/site-metadata-1.json +34 -0
- python_cad_tools/schemas/step-validation-1.json +1 -0
- python_cad_tools/site.py +676 -0
- python_cad_tools/templates/project/.gitignore +9 -0
- python_cad_tools/templates/project/README.md +38 -0
- python_cad_tools/templates/project/config.py +36 -0
- python_cad_tools/templates/project/drawing_annotations.py +117 -0
- python_cad_tools/templates/project/model.py +87 -0
- python_cad_tools/templates/project/pyproject.toml +27 -0
- python_cad_tools/units.py +207 -0
- python_cad_tools/validation.py +153 -0
- python_cad_tools-0.1.0rc1.dist-info/METADATA +149 -0
- python_cad_tools-0.1.0rc1.dist-info/RECORD +236 -0
- python_cad_tools-0.1.0rc1.dist-info/WHEEL +5 -0
- python_cad_tools-0.1.0rc1.dist-info/entry_points.txt +2 -0
- python_cad_tools-0.1.0rc1.dist-info/licenses/LICENSE +679 -0
- python_cad_tools-0.1.0rc1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"""Artifact inventory, parser-level validation, and final-byte verification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import mimetypes
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from importlib import metadata, resources
|
|
11
|
+
from pathlib import Path, PurePosixPath
|
|
12
|
+
from typing import Any, Literal, cast
|
|
13
|
+
from xml.etree import ElementTree
|
|
14
|
+
|
|
15
|
+
from .determinism import semantic_hash, sha256_file
|
|
16
|
+
from .exceptions import ArtifactIntegrityError, BuildExportError
|
|
17
|
+
from .manifests import load_json_document, validate_json_document
|
|
18
|
+
from .validation import ValidationIssue
|
|
19
|
+
|
|
20
|
+
BUILD_MANIFEST_RELATIVE = "manifests/build-manifest.json"
|
|
21
|
+
_HEX = re.compile(r"[0-9a-f]{64}\Z")
|
|
22
|
+
_ARTIFACT_FIELDS = {"path", "size", "sha256", "media_type", "producer", "schema_id", "schema_version", "volatile"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True, slots=True)
|
|
26
|
+
class ArtifactRecord:
|
|
27
|
+
path: str
|
|
28
|
+
size: int
|
|
29
|
+
sha256: str
|
|
30
|
+
media_type: str
|
|
31
|
+
producer: str
|
|
32
|
+
schema_id: str | None
|
|
33
|
+
schema_version: int | None
|
|
34
|
+
volatile: bool
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class VerificationResult:
|
|
39
|
+
ok: bool
|
|
40
|
+
output_root: Path
|
|
41
|
+
issues: tuple[ValidationIssue, ...]
|
|
42
|
+
artifacts: tuple[ArtifactRecord, ...]
|
|
43
|
+
build_manifest: Path
|
|
44
|
+
artifact_set_hash: str
|
|
45
|
+
stable_artifact_set_hash: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class PackageBuildInfo:
|
|
50
|
+
schema_id: Literal["urn:python-cad-tools:schema:package-build-info:1"]
|
|
51
|
+
schema_version: Literal[1]
|
|
52
|
+
distribution: Literal["python-cad-tools"]
|
|
53
|
+
version: str
|
|
54
|
+
source_commit: str
|
|
55
|
+
source_dirty: bool
|
|
56
|
+
viewer_shell_sha256: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_build_manifest(path: Path) -> dict[str, Any]:
|
|
60
|
+
value = validate_json_document(path)
|
|
61
|
+
if value["schema_id"] != "urn:python-cad-tools:schema:build-manifest:1":
|
|
62
|
+
raise ArtifactIntegrityError(f"Not a build manifest: {path}")
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_design_manifest(path: Path) -> dict[str, Any]:
|
|
67
|
+
value = validate_json_document(path)
|
|
68
|
+
if value["schema_id"] != "urn:python-cad-tools:schema:design-manifest:1":
|
|
69
|
+
raise ArtifactIntegrityError(f"Not a design manifest: {path}")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def artifact_records(output_root: Path) -> tuple[ArtifactRecord, ...]:
|
|
74
|
+
records: list[ArtifactRecord] = []
|
|
75
|
+
for relative, path in _inventory(output_root).items():
|
|
76
|
+
if relative == BUILD_MANIFEST_RELATIVE:
|
|
77
|
+
continue
|
|
78
|
+
schema_id: str | None = None
|
|
79
|
+
schema_version: int | None = None
|
|
80
|
+
if path.suffix == ".json":
|
|
81
|
+
document = validate_json_document(path)
|
|
82
|
+
schema_id = cast(str, document["schema_id"])
|
|
83
|
+
schema_version = cast(int, document["schema_version"])
|
|
84
|
+
records.append(
|
|
85
|
+
ArtifactRecord(
|
|
86
|
+
path=relative,
|
|
87
|
+
size=path.stat().st_size,
|
|
88
|
+
sha256=sha256_file(path),
|
|
89
|
+
media_type=_media_type(path),
|
|
90
|
+
producer="python-cad-tools",
|
|
91
|
+
schema_id=schema_id,
|
|
92
|
+
schema_version=schema_version,
|
|
93
|
+
volatile=relative == "manifests/run-metadata.json",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
return tuple(sorted(records, key=lambda record: record.path))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def validate_export_tree(output_root: Path, selected_formats: tuple[str, ...], artifact_stem: str) -> None:
|
|
100
|
+
"""Validate the complete exporter output before inventory hashing."""
|
|
101
|
+
actual = set(_inventory(output_root))
|
|
102
|
+
expected = _expected_paths(selected_formats, artifact_stem, ".gitkeep" in actual)
|
|
103
|
+
if BUILD_MANIFEST_RELATIVE in actual:
|
|
104
|
+
expected.add(BUILD_MANIFEST_RELATIVE)
|
|
105
|
+
missing = sorted(expected - actual)
|
|
106
|
+
unexpected = sorted(actual - expected)
|
|
107
|
+
if missing or unexpected:
|
|
108
|
+
parts = []
|
|
109
|
+
if missing:
|
|
110
|
+
parts.append("missing: " + ", ".join(missing))
|
|
111
|
+
if unexpected:
|
|
112
|
+
parts.append("unexpected: " + ", ".join(unexpected))
|
|
113
|
+
raise ArtifactIntegrityError("Invalid staged artifact set (" + "; ".join(parts) + ")")
|
|
114
|
+
for relative in sorted(actual):
|
|
115
|
+
if relative.endswith(".json"):
|
|
116
|
+
validate_json_document(output_root / relative)
|
|
117
|
+
_parse_formats(output_root, selected_formats, artifact_stem)
|
|
118
|
+
_reconcile_formats(output_root, selected_formats)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def verify_output_root(output_root: Path, project_root: Path | None = None) -> VerificationResult:
|
|
122
|
+
if output_root.is_symlink() or not output_root.is_dir():
|
|
123
|
+
raise ArtifactIntegrityError(f"Generated output is missing or unsafe: {output_root}")
|
|
124
|
+
manifest_path = output_root / BUILD_MANIFEST_RELATIVE
|
|
125
|
+
document = load_build_manifest(manifest_path)
|
|
126
|
+
records = _records_from_manifest(document)
|
|
127
|
+
actual = _inventory(output_root)
|
|
128
|
+
expected = {record.path for record in records} | {BUILD_MANIFEST_RELATIVE}
|
|
129
|
+
if set(actual) != expected:
|
|
130
|
+
missing = sorted(expected - set(actual))
|
|
131
|
+
unexpected = sorted(set(actual) - expected)
|
|
132
|
+
raise ArtifactIntegrityError(f"Artifact inventory mismatch; missing={missing}, unexpected={unexpected}")
|
|
133
|
+
for record in records:
|
|
134
|
+
path = actual[record.path]
|
|
135
|
+
size = path.stat().st_size
|
|
136
|
+
digest = sha256_file(path)
|
|
137
|
+
if size != record.size:
|
|
138
|
+
raise ArtifactIntegrityError(f"Artifact size mismatch for {record.path}: expected {record.size}, got {size}")
|
|
139
|
+
if digest != record.sha256:
|
|
140
|
+
raise ArtifactIntegrityError(f"Artifact SHA-256 mismatch for {record.path}: expected {record.sha256}, got {digest}")
|
|
141
|
+
if path.suffix == ".json":
|
|
142
|
+
value = validate_json_document(path)
|
|
143
|
+
if value["schema_id"] != record.schema_id or value["schema_version"] != record.schema_version:
|
|
144
|
+
raise ArtifactIntegrityError(f"Recorded schema mismatch for {record.path}")
|
|
145
|
+
artifact_hash = semantic_hash([asdict(record) for record in records])
|
|
146
|
+
stable_hash = semantic_hash([asdict(record) for record in records if not record.volatile])
|
|
147
|
+
if document["artifact_set_hash"] != artifact_hash:
|
|
148
|
+
raise ArtifactIntegrityError("Build manifest artifact_set_hash does not reproduce")
|
|
149
|
+
if document["stable_artifact_set_hash"] != stable_hash:
|
|
150
|
+
raise ArtifactIntegrityError("Build manifest stable_artifact_set_hash does not reproduce")
|
|
151
|
+
if project_root is not None:
|
|
152
|
+
_verify_sources(project_root, document)
|
|
153
|
+
selected = _selected_formats(document)
|
|
154
|
+
design = load_design_manifest(output_root / "manifests/design-manifest.json")
|
|
155
|
+
design_without_hash = dict(design)
|
|
156
|
+
recorded_design_hash = design_without_hash.pop("semantic_hash", None)
|
|
157
|
+
if recorded_design_hash != semantic_hash(design_without_hash):
|
|
158
|
+
raise ArtifactIntegrityError("Design manifest semantic_hash does not reproduce")
|
|
159
|
+
if document["design_semantic_hash"] != design["semantic_hash"]:
|
|
160
|
+
raise ArtifactIntegrityError("Build and design manifest semantic hashes disagree")
|
|
161
|
+
validate_export_tree(output_root, selected, str(design["artifact_stem"]))
|
|
162
|
+
return VerificationResult(
|
|
163
|
+
ok=True,
|
|
164
|
+
output_root=output_root,
|
|
165
|
+
issues=(),
|
|
166
|
+
artifacts=records,
|
|
167
|
+
build_manifest=manifest_path,
|
|
168
|
+
artifact_set_hash=artifact_hash,
|
|
169
|
+
stable_artifact_set_hash=stable_hash,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def verify_build_output(project_root: Path) -> VerificationResult:
|
|
174
|
+
from ._locking import project_lock
|
|
175
|
+
from ._transaction import recover_promotion
|
|
176
|
+
from .project_config import load_project_config, output_path, resolve_project_root
|
|
177
|
+
|
|
178
|
+
root = resolve_project_root(project_root)
|
|
179
|
+
config = load_project_config(root)
|
|
180
|
+
output = output_path(root, config)
|
|
181
|
+
with project_lock(root):
|
|
182
|
+
recover_promotion(output, lambda candidate: verify_output_root(candidate))
|
|
183
|
+
return verify_output_root(output, root)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_package_build_info() -> PackageBuildInfo:
|
|
187
|
+
try:
|
|
188
|
+
document = json.loads(resources.files("python_cad_tools").joinpath("build-info.json").read_text(encoding="utf-8"))
|
|
189
|
+
except (FileNotFoundError, json.JSONDecodeError) as error:
|
|
190
|
+
raise ArtifactIntegrityError("The installed distribution has no valid build-info.json") from error
|
|
191
|
+
fields = {"schema_id", "schema_version", "distribution", "version", "source_commit", "source_dirty", "viewer_shell_sha256"}
|
|
192
|
+
if not isinstance(document, dict) or set(document) != fields:
|
|
193
|
+
raise ArtifactIntegrityError("build-info.json must contain an object")
|
|
194
|
+
installed_version = metadata.version("python-cad-tools")
|
|
195
|
+
if (
|
|
196
|
+
document.get("schema_id") != "urn:python-cad-tools:schema:package-build-info:1"
|
|
197
|
+
or document.get("schema_version") != 1
|
|
198
|
+
or document.get("distribution") != "python-cad-tools"
|
|
199
|
+
or document.get("version") != installed_version
|
|
200
|
+
or type(document.get("source_dirty")) is not bool
|
|
201
|
+
or not isinstance(document.get("source_commit"), str)
|
|
202
|
+
or not isinstance(document.get("viewer_shell_sha256"), str)
|
|
203
|
+
or _HEX.fullmatch(document["viewer_shell_sha256"]) is None
|
|
204
|
+
):
|
|
205
|
+
raise ArtifactIntegrityError("build-info.json contains invalid package identity or integrity fields")
|
|
206
|
+
try:
|
|
207
|
+
return PackageBuildInfo(
|
|
208
|
+
schema_id=cast(Any, document["schema_id"]),
|
|
209
|
+
schema_version=cast(Any, document["schema_version"]),
|
|
210
|
+
distribution=cast(Any, document["distribution"]),
|
|
211
|
+
version=installed_version,
|
|
212
|
+
source_commit=str(document["source_commit"]),
|
|
213
|
+
source_dirty=bool(document["source_dirty"]),
|
|
214
|
+
viewer_shell_sha256=str(document["viewer_shell_sha256"]),
|
|
215
|
+
)
|
|
216
|
+
except KeyError as error:
|
|
217
|
+
raise ArtifactIntegrityError(f"build-info.json is missing {error.args[0]!r}") from error
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _records_from_manifest(document: dict[str, Any]) -> tuple[ArtifactRecord, ...]:
|
|
221
|
+
raw = document.get("artifacts")
|
|
222
|
+
if not isinstance(raw, list):
|
|
223
|
+
raise ArtifactIntegrityError("Build manifest artifacts must be an array")
|
|
224
|
+
records: list[ArtifactRecord] = []
|
|
225
|
+
seen: set[str] = set()
|
|
226
|
+
for value in raw:
|
|
227
|
+
if not isinstance(value, dict) or set(value) != _ARTIFACT_FIELDS:
|
|
228
|
+
raise ArtifactIntegrityError("Build manifest contains an invalid artifact record")
|
|
229
|
+
relative = _safe_relative(value.get("path"))
|
|
230
|
+
if relative in seen:
|
|
231
|
+
raise ArtifactIntegrityError(f"Build manifest contains duplicate path: {relative}")
|
|
232
|
+
seen.add(relative)
|
|
233
|
+
if type(value.get("size")) is not int or value["size"] < 0:
|
|
234
|
+
raise ArtifactIntegrityError(f"Invalid artifact size for {relative}")
|
|
235
|
+
if not isinstance(value.get("sha256"), str) or _HEX.fullmatch(value["sha256"]) is None:
|
|
236
|
+
raise ArtifactIntegrityError(f"Invalid artifact SHA-256 for {relative}")
|
|
237
|
+
if not isinstance(value.get("media_type"), str) or not isinstance(value.get("producer"), str):
|
|
238
|
+
raise ArtifactIntegrityError(f"Invalid artifact metadata for {relative}")
|
|
239
|
+
schema_id = value.get("schema_id")
|
|
240
|
+
schema_version = value.get("schema_version")
|
|
241
|
+
if (schema_id is None) != (schema_version is None):
|
|
242
|
+
raise ArtifactIntegrityError(f"Incomplete schema metadata for {relative}")
|
|
243
|
+
if schema_id is not None and (not isinstance(schema_id, str) or type(schema_version) is not int):
|
|
244
|
+
raise ArtifactIntegrityError(f"Invalid schema metadata for {relative}")
|
|
245
|
+
if type(value.get("volatile")) is not bool:
|
|
246
|
+
raise ArtifactIntegrityError(f"Invalid volatile flag for {relative}")
|
|
247
|
+
records.append(ArtifactRecord(**value))
|
|
248
|
+
if [record.path for record in records] != sorted(record.path for record in records):
|
|
249
|
+
raise ArtifactIntegrityError("Build manifest artifact records are not path-sorted")
|
|
250
|
+
volatile = [record.path for record in records if record.volatile]
|
|
251
|
+
if volatile != ["manifests/run-metadata.json"]:
|
|
252
|
+
raise ArtifactIntegrityError(f"Unsupported volatile artifact set: {volatile}")
|
|
253
|
+
return tuple(records)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _inventory(root: Path) -> dict[str, Path]:
|
|
257
|
+
if root.is_symlink() or not root.is_dir():
|
|
258
|
+
raise ArtifactIntegrityError(f"Artifact root is not a regular directory: {root}")
|
|
259
|
+
result: dict[str, Path] = {}
|
|
260
|
+
for directory, names, files in os.walk(root, followlinks=False):
|
|
261
|
+
current = Path(directory)
|
|
262
|
+
for name in sorted(names):
|
|
263
|
+
path = current / name
|
|
264
|
+
if path.is_symlink():
|
|
265
|
+
raise ArtifactIntegrityError(f"Artifact tree contains a symlink: {path.relative_to(root)}")
|
|
266
|
+
for name in sorted(files):
|
|
267
|
+
path = current / name
|
|
268
|
+
if path.is_symlink() or not path.is_file():
|
|
269
|
+
raise ArtifactIntegrityError(f"Artifact is not a regular file: {path.relative_to(root)}")
|
|
270
|
+
relative = _safe_relative(path.relative_to(root).as_posix())
|
|
271
|
+
if relative in result:
|
|
272
|
+
raise ArtifactIntegrityError(f"Duplicate artifact path: {relative}")
|
|
273
|
+
result[relative] = path
|
|
274
|
+
return dict(sorted(result.items()))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _safe_relative(value: object) -> str:
|
|
278
|
+
if not isinstance(value, str) or not value or "\\" in value or "\x00" in value:
|
|
279
|
+
raise ArtifactIntegrityError(f"Unsafe artifact path: {value!r}")
|
|
280
|
+
path = PurePosixPath(value)
|
|
281
|
+
if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts) or path.as_posix() != value:
|
|
282
|
+
raise ArtifactIntegrityError(f"Unsafe artifact path: {value!r}")
|
|
283
|
+
return value
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _expected_paths(formats: tuple[str, ...], stem: str, anchor: bool) -> set[str]:
|
|
287
|
+
expected = {"manifests/design-manifest.json", "manifests/run-metadata.json"}
|
|
288
|
+
if anchor:
|
|
289
|
+
expected.add(".gitkeep")
|
|
290
|
+
if "step" in formats:
|
|
291
|
+
expected |= {f"step/{stem}.step", "step/validation.json"}
|
|
292
|
+
if "ifc" in formats:
|
|
293
|
+
expected |= {f"ifc/{stem}.ifc", "ifc/validation.json"}
|
|
294
|
+
if "glb" in formats:
|
|
295
|
+
expected |= {f"glb/{stem}.glb", "glb/manifest.json"}
|
|
296
|
+
if "quantities" in formats:
|
|
297
|
+
expected |= {
|
|
298
|
+
"quantities/quantities.json",
|
|
299
|
+
"quantities/quantities.csv",
|
|
300
|
+
"quantities/materials.csv",
|
|
301
|
+
"quantities/summary.md",
|
|
302
|
+
}
|
|
303
|
+
if "drawings" in formats:
|
|
304
|
+
expected.add("drawings/annotation-manifest.json")
|
|
305
|
+
expected.add(f"drawings/pdf/{stem}_Conceptual_Drawings.pdf")
|
|
306
|
+
for view in ("plan", "front", "side", "section"):
|
|
307
|
+
expected.add(f"drawings/svg/{stem}_{view}.svg")
|
|
308
|
+
expected.add(f"drawings/dxf/{stem}_{view}.dxf")
|
|
309
|
+
return expected
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _parse_formats(root: Path, formats: tuple[str, ...], stem: str) -> None:
|
|
313
|
+
try:
|
|
314
|
+
if "step" in formats:
|
|
315
|
+
from build123d import import_step
|
|
316
|
+
|
|
317
|
+
from .exporters.step import step_schema
|
|
318
|
+
|
|
319
|
+
step = root / f"step/{stem}.step"
|
|
320
|
+
if not step_schema(step).startswith("AP242_") or not import_step(step).is_valid:
|
|
321
|
+
raise ArtifactIntegrityError("STEP parser validation failed")
|
|
322
|
+
if "ifc" in formats:
|
|
323
|
+
import ifcopenshell
|
|
324
|
+
|
|
325
|
+
value = ifcopenshell.open(root / f"ifc/{stem}.ifc")
|
|
326
|
+
if value.schema != "IFC4":
|
|
327
|
+
raise ArtifactIntegrityError(f"Unsupported emitted IFC schema: {value.schema}")
|
|
328
|
+
if "glb" in formats:
|
|
329
|
+
import trimesh
|
|
330
|
+
|
|
331
|
+
from .exporters.glb import glb_json
|
|
332
|
+
|
|
333
|
+
glb = root / f"glb/{stem}.glb"
|
|
334
|
+
glb_json(glb)
|
|
335
|
+
if not isinstance(trimesh.load(glb, force="scene", process=False), trimesh.Scene):
|
|
336
|
+
raise ArtifactIntegrityError("GLB parser did not return a scene")
|
|
337
|
+
if "drawings" in formats:
|
|
338
|
+
import ezdxf
|
|
339
|
+
from pypdf import PdfReader
|
|
340
|
+
|
|
341
|
+
for svg in sorted((root / "drawings/svg").glob("*.svg")):
|
|
342
|
+
ElementTree.parse(svg)
|
|
343
|
+
for dxf in sorted((root / "drawings/dxf").glob("*.dxf")):
|
|
344
|
+
if ezdxf.readfile(dxf).audit().has_errors:
|
|
345
|
+
raise ArtifactIntegrityError(f"DXF audit failed: {dxf.name}")
|
|
346
|
+
if not PdfReader(root / f"drawings/pdf/{stem}_Conceptual_Drawings.pdf").pages:
|
|
347
|
+
raise ArtifactIntegrityError("PDF contains no pages")
|
|
348
|
+
_reconcile_drawing_annotations(root, stem)
|
|
349
|
+
except ArtifactIntegrityError:
|
|
350
|
+
raise
|
|
351
|
+
except (OSError, ValueError, RuntimeError, BuildExportError) as error:
|
|
352
|
+
raise ArtifactIntegrityError(f"Final artifact parser validation failed: {error}") from error
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _reconcile_formats(root: Path, formats: tuple[str, ...]) -> None:
|
|
356
|
+
design = load_design_manifest(root / "manifests/design-manifest.json")
|
|
357
|
+
elements = design["elements"]
|
|
358
|
+
if not isinstance(elements, list):
|
|
359
|
+
raise ArtifactIntegrityError("Design manifest elements must be an array")
|
|
360
|
+
if "step" in formats:
|
|
361
|
+
report = load_json_document(root / "step/validation.json")
|
|
362
|
+
expected_elements = [item for item in elements if item["physical"] and "step" in item["export_formats"]]
|
|
363
|
+
if report["solid_count"] != len(expected_elements):
|
|
364
|
+
raise ArtifactIntegrityError("STEP/design solid-count mismatch")
|
|
365
|
+
_require_matching_bounds("STEP", report["bounds_mm"], _combined_manifest_bounds(expected_elements))
|
|
366
|
+
if "ifc" in formats:
|
|
367
|
+
import ifcopenshell
|
|
368
|
+
|
|
369
|
+
from .exporters.ifc import property_value
|
|
370
|
+
|
|
371
|
+
ifc_path = next((root / "ifc").glob("*.ifc"))
|
|
372
|
+
actual = sorted(property_value(item, "StableId") for item in ifcopenshell.open(ifc_path).by_type("IfcBuildingElementProxy"))
|
|
373
|
+
expected = sorted(
|
|
374
|
+
item["id"]
|
|
375
|
+
for item in elements
|
|
376
|
+
if item["physical"] and "ifc" in item["export_formats"] and item["ifc_class"] is not None
|
|
377
|
+
)
|
|
378
|
+
if actual != expected:
|
|
379
|
+
raise ArtifactIntegrityError(f"IFC/design identity mismatch: expected {expected}, got {actual}")
|
|
380
|
+
report = load_json_document(root / "ifc/validation.json")
|
|
381
|
+
expected_elements = [
|
|
382
|
+
item
|
|
383
|
+
for item in elements
|
|
384
|
+
if item["physical"] and "ifc" in item["export_formats"] and item["ifc_class"] is not None
|
|
385
|
+
]
|
|
386
|
+
_require_matching_bounds("IFC", report["bounds_mm"], _combined_manifest_bounds(expected_elements))
|
|
387
|
+
if "glb" in formats:
|
|
388
|
+
from .exporters.glb import glb_json
|
|
389
|
+
|
|
390
|
+
glb_path = next((root / "glb").glob("*.glb"))
|
|
391
|
+
document = glb_json(glb_path)
|
|
392
|
+
actual = sorted(node.get("extras", {}).get("stable_id") for node in document["nodes"] if node.get("extras", {}).get("stable_id"))
|
|
393
|
+
expected = sorted(item["id"] for item in elements if item["physical"] and "glb" in item["export_formats"])
|
|
394
|
+
if actual != expected:
|
|
395
|
+
raise ArtifactIntegrityError(f"GLB/design identity mismatch: expected {expected}, got {actual}")
|
|
396
|
+
report = load_json_document(root / "glb/manifest.json")
|
|
397
|
+
expected_elements = [item for item in elements if item["physical"] and "glb" in item["export_formats"]]
|
|
398
|
+
_require_matching_bounds("GLB", report["bounds_cad_mm"], _combined_manifest_bounds(expected_elements))
|
|
399
|
+
if "quantities" in formats:
|
|
400
|
+
quantities = load_json_document(root / "quantities/quantities.json")
|
|
401
|
+
actual = sorted(record["element_id"] for record in quantities["records"])
|
|
402
|
+
expected = sorted(item["id"] for item in elements if item["physical"] and "quantities" in item["export_formats"])
|
|
403
|
+
if actual != expected:
|
|
404
|
+
raise ArtifactIntegrityError(f"Quantity/design identity mismatch: expected {expected}, got {actual}")
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _reconcile_drawing_annotations(root: Path, stem: str) -> None:
|
|
408
|
+
import ezdxf
|
|
409
|
+
from pypdf import PdfReader
|
|
410
|
+
|
|
411
|
+
manifest = load_json_document(root / "drawings/annotation-manifest.json")
|
|
412
|
+
sheets = manifest.get("sheets")
|
|
413
|
+
annotations = manifest.get("annotations")
|
|
414
|
+
if not isinstance(sheets, list) or not isinstance(annotations, list):
|
|
415
|
+
raise ArtifactIntegrityError("Drawing annotation manifest arrays are invalid")
|
|
416
|
+
rendered = {record["id"] for record in annotations if record.get("rendered_entity") is True}
|
|
417
|
+
declared_by_sheet: set[str] = set()
|
|
418
|
+
reader = PdfReader(root / f"drawings/pdf/{stem}_Conceptual_Drawings.pdf")
|
|
419
|
+
if len(reader.pages) != len(sheets):
|
|
420
|
+
raise ArtifactIntegrityError("Drawing annotation manifest/PDF page-count mismatch")
|
|
421
|
+
for expected_page, sheet in enumerate(sheets, start=1):
|
|
422
|
+
if not isinstance(sheet, dict):
|
|
423
|
+
raise ArtifactIntegrityError("Drawing annotation manifest contains an invalid sheet record")
|
|
424
|
+
sheet_id = sheet.get("sheet_id")
|
|
425
|
+
view_id = sheet.get("view_id")
|
|
426
|
+
page_number = sheet.get("page_number")
|
|
427
|
+
expected_ids = sheet.get("annotation_ids")
|
|
428
|
+
expected_text = sheet.get("pdf_text")
|
|
429
|
+
if (
|
|
430
|
+
not isinstance(sheet_id, str)
|
|
431
|
+
or not isinstance(view_id, str)
|
|
432
|
+
or page_number != expected_page
|
|
433
|
+
or not isinstance(expected_ids, list)
|
|
434
|
+
or not all(isinstance(value, str) for value in expected_ids)
|
|
435
|
+
or not isinstance(expected_text, list)
|
|
436
|
+
or not all(isinstance(value, str) for value in expected_text)
|
|
437
|
+
):
|
|
438
|
+
raise ArtifactIntegrityError("Drawing annotation manifest sheet mapping is invalid")
|
|
439
|
+
svg = root / f"drawings/svg/{stem}_{view_id}.svg"
|
|
440
|
+
svg_root = ElementTree.parse(svg).getroot()
|
|
441
|
+
if svg_root.get("data-sheet") != sheet_id:
|
|
442
|
+
raise ArtifactIntegrityError(f"SVG sheet identity mismatch for {view_id}")
|
|
443
|
+
svg_ids = sorted(
|
|
444
|
+
element.attrib["data-annotation-id"]
|
|
445
|
+
for element in svg_root.iter()
|
|
446
|
+
if "data-annotation-id" in element.attrib
|
|
447
|
+
)
|
|
448
|
+
dxf = ezdxf.readfile(root / f"drawings/dxf/{stem}_{view_id}.dxf")
|
|
449
|
+
dxf_ids: list[str] = []
|
|
450
|
+
for entity in dxf.modelspace():
|
|
451
|
+
if entity.has_xdata("PYTHON_CAD_TOOLS"):
|
|
452
|
+
tags = entity.get_xdata("PYTHON_CAD_TOOLS")
|
|
453
|
+
dxf_ids.extend(str(tag.value) for tag in tags if tag.code == 1000)
|
|
454
|
+
if expected_ids != sorted(expected_ids) or svg_ids != expected_ids or sorted(dxf_ids) != expected_ids:
|
|
455
|
+
raise ArtifactIntegrityError(
|
|
456
|
+
f"Drawing annotation identity mismatch on {sheet_id}: expected {expected_ids}, SVG {svg_ids}, DXF {sorted(dxf_ids)}"
|
|
457
|
+
)
|
|
458
|
+
declared_by_sheet.update(expected_ids)
|
|
459
|
+
extracted = _normalized_text(reader.pages[expected_page - 1].extract_text() or "")
|
|
460
|
+
for value in expected_text:
|
|
461
|
+
if _normalized_text(value) not in extracted:
|
|
462
|
+
raise ArtifactIntegrityError(
|
|
463
|
+
f"Drawing annotation PDF text mismatch on {sheet_id}: missing {value!r}"
|
|
464
|
+
)
|
|
465
|
+
if declared_by_sheet != rendered:
|
|
466
|
+
raise ArtifactIntegrityError("Drawing annotation sidecar rendered-entity inventory is inconsistent")
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _normalized_text(value: str) -> str:
|
|
470
|
+
return " ".join(value.split())
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _combined_manifest_bounds(elements: list[dict[str, Any]]) -> list[float]:
|
|
474
|
+
if not elements:
|
|
475
|
+
return [0.0] * 6
|
|
476
|
+
return [
|
|
477
|
+
round(min(float(item["bounds_mm"][index]) for item in elements), 6)
|
|
478
|
+
for index in range(3)
|
|
479
|
+
] + [
|
|
480
|
+
round(max(float(item["bounds_mm"][index]) for item in elements), 6)
|
|
481
|
+
for index in range(3, 6)
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _require_matching_bounds(label: str, actual: object, expected: list[float]) -> None:
|
|
486
|
+
if not isinstance(actual, list) or len(actual) != 6:
|
|
487
|
+
raise ArtifactIntegrityError(f"{label} bounds are missing or invalid")
|
|
488
|
+
try:
|
|
489
|
+
matches = all(abs(float(left) - right) <= 1e-6 for left, right in zip(actual, expected, strict=True))
|
|
490
|
+
except (TypeError, ValueError):
|
|
491
|
+
matches = False
|
|
492
|
+
if not matches:
|
|
493
|
+
raise ArtifactIntegrityError(f"{label}/design bounds mismatch: expected {expected}, got {actual}")
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _verify_sources(project_root: Path, document: dict[str, Any]) -> None:
|
|
497
|
+
raw = document.get("source_inputs")
|
|
498
|
+
if not isinstance(raw, list):
|
|
499
|
+
raise ArtifactIntegrityError("Build manifest source_inputs must be an array")
|
|
500
|
+
current = []
|
|
501
|
+
for record in raw:
|
|
502
|
+
if not isinstance(record, dict) or set(record) != {"path", "size", "sha256"}:
|
|
503
|
+
raise ArtifactIntegrityError("Invalid source input record")
|
|
504
|
+
relative = _safe_relative(record["path"])
|
|
505
|
+
path = project_root / PurePosixPath(relative)
|
|
506
|
+
if path.is_symlink() or not path.is_file() or not path.resolve().is_relative_to(project_root):
|
|
507
|
+
raise ArtifactIntegrityError(f"Authoritative source is missing or unsafe: {relative}")
|
|
508
|
+
current.append({"path": relative, "size": path.stat().st_size, "sha256": sha256_file(path)})
|
|
509
|
+
if current != raw or semantic_hash(current) != document.get("source_set_hash"):
|
|
510
|
+
raise ArtifactIntegrityError("Authoritative project sources differ from the verified build")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _selected_formats(document: dict[str, Any]) -> tuple[str, ...]:
|
|
514
|
+
from .project_config import FORMAT_ORDER
|
|
515
|
+
|
|
516
|
+
raw = document.get("selected_formats")
|
|
517
|
+
if not isinstance(raw, list) or not all(isinstance(item, str) for item in raw):
|
|
518
|
+
raise ArtifactIntegrityError("Build manifest selected_formats is invalid")
|
|
519
|
+
if not raw or len(raw) != len(set(raw)) or any(item not in FORMAT_ORDER for item in raw):
|
|
520
|
+
raise ArtifactIntegrityError("Build manifest selected_formats contains an unsupported or duplicate format")
|
|
521
|
+
if raw != [item for item in FORMAT_ORDER if item in raw]:
|
|
522
|
+
raise ArtifactIntegrityError("Build manifest selected_formats is not in canonical order")
|
|
523
|
+
return tuple(raw)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _media_type(path: Path) -> str:
|
|
527
|
+
explicit = {
|
|
528
|
+
".step": "model/step",
|
|
529
|
+
".ifc": "application/x-step",
|
|
530
|
+
".glb": "model/gltf-binary",
|
|
531
|
+
".dxf": "image/vnd.dxf",
|
|
532
|
+
".csv": "text/csv; charset=utf-8",
|
|
533
|
+
".md": "text/markdown; charset=utf-8",
|
|
534
|
+
}
|
|
535
|
+
return explicit.get(path.suffix.lower()) or mimetypes.guess_type(path.name)[0] or "application/octet-stream"
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
__all__ = [
|
|
539
|
+
"ArtifactRecord",
|
|
540
|
+
"PackageBuildInfo",
|
|
541
|
+
"VerificationResult",
|
|
542
|
+
"get_package_build_info",
|
|
543
|
+
"load_build_manifest",
|
|
544
|
+
"load_design_manifest",
|
|
545
|
+
"verify_build_output",
|
|
546
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"distribution":"python-cad-tools","schema_id":"urn:python-cad-tools:schema:package-build-info:1","schema_version":1,"source_commit":"2266dd501edbb417c10eba356311808f5d29cce0","source_dirty":true,"version":"0.1.0rc1","viewer_shell_sha256":"5be46f14101b955176328ba78760be7ebaee08a3b73ea76a884d11ce38b25882"}
|